├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── benchmarks ├── __init__.py ├── common.py ├── time_filter.py ├── time_filter_fast.py └── time_map.py ├── proto ├── RequestsCommon.proto ├── TimeFilterFastRequest.proto └── TimeFilterFastResponse.proto ├── pyproject.toml ├── tests ├── __init__.py ├── conftest.py ├── distance_map_test.py ├── geocoding_test.py ├── geohash_fast_test.py ├── geohash_test.py ├── h3_fast_test.py ├── h3_test.py ├── map_info_test.py ├── postcodes_test.py ├── routes_test.py ├── supported_locations.py ├── time_filter_fast_test.py ├── time_filter_proto_test.py ├── time_filter_test.py ├── time_map_fast_test.py ├── time_map_test.py ├── wkt_parsing_test.py ├── wkt_pretty_print_test.py └── wkt_validate_objects_test.py └── traveltimepy ├── __init__.py ├── accept_type.py ├── dto ├── __init__.py ├── common.py ├── requests │ ├── __init__.py │ ├── distance_map.py │ ├── geohash.py │ ├── geohash_fast.py │ ├── h3.py │ ├── h3_fast.py │ ├── postcodes.py │ ├── postcodes_zones.py │ ├── request.py │ ├── routes.py │ ├── supported_locations.py │ ├── time_filter.py │ ├── time_filter_fast.py │ ├── time_filter_proto.py │ ├── time_map.py │ ├── time_map_fast.py │ ├── time_map_fast_geojson.py │ ├── time_map_fast_wkt.py │ ├── time_map_geojson.py │ └── time_map_wkt.py ├── responses │ ├── __init__.py │ ├── error.py │ ├── geohash.py │ ├── h3.py │ ├── map_info.py │ ├── postcodes.py │ ├── routes.py │ ├── supported_locations.py │ ├── time_filter.py │ ├── time_filter_fast.py │ ├── time_filter_proto.py │ ├── time_map.py │ ├── time_map_wkt.py │ └── zones.py └── transportation.py ├── errors.py ├── http.py ├── itertools.py ├── mapper.py ├── proto_http.py ├── py.typed ├── sdk.py └── wkt ├── __init__.py ├── constants.py ├── error.py ├── geometries.py ├── helper.py └── parsing.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "13:00" 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Unit Tests 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master, new_sdk_version] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | max-parallel: 1 18 | matrix: 19 | python-version: 20 | - "3.8" 21 | - "3.9" 22 | - "3.10" 23 | - "3.11" 24 | - "3.12" 25 | steps: 26 | - uses: actions/checkout@v3 27 | with: 28 | persist-credentials: false 29 | - name: Set up Python ${{ matrix.python-version }} 30 | uses: actions/setup-python@v4 31 | with: 32 | python-version: ${{ matrix.python-version }} 33 | 34 | - name: Install Package with dependencies 35 | run: | 36 | pip install -U setuptools 37 | pip install -e ".[test]" 38 | - name: Lint with flake8 39 | run: | 40 | flake8 --max-line-length=127 41 | - name: Formatting check with black 42 | run: | 43 | black . --check --diff 44 | - name: Type check with mypy 45 | run: | 46 | mypy --install-types --non-interactive . 47 | mypy . 48 | 49 | - name: Set up protoc 50 | uses: arduino/setup-protoc@v1 51 | with: 52 | version: '3.x' 53 | 54 | - name: Generate proto files 55 | run: | 56 | protoc -I=proto/ --python_out=. proto/*.proto 57 | 58 | - name: Test with pytest 59 | run: | 60 | pytest 61 | env: 62 | PROTO_APP_ID: ${{ secrets.PROTO_APP_ID }} 63 | PROTO_API_KEY: ${{ secrets.PROTO_API_KEY }} 64 | APP_ID: ${{ secrets.APP_ID }} 65 | API_KEY: ${{ secrets.API_KEY }} 66 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Build & Upload package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | persist-credentials: false 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: "3.10" 22 | 23 | - name: Set up protoc 24 | uses: arduino/setup-protoc@v1 25 | with: 26 | version: '3.x' 27 | 28 | - name: Generate proto files 29 | run: | 30 | protoc -I=proto/ --python_out=. proto/*.proto 31 | 32 | - name: Install setuptools 33 | run: | 34 | python -m pip install --upgrade pip 35 | pip install build setuptools twine 36 | 37 | - name: Build and publish 38 | env: 39 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 40 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 41 | run: | 42 | python -m build 43 | twine upload dist/* 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/intellij,python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=intellij,python 3 | 4 | ### Intellij ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | .idea/ 9 | 10 | 11 | # CMake 12 | cmake-build-*/ 13 | 14 | # File-based project format 15 | *.iws 16 | 17 | # IntelliJ 18 | out/ 19 | 20 | # mpeltonen/sbt-idea plugin 21 | .idea_modules/ 22 | 23 | # JIRA plugin 24 | atlassian-ide-plugin.xml 25 | 26 | ### Python ### 27 | # Byte-compiled / optimized / DLL files 28 | __pycache__/ 29 | *.py[cod] 30 | *$py.class 31 | 32 | # C extensions 33 | *.so 34 | 35 | # Distribution / packaging 36 | .Python 37 | build/ 38 | develop-eggs/ 39 | dist/ 40 | downloads/ 41 | eggs/ 42 | .eggs/ 43 | lib/ 44 | lib64/ 45 | parts/ 46 | sdist/ 47 | var/ 48 | wheels/ 49 | share/python-wheels/ 50 | *.egg-info/ 51 | .installed.cfg 52 | *.egg 53 | MANIFEST 54 | 55 | # PyInstaller 56 | # Usually these files are written by a python script from a template 57 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 58 | *.manifest 59 | *.spec 60 | 61 | # Installer logs 62 | pip-log.txt 63 | pip-delete-this-directory.txt 64 | 65 | # Unit test / coverage reports 66 | htmlcov/ 67 | .tox/ 68 | .nox/ 69 | .coverage 70 | .coverage.* 71 | .cache 72 | nosetests.xml 73 | coverage.xml 74 | *.cover 75 | *.py,cover 76 | .hypothesis/ 77 | .pytest_cache/ 78 | cover/ 79 | 80 | # Translations 81 | *.mo 82 | *.pot 83 | 84 | # Django stuff: 85 | *.log 86 | local_settings.py 87 | db.sqlite3 88 | db.sqlite3-journal 89 | 90 | # Flask stuff: 91 | instance/ 92 | .webassets-cache 93 | 94 | # Scrapy stuff: 95 | .scrapy 96 | 97 | # Sphinx documentation 98 | docs/_build/ 99 | 100 | # PyBuilder 101 | .pybuilder/ 102 | target/ 103 | 104 | # Jupyter Notebook 105 | .ipynb_checkpoints 106 | 107 | # IPython 108 | profile_default/ 109 | ipython_config.py 110 | 111 | # pyenv 112 | # For a library or package, you might want to ignore these files since the code is 113 | # intended to run in multiple environments; otherwise, check them in: 114 | # .python-version 115 | 116 | # pipenv 117 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 118 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 119 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 120 | # install all needed dependencies. 121 | #Pipfile.lock 122 | 123 | # poetry 124 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 125 | # This is especially recommended for binary packages to ensure reproducibility, and is more 126 | # commonly ignored for libraries. 127 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 128 | #poetry.lock 129 | 130 | # pdm 131 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 132 | #pdm.lock 133 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 134 | # in version control. 135 | # https://pdm.fming.dev/#use-with-ide 136 | .pdm.toml 137 | 138 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 139 | __pypackages__/ 140 | 141 | # Celery stuff 142 | celerybeat-schedule 143 | celerybeat.pid 144 | 145 | # SageMath parsed files 146 | *.sage.py 147 | 148 | # Environments 149 | .env 150 | .venv 151 | env/ 152 | venv/ 153 | ENV/ 154 | env.bak/ 155 | venv.bak/ 156 | 157 | # Spyder project settings 158 | .spyderproject 159 | .spyproject 160 | 161 | # Rope project settings 162 | .ropeproject 163 | 164 | # mkdocs documentation 165 | /site 166 | 167 | # mypy 168 | .mypy_cache/ 169 | .dmypy.json 170 | dmypy.json 171 | 172 | # Pyre type checker 173 | .pyre/ 174 | 175 | # pytype static type analyzer 176 | .pytype/ 177 | 178 | # Cython debug symbols 179 | cython_debug/ 180 | 181 | # Proto 182 | *_pb2.py 183 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 TravelTime 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /benchmarks/__init__.py: -------------------------------------------------------------------------------- 1 | """Module for benchmarks""" 2 | -------------------------------------------------------------------------------- /benchmarks/common.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import List 3 | 4 | from traveltimepy import Location, Coordinates 5 | 6 | 7 | def generate_float(value: float, radius: float) -> float: 8 | return random.uniform(value - radius, value + radius) 9 | 10 | 11 | def generate_locations( 12 | lat: float, lng: float, radius: float, name: str, amount: int 13 | ) -> List[Location]: 14 | return [ 15 | Location( 16 | id="{} {}".format(name, i), 17 | coords=Coordinates( 18 | lat=generate_float(lat, radius), lng=generate_float(lng, radius) 19 | ), 20 | ) 21 | for i in range(amount) 22 | ] 23 | 24 | 25 | def generate_coordinates( 26 | lat: float, lng: float, radius: float, amount: int 27 | ) -> List[Coordinates]: 28 | return [ 29 | Coordinates(lat=generate_float(lat, radius), lng=generate_float(lng, radius)) 30 | for _ in range(amount) 31 | ] 32 | -------------------------------------------------------------------------------- /benchmarks/time_filter.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | from datetime import datetime 4 | 5 | from benchmarks.common import generate_locations 6 | from traveltimepy import TravelTimeSdk, Driving 7 | 8 | 9 | async def generate_matrix(size: int): 10 | sdk = TravelTimeSdk("APP_ID", "API_KEY") 11 | locations = generate_locations(51.507609, -0.128315, 0.05, "Location", size) 12 | location_ids = [location.id for location in locations] 13 | search_ids = [ 14 | (location_id, list(filter(lambda cur_id: cur_id != location_id, location_ids))) 15 | for location_id in location_ids 16 | ] 17 | return await sdk.time_filter_async( 18 | locations=locations, 19 | search_ids=dict(search_ids), 20 | transportation=Driving(), 21 | arrival_time=datetime.now(), 22 | ) 23 | 24 | 25 | if __name__ == "__main__": 26 | start = time.perf_counter() 27 | response = asyncio.run(generate_matrix(50)) 28 | request_time = time.perf_counter() - start 29 | print("Request completed in {0:.0f}s for matrix 50 * 50".format(request_time)) 30 | 31 | start = time.perf_counter() 32 | response2 = asyncio.run(generate_matrix(100)) 33 | request_time = time.perf_counter() - start 34 | print("Request completed in {0:.0f}s for matrix 100 * 100".format(request_time)) 35 | 36 | start = time.perf_counter() 37 | response3 = asyncio.run(generate_matrix(300)) 38 | request_time = time.perf_counter() - start 39 | print("Request completed in {0:.0f}s for matrix 300 * 300".format(request_time)) 40 | -------------------------------------------------------------------------------- /benchmarks/time_filter_fast.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | 4 | from benchmarks.common import generate_locations 5 | from traveltimepy import TravelTimeSdk, Transportation 6 | 7 | 8 | async def generate_matrix(size: int): 9 | sdk = TravelTimeSdk("APP_ID", "API_KEY") 10 | locations = generate_locations(51.507609, -0.128315, 0.05, "Location", size) 11 | location_ids = [location.id for location in locations] 12 | search_ids = [ 13 | (location_id, list(filter(lambda cur_id: cur_id != location_id, location_ids))) 14 | for location_id in location_ids 15 | ] 16 | return await sdk.time_filter_fast_async( 17 | locations=locations, 18 | search_ids=dict(search_ids), 19 | transportation=Transportation(type="driving"), 20 | ) 21 | 22 | 23 | if __name__ == "__main__": 24 | start = time.perf_counter() 25 | response = asyncio.run(generate_matrix(50)) 26 | request_time = time.perf_counter() - start 27 | print("Request completed in {0:.0f}s for matrix 50 * 50".format(request_time)) 28 | 29 | start = time.perf_counter() 30 | response2 = asyncio.run(generate_matrix(100)) 31 | request_time = time.perf_counter() - start 32 | print("Request completed in {0:.0f}s for matrix 100 * 100".format(request_time)) 33 | 34 | start = time.perf_counter() 35 | response3 = asyncio.run(generate_matrix(300)) 36 | request_time = time.perf_counter() - start 37 | print("Request completed in {0:.0f}s for matrix 300 * 300".format(request_time)) 38 | -------------------------------------------------------------------------------- /benchmarks/time_map.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | from datetime import datetime 4 | 5 | from benchmarks.common import generate_coordinates 6 | from traveltimepy import TravelTimeSdk, Driving 7 | 8 | 9 | async def generate_isochrones(size: int): 10 | sdk = TravelTimeSdk("APP_ID", "API_KEY") 11 | coordinates = generate_coordinates(51.507609, -0.128315, 0.05, size) 12 | return await sdk.time_map_async( 13 | coordinates=coordinates, transportation=Driving(), arrival_time=datetime.now() 14 | ) 15 | 16 | 17 | if __name__ == "__main__": 18 | start = time.perf_counter() 19 | response = asyncio.run(generate_isochrones(50)) 20 | request_time = time.perf_counter() - start 21 | print("Request completed in {0:.0f}s for 50 isochrones".format(request_time)) 22 | 23 | start = time.perf_counter() 24 | response2 = asyncio.run(generate_isochrones(100)) 25 | request_time = time.perf_counter() - start 26 | print("Request completed in {0:.0f}s for 100 isochrones".format(request_time)) 27 | 28 | start = time.perf_counter() 29 | response3 = asyncio.run(generate_isochrones(300)) 30 | request_time = time.perf_counter() - start 31 | print("Request completed in {0:.0f}s for 300 isochrones".format(request_time)) 32 | -------------------------------------------------------------------------------- /proto/RequestsCommon.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package com.igeolise.traveltime.rabbitmq.requests; 4 | 5 | message Coords { 6 | float lat = 1; 7 | float lng = 2; 8 | } 9 | 10 | message Transportation { 11 | TransportationType type = 1; 12 | 13 | // override defaults for some of the transportation modes 14 | oneof transportationDetails { 15 | PublicTransportDetails publicTransport = 2; 16 | DrivingAndPublicTransportDetails drivingAndPublicTransport = 3; 17 | } 18 | } 19 | 20 | // semantically valid only for TransportationType.PUBLIC_TRANSPORT 21 | message PublicTransportDetails { 22 | // limits the possible duration of walking paths 23 | // 24 | // walkingTimeToStation limit is of low precedence and will not override the global 25 | // travel time limit 26 | // 27 | // walkingTimeToStation must be 0 > and <= 1800 28 | // if walkingTimeToStation is not set: use service default 29 | // if walkingTimeToStation > 0: limit the path to at most this value 30 | OptionalPositiveUInt32 walkingTimeToStation = 1; 31 | } 32 | 33 | // semantically valid only for TransportationType.DRIVING_AND_PUBLIC_TRANSPORT 34 | message DrivingAndPublicTransportDetails { 35 | // limits the possible duration of walking paths 36 | // 37 | // walkingTimeToStation limit is of low precedence and will not override the global 38 | // travel time limit 39 | // 40 | // walkingTimeToStation must be > 0 and <= 1800 41 | // if walkingTimeToStation is not set: use service default 42 | // if walkingTimeToStation > 0: limit the walking path to at most this value 43 | OptionalPositiveUInt32 walkingTimeToStation = 1; 44 | 45 | // limits the possible duration of driving paths 46 | // 47 | // drivingTimeToStation limit is of low precedence and will not override the global 48 | // travel time limit 49 | // 50 | // drivingTimeToStation must be > 0 and <= 1800 51 | // if drivingTimeToStation is not set: use service default 52 | // if drivingTimeToStation > 0: limit the path to at most this value 53 | OptionalPositiveUInt32 drivingTimeToStation = 2; 54 | 55 | // constant penalty to apply to simulate the difficulty of finding a parking 56 | // spot 57 | // 58 | // if parkingTime < 0: use service default (300s) 59 | // if parkingTime >= 0: apply the parking penalty when searching for possible 60 | // paths 61 | // 62 | // NOTE: parkingTime penalty cannot be greater than the global travel time 63 | // limit 64 | OptionalNonNegativeUInt32 parkingTime = 3; 65 | } 66 | 67 | enum TransportationType { 68 | // Considers all paths found by the following steps: 69 | // * up to 30 minutes of walking (always included even if no stops found) 70 | // * all connections in the 30 minute walking range from public transport 71 | // stops to other public transport stops in travel_time_limit, AND 72 | // * up to 30 minutes of walking from public transport stops that were visited 73 | // by public transport (IOW a path 74 | // [origin]--walking->[stop]--walking-->[destination] is not possible but 75 | // [origin]--walking->[stop]--public_transport-->[stop]--walking--> is. 76 | PUBLIC_TRANSPORT = 0; 77 | // Considers all paths found traveling by car from origin(s) to 78 | // destination(s) within the travel_time_limit 79 | DRIVING = 1; 80 | // Considers all paths found by the following steps: 81 | // * up to 30 minutes of driving (always included even no stops found) 82 | // * all connections in the 30 minute driving range from public transport stops 83 | // to other public transport stops in travel_time_limit, AND 84 | // * up to 30 minutes of walking from public transport stops that were visited 85 | // by public transport (IOW a path 86 | // [origin]--driving->[stop]--walking-->[destination] is not possible but 87 | // [origin]--driving->[stop]--public_transport-->[stop]--walking--> is. 88 | // AND/OR 89 | // * up to 30 minutes of walking 90 | // 91 | DRIVING_AND_PUBLIC_TRANSPORT = 2; 92 | // Considers all paths found travelling by car from origin(s) to 93 | // destination(s) including all paths that are traversable by ferries that 94 | // take cars within the travel_time_limit. 95 | DRIVING_AND_FERRY = 3; 96 | // Considers all paths found travelling by foot from origin(s) to 97 | // destination(s) within the travel_time_limit 98 | WALKING = 4; 99 | // Considers all paths found travelling by foot from origin(s) to 100 | // destination(s) including all paths that are traversable by ferries that 101 | // take passengers within the travel_time_limit 102 | WALKING_AND_FERRY = 7; 103 | // Considers all paths found travelling by bike from origin(s) to 104 | // destination(s) within the travel_time_limit 105 | CYCLING = 5; 106 | // Considers all paths found travelling by bike from origin(s) to 107 | // destination(s) including all paths that are traversable by ferries that 108 | // take bikes within the travel_time_limit 109 | CYCLING_AND_FERRY = 6; 110 | } 111 | 112 | enum TimePeriod { 113 | WEEKDAY_MORNING = 0; 114 | } 115 | 116 | enum CellPropertyType { 117 | MEAN = 0; 118 | MIN = 1; 119 | MAX = 2; 120 | } 121 | 122 | // represents an optional positive (strictly greater than 0) uint32 parameter 123 | // 124 | // the positive requirement cannot be checked at the protocol level and will 125 | // only be verified by the server 126 | message OptionalPositiveUInt32 { 127 | uint32 value = 1; 128 | } 129 | 130 | // represents an optional non negative (greater or equal than 0) uint32 parameter 131 | // 132 | // the non negative requirement cannot be checked at the protocol level and will only 133 | // be verified by the server 134 | message OptionalNonNegativeUInt32 { 135 | uint32 value = 1; 136 | } 137 | -------------------------------------------------------------------------------- /proto/TimeFilterFastRequest.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package com.igeolise.traveltime.rabbitmq.requests; 4 | 5 | import "RequestsCommon.proto"; 6 | 7 | message TimeFilterFastRequest { 8 | enum Property { 9 | FARES = 0; 10 | DISTANCES = 1; 11 | } 12 | message OneToMany { 13 | Coords departureLocation = 1; 14 | /* 15 | * We encode arrival locations as deltas (relative to the source) using a fixedpoint encoding i.e 16 | * deltaLat = round((lat - sourceLat) * 10^5).toInt 17 | * deltaLon = round((lon - sourceLon) * 10^5).toInt 18 | * 19 | * The deltas should be interleaved in the `locationDeltas` field i.e 20 | * 21 | * locationDeltas[0] should be the first lat 22 | * locationDeltas[1] should be the first lon 23 | * locationDeltas[2] should be the second lat 24 | * ... 25 | * etc 26 | */ 27 | repeated sint32 locationDeltas = 2; 28 | Transportation transportation = 3; 29 | TimePeriod arrivalTimePeriod = 4; 30 | sint32 travelTime = 5; 31 | repeated Property properties = 6; 32 | } 33 | 34 | message ManyToOne { 35 | Coords arrivalLocation = 1; 36 | /* 37 | * We encode arrival locations as deltas (relative to the source) using a fixedpoint encoding i.e 38 | * deltaLat = round((lat - sourceLat) * 10^5).toInt 39 | * deltaLon = round((lon - sourceLon) * 10^5).toInt 40 | * 41 | * The deltas should be interleaved in the `locationDeltas` field i.e 42 | * 43 | * locationDeltas[0] should be the first lat 44 | * locationDeltas[1] should be the first lon 45 | * locationDeltas[2] should be the second lat 46 | * ... 47 | * etc 48 | */ 49 | repeated sint32 locationDeltas = 2; 50 | Transportation transportation = 3; 51 | TimePeriod arrivalTimePeriod = 4; 52 | sint32 travelTime = 5; 53 | repeated Property properties = 6; 54 | } 55 | 56 | OneToMany oneToManyRequest = 1; 57 | ManyToOne manyToOneRequest = 2; 58 | } 59 | 60 | -------------------------------------------------------------------------------- /proto/TimeFilterFastResponse.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package com.igeolise.traveltime.rabbitmq.responses; 4 | 5 | message TimeFilterFastResponse { 6 | message Properties { 7 | repeated sint32 travelTimes = 1; 8 | repeated int32 monthlyFares = 2; 9 | repeated int32 distances = 3; 10 | } 11 | 12 | message Error { 13 | ErrorType type = 1; 14 | } 15 | 16 | enum ErrorType { 17 | /* 18 | * Catch all unknown error type 19 | */ 20 | UNKNOWN = 0 [deprecated = true]; 21 | /* 22 | * oneToManyRequest to many field must not be null 23 | */ 24 | ONE_TO_MANY_MUST_NOT_BE_NULL = 1 [deprecated = true]; 25 | /* 26 | * Source (either departure or arrival location) must not be null 27 | */ 28 | SOURCE_MUST_NOT_BE_NULL = 2 [deprecated = true]; 29 | /* 30 | * Transportation mode must not be null. 31 | */ 32 | TRANSPORTATION_MUST_NOT_BE_NULL = 3 [deprecated = true]; 33 | /* 34 | * Source (either departure or arrival location) must not be null 35 | */ 36 | SOURCE_NOT_IN_GEOMETRY = 4 [deprecated = true]; 37 | 38 | /* 39 | * Transportation mode unrecognized. 40 | */ 41 | UNRECOGNIZED_TRANSPORTATION_MODE = 5 [deprecated = true]; 42 | 43 | /* 44 | * The travel time limit is too low to process this request. 45 | */ 46 | TRAVEL_TIME_LIMIT_TOO_LOW = 6 [deprecated = true]; 47 | 48 | /* 49 | * The travel time limit is too high to process this request. 50 | */ 51 | TRAVEL_TIME_LIMIT_TOO_HIGH = 7 [deprecated = true]; 52 | 53 | /* 54 | * User id not set. 55 | */ 56 | AUTH_ERROR_NO_USER_ID = 8 [deprecated = true]; 57 | 58 | /* 59 | * Message sent to wrong queue - transportation mode cannot be handled. 60 | */ 61 | SERVICE_MISMATCH_WRONG_TRANSPORTATION_MODE = 9 [deprecated = true]; 62 | 63 | /* 64 | * Source is in a area that doesn't have any points that can be out of 65 | * search e.g a lake, mountains or other desolate areas. 66 | */ 67 | SOURCE_OUT_OF_REACH = 10 [deprecated = true]; 68 | 69 | /* 70 | * The interleaved deltas array should have (lat/lon) deltas and have an 71 | * even number of elements 72 | */ 73 | INTERLEAVED_DELTAS_INVALID_COORDINATE_PAIRS = 11 [deprecated = true]; 74 | 75 | /* 76 | * Public transport requests do not support returning distances for 77 | * returned points. 78 | */ 79 | DISTANCE_PROPERTY_NOT_SUPPORTED = 12 [deprecated = true]; 80 | 81 | /* 82 | * ManyToOne and OneToMany cannot be sent at the same time 83 | */ 84 | BOTH_MANY_TO_ONE_AND_ONE_TO_MANY_CANNOT_BE_SENT = 13 [deprecated = true]; 85 | 86 | /* 87 | * ManyToOne or OneToMany cannot be null 88 | */ 89 | ONE_TO_MANY_OR_MANY_TO_ONE_MUST_NOT_BE_NULL = 14 [deprecated = true]; 90 | /* 91 | * Invalid proto request 92 | */ 93 | INVALID_PROTO_REQUEST = 15 [deprecated = true]; 94 | } 95 | 96 | Error error = 1 [deprecated = true]; 97 | Properties properties = 2; 98 | } 99 | 100 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "setuptools_scm"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "traveltimepy" 7 | dynamic = ["version"] 8 | description = "Python Interface to Travel Time." 9 | readme = { file = "README.md", content-type = "text/markdown" } 10 | keywords = ["traveltimepy", "api", "maps"] 11 | license = { text = "MIT" } 12 | authors = [{ name = "TravelTime" }] 13 | requires-python = ">=3.8" 14 | dependencies = [ 15 | "pydantic", 16 | "typing-extensions", 17 | "geojson-pydantic>=1.0.1", 18 | "shapely", 19 | "dacite", 20 | "certifi>=2021.5.30", 21 | "aiohttp", 22 | "aiolimiter", 23 | "aiohttp-retry", 24 | "protobuf==4.21.12", 25 | "types-protobuf", 26 | ] 27 | classifiers = [ 28 | "Programming Language :: Python :: 3", 29 | "Programming Language :: Python :: 3.8", 30 | "Programming Language :: Python :: 3.9", 31 | "Programming Language :: Python :: 3.10", 32 | "Programming Language :: Python :: 3.11", 33 | "Programming Language :: Python :: 3.12", 34 | "License :: OSI Approved :: MIT License", 35 | ] 36 | 37 | [project.optional-dependencies] 38 | test = [ 39 | "pytest", 40 | "pytest-asyncio", 41 | "flake8", 42 | "flake8-pyproject", 43 | "mypy", 44 | "black", 45 | "types-shapely", 46 | ] 47 | 48 | [tool.setuptools] 49 | zip-safe = false 50 | include-package-data = true 51 | py-modules = [ 52 | "RequestsCommon_pb2", 53 | "TimeFilterFastRequest_pb2", 54 | "TimeFilterFastResponse_pb2" 55 | ] 56 | packages = { find = {} } 57 | 58 | [tool.setuptools_scm] 59 | 60 | [tool.flake8] 61 | per-file-ignores = ["**/__init__.py:F401"] 62 | max-line-length = 88 63 | extend-ignore = ["E203"] # See https://github.com/PyCQA/pycodestyle/issues/373 64 | 65 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Module for tests, build on `pytest`""" 2 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | 4 | import pytest 5 | 6 | from traveltimepy import Location, Coordinates 7 | from traveltimepy.sdk import TravelTimeSdk 8 | 9 | 10 | @pytest.fixture 11 | def sdk() -> TravelTimeSdk: 12 | return TravelTimeSdk(os.environ["APP_ID"], os.environ["API_KEY"]) 13 | 14 | 15 | @pytest.fixture 16 | def proto_sdk() -> TravelTimeSdk: 17 | return TravelTimeSdk(os.environ["PROTO_APP_ID"], os.environ["PROTO_API_KEY"]) 18 | 19 | 20 | @pytest.fixture 21 | def locations() -> List[Location]: 22 | return [ 23 | Location(id="London center", coords=Coordinates(lat=51.508930, lng=-0.131387)), 24 | Location(id="Hyde Park", coords=Coordinates(lat=51.508824, lng=-0.167093)), 25 | Location(id="ZSL London Zoo", coords=Coordinates(lat=51.536067, lng=-0.153596)), 26 | ] 27 | -------------------------------------------------------------------------------- /tests/distance_map_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from datetime import datetime 3 | 4 | from traveltimepy import Coordinates, Driving, LevelOfDetail, TravelTimeSdk 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_departures(sdk: TravelTimeSdk): 9 | results = await sdk.distance_map_async( 10 | coordinates=[ 11 | Coordinates(lat=51.507609, lng=-0.128315), 12 | Coordinates(lat=51.517609, lng=-0.138315), 13 | ], 14 | departure_time=datetime.now(), 15 | travel_distance=900, 16 | transportation=Driving(), 17 | level_of_detail=LevelOfDetail(scale_type="simple", level="lowest"), 18 | ) 19 | assert len(results) == 2 20 | 21 | 22 | @pytest.mark.asyncio 23 | async def test_arrivals(sdk: TravelTimeSdk): 24 | results = await sdk.distance_map_async( 25 | coordinates=[ 26 | Coordinates(lat=51.507609, lng=-0.128315), 27 | Coordinates(lat=51.517609, lng=-0.138315), 28 | ], 29 | arrival_time=datetime.now(), 30 | travel_distance=900, 31 | transportation=Driving(), 32 | level_of_detail=LevelOfDetail(scale_type="simple", level="lowest"), 33 | ) 34 | assert len(results) == 2 35 | -------------------------------------------------------------------------------- /tests/geocoding_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from traveltimepy import TravelTimeSdk 3 | 4 | 5 | @pytest.mark.asyncio 6 | async def test_geocoding_search(sdk: TravelTimeSdk): 7 | response = await sdk.geocoding_async( 8 | query="Parliament square", limit=30, within_countries=["gb", "de"] 9 | ) 10 | assert len(response.features) > 0 11 | assert len(response.features) < 31 12 | 13 | 14 | @pytest.mark.asyncio 15 | async def test_geocoding_reverse(sdk: TravelTimeSdk): 16 | response = await sdk.geocoding_reverse_async(lat=51.507281, lng=-0.132120) 17 | assert len(response.features) > 0 18 | -------------------------------------------------------------------------------- /tests/geohash_fast_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from traveltimepy import Transportation, TravelTimeSdk 4 | from traveltimepy.dto.common import CellProperty, Coordinates, GeohashCentroid 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_one_to_many(sdk: TravelTimeSdk): 9 | results = await sdk.geohash_fast_async( 10 | coordinates=[ 11 | Coordinates(lat=51.507609, lng=-0.128315), 12 | GeohashCentroid(geohash_centroid="gcpvj3"), 13 | ], 14 | resolution=6, 15 | properties=[CellProperty.MIN, CellProperty.MAX, CellProperty.MEAN], 16 | travel_time=900, 17 | transportation=Transportation(type="public_transport"), 18 | ) 19 | 20 | assert len(results) == 2 21 | 22 | 23 | @pytest.mark.asyncio 24 | async def test_many_to_one(sdk: TravelTimeSdk): 25 | results = await sdk.geohash_fast_async( 26 | coordinates=[ 27 | Coordinates(lat=51.507609, lng=-0.128315), 28 | GeohashCentroid(geohash_centroid="gcpvj3"), 29 | ], 30 | resolution=6, 31 | properties=[CellProperty.MIN, CellProperty.MAX, CellProperty.MEAN], 32 | travel_time=900, 33 | transportation=Transportation(type="public_transport"), 34 | one_to_many=False, 35 | ) 36 | 37 | assert len(results) == 2 38 | -------------------------------------------------------------------------------- /tests/geohash_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from datetime import datetime 3 | 4 | from traveltimepy import Coordinates, Driving, Range, TravelTimeSdk 5 | from traveltimepy.dto.common import CellProperty, GeohashCentroid 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_departures(sdk: TravelTimeSdk): 10 | results = await sdk.geohash_async( 11 | coordinates=[ 12 | Coordinates(lat=51.507609, lng=-0.128315), 13 | GeohashCentroid(geohash_centroid="gcpvhb"), 14 | ], 15 | resolution=6, 16 | properties=[CellProperty.MIN, CellProperty.MAX, CellProperty.MEAN], 17 | departure_time=datetime.now(), 18 | travel_time=900, 19 | transportation=Driving(), 20 | search_range=Range(enabled=True, width=1800), 21 | ) 22 | assert len(results) == 2 23 | 24 | 25 | @pytest.mark.asyncio 26 | async def test_arrivals(sdk: TravelTimeSdk): 27 | results = await sdk.geohash_async( 28 | coordinates=[ 29 | Coordinates(lat=51.507609, lng=-0.128315), 30 | GeohashCentroid(geohash_centroid="gcpvhb"), 31 | ], 32 | resolution=6, 33 | properties=[CellProperty.MIN, CellProperty.MAX, CellProperty.MEAN], 34 | arrival_time=datetime.now(), 35 | travel_time=900, 36 | transportation=Driving(), 37 | search_range=Range(enabled=True, width=1800), 38 | ) 39 | assert len(results) == 2 40 | 41 | 42 | @pytest.mark.asyncio 43 | async def test_union_departures(sdk: TravelTimeSdk): 44 | result = await sdk.geohash_union_async( 45 | coordinates=[ 46 | Coordinates(lat=51.507609, lng=-0.128315), 47 | GeohashCentroid(geohash_centroid="gcpvhb"), 48 | ], 49 | resolution=6, 50 | properties=[CellProperty.MIN, CellProperty.MAX, CellProperty.MEAN], 51 | departure_time=datetime.now(), 52 | travel_time=900, 53 | transportation=Driving(), 54 | search_range=Range(enabled=True, width=1800), 55 | ) 56 | assert len(result.cells) > 0 57 | 58 | 59 | @pytest.mark.asyncio 60 | async def test_intersection_arrivals(sdk: TravelTimeSdk): 61 | result = await sdk.geohash_intersection_async( 62 | coordinates=[ 63 | Coordinates(lat=51.507609, lng=-0.128315), 64 | GeohashCentroid(geohash_centroid="gcpvhb"), 65 | ], 66 | resolution=6, 67 | properties=[CellProperty.MIN, CellProperty.MAX, CellProperty.MEAN], 68 | arrival_time=datetime.now(), 69 | travel_time=900, 70 | transportation=Driving(), 71 | search_range=Range(enabled=True, width=1800), 72 | ) 73 | assert len(result.cells) > 0 74 | -------------------------------------------------------------------------------- /tests/h3_fast_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from traveltimepy import Transportation, TravelTimeSdk 4 | from traveltimepy.dto.common import CellProperty, Coordinates, H3Centroid 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_one_to_many(sdk: TravelTimeSdk): 9 | results = await sdk.h3_fast_async( 10 | coordinates=[ 11 | Coordinates(lat=51.507609, lng=-0.128315), 12 | H3Centroid(h3_centroid="87195da49ffffff"), 13 | ], 14 | resolution=7, 15 | properties=[CellProperty.MIN, CellProperty.MAX, CellProperty.MEAN], 16 | travel_time=900, 17 | transportation=Transportation(type="public_transport"), 18 | ) 19 | 20 | assert len(results) == 2 21 | 22 | 23 | @pytest.mark.asyncio 24 | async def test_many_to_one(sdk: TravelTimeSdk): 25 | results = await sdk.h3_fast_async( 26 | coordinates=[ 27 | Coordinates(lat=51.507609, lng=-0.128315), 28 | H3Centroid(h3_centroid="87195da49ffffff"), 29 | ], 30 | resolution=7, 31 | properties=[CellProperty.MIN, CellProperty.MAX, CellProperty.MEAN], 32 | travel_time=900, 33 | transportation=Transportation(type="public_transport"), 34 | one_to_many=False, 35 | ) 36 | 37 | assert len(results) == 2 38 | -------------------------------------------------------------------------------- /tests/h3_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from datetime import datetime 3 | 4 | from traveltimepy import Coordinates, Driving, Range, TravelTimeSdk 5 | from traveltimepy.dto.common import CellProperty, H3Centroid 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_departures(sdk: TravelTimeSdk): 10 | results = await sdk.h3_async( 11 | coordinates=[ 12 | Coordinates(lat=51.507609, lng=-0.128315), 13 | H3Centroid(h3_centroid="87195da49ffffff"), 14 | ], 15 | resolution=7, 16 | properties=[CellProperty.MIN, CellProperty.MAX, CellProperty.MEAN], 17 | departure_time=datetime.now(), 18 | travel_time=900, 19 | transportation=Driving(), 20 | search_range=Range(enabled=True, width=1800), 21 | ) 22 | assert len(results) == 2 23 | 24 | 25 | @pytest.mark.asyncio 26 | async def test_arrivals(sdk: TravelTimeSdk): 27 | results = await sdk.h3_async( 28 | coordinates=[ 29 | Coordinates(lat=51.507609, lng=-0.128315), 30 | H3Centroid(h3_centroid="87195da49ffffff"), 31 | ], 32 | resolution=7, 33 | properties=[CellProperty.MIN, CellProperty.MAX, CellProperty.MEAN], 34 | arrival_time=datetime.now(), 35 | travel_time=900, 36 | transportation=Driving(), 37 | search_range=Range(enabled=True, width=1800), 38 | ) 39 | assert len(results) == 2 40 | 41 | 42 | @pytest.mark.asyncio 43 | async def test_union_departures(sdk: TravelTimeSdk): 44 | result = await sdk.h3_union_async( 45 | coordinates=[ 46 | Coordinates(lat=51.507609, lng=-0.128315), 47 | H3Centroid(h3_centroid="87195da49ffffff"), 48 | ], 49 | resolution=7, 50 | properties=[CellProperty.MIN, CellProperty.MAX, CellProperty.MEAN], 51 | departure_time=datetime.now(), 52 | travel_time=900, 53 | transportation=Driving(), 54 | search_range=Range(enabled=True, width=1800), 55 | ) 56 | assert len(result.cells) > 0 57 | 58 | 59 | @pytest.mark.asyncio 60 | async def test_intersection_arrivals(sdk: TravelTimeSdk): 61 | result = await sdk.h3_intersection_async( 62 | coordinates=[ 63 | Coordinates(lat=51.507609, lng=-0.128315), 64 | H3Centroid(h3_centroid="87195da49ffffff"), 65 | ], 66 | resolution=7, 67 | properties=[CellProperty.MIN, CellProperty.MAX, CellProperty.MEAN], 68 | arrival_time=datetime.now(), 69 | travel_time=900, 70 | transportation=Driving(), 71 | search_range=Range(enabled=True, width=1800), 72 | ) 73 | assert len(result.cells) > 0 74 | -------------------------------------------------------------------------------- /tests/map_info_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from traveltimepy.sdk import TravelTimeSdk 4 | 5 | 6 | @pytest.mark.asyncio 7 | async def test_map_info(sdk: TravelTimeSdk): 8 | maps = await sdk.map_info_async() 9 | assert len(maps) > 0 10 | -------------------------------------------------------------------------------- /tests/postcodes_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from datetime import datetime 3 | 4 | from traveltimepy import Coordinates, PublicTransport, TravelTimeSdk 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_departures(sdk: TravelTimeSdk): 9 | results = await sdk.postcodes_async( 10 | coordinates=[Coordinates(lat=51.507609, lng=-0.128315)], 11 | departure_time=datetime.now(), 12 | transportation=PublicTransport(), 13 | ) 14 | assert len(results) > 0 15 | 16 | 17 | @pytest.mark.asyncio 18 | async def test_arrivals(sdk: TravelTimeSdk): 19 | results = await sdk.postcodes_async( 20 | coordinates=[Coordinates(lat=51.507609, lng=-0.128315)], 21 | arrival_time=datetime.now(), 22 | transportation=PublicTransport(), 23 | ) 24 | assert len(results) > 0 25 | 26 | 27 | @pytest.mark.asyncio 28 | async def test_districts_departure(sdk: TravelTimeSdk): 29 | results = await sdk.postcodes_districts_async( 30 | coordinates=[Coordinates(lat=51.507609, lng=-0.128315)], 31 | departure_time=datetime.now(), 32 | transportation=PublicTransport(), 33 | ) 34 | assert len(results) > 0 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_districts_arrival(sdk: TravelTimeSdk): 39 | results = await sdk.postcodes_districts_async( 40 | coordinates=[Coordinates(lat=51.507609, lng=-0.128315)], 41 | arrival_time=datetime.now(), 42 | transportation=PublicTransport(), 43 | ) 44 | assert len(results) > 0 45 | 46 | 47 | @pytest.mark.asyncio 48 | async def test_sectors_departure(sdk: TravelTimeSdk): 49 | results = await sdk.postcodes_sectors_async( 50 | coordinates=[Coordinates(lat=51.507609, lng=-0.128315)], 51 | departure_time=datetime.now(), 52 | transportation=PublicTransport(), 53 | ) 54 | assert len(results) > 0 55 | 56 | 57 | @pytest.mark.asyncio 58 | async def test_sectors_arrival(sdk: TravelTimeSdk): 59 | results = await sdk.postcodes_sectors_async( 60 | coordinates=[Coordinates(lat=51.507609, lng=-0.128315)], 61 | arrival_time=datetime.now(), 62 | transportation=PublicTransport(), 63 | ) 64 | assert len(results) > 0 65 | -------------------------------------------------------------------------------- /tests/routes_test.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | from datetime import datetime 5 | 6 | from traveltimepy import PublicTransport, Driving, Location, Coordinates 7 | from traveltimepy.dto.common import Snapping, SnappingAcceptRoads, SnappingPenalty 8 | from traveltimepy.sdk import TravelTimeSdk 9 | 10 | 11 | @pytest.mark.asyncio 12 | async def test_departures(sdk: TravelTimeSdk, locations): 13 | results = await sdk.routes_async( 14 | locations=locations, 15 | search_ids={ 16 | "London center": ["Hyde Park", "ZSL London Zoo"], 17 | "ZSL London Zoo": ["Hyde Park", "London center"], 18 | }, 19 | transportation=PublicTransport(), 20 | departure_time=datetime.now(), 21 | ) 22 | assert len(results) == 2 23 | 24 | 25 | @pytest.mark.asyncio 26 | async def test_arrivals(sdk: TravelTimeSdk, locations): 27 | results = await sdk.routes_async( 28 | locations=locations, 29 | search_ids={ 30 | "London center": ["Hyde Park", "ZSL London Zoo"], 31 | "ZSL London Zoo": ["Hyde Park", "London center"], 32 | }, 33 | transportation=PublicTransport(), 34 | departure_time=datetime.now(), 35 | ) 36 | assert len(results) == 2 37 | 38 | 39 | @pytest.mark.asyncio 40 | async def test_snapping(sdk: TravelTimeSdk): 41 | locations: List[Location] = [ 42 | Location(id="A", coords=Coordinates(lat=53.806479, lng=-2.615711)), 43 | Location(id="B", coords=Coordinates(lat=53.810129, lng=-2.601099)), 44 | ] 45 | result_with_penalty = await sdk.routes_async( 46 | locations=locations, 47 | search_ids={ 48 | "A": ["B"], 49 | }, 50 | transportation=Driving(), 51 | departure_time=datetime.now(), 52 | ) 53 | traveltime_with_penalty = ( 54 | result_with_penalty[0].locations[0].properties[0].travel_time 55 | ) 56 | result_without_penalty = await sdk.routes_async( 57 | locations=locations, 58 | search_ids={ 59 | "A": ["B"], 60 | }, 61 | transportation=Driving(), 62 | departure_time=datetime.now(), 63 | snapping=Snapping( 64 | penalty=SnappingPenalty.DISABLED, 65 | accept_roads=SnappingAcceptRoads.ANY_DRIVABLE, 66 | ), 67 | ) 68 | traveltime_without_penalty = ( 69 | result_without_penalty[0].locations[0].properties[0].travel_time 70 | ) 71 | assert traveltime_with_penalty is not None 72 | assert traveltime_without_penalty is not None 73 | assert traveltime_with_penalty > traveltime_without_penalty 74 | -------------------------------------------------------------------------------- /tests/supported_locations.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from traveltimepy import Location, Coordinates, TravelTimeSdk 4 | 5 | 6 | @pytest.mark.asyncio 7 | async def test_supported_locations(sdk: TravelTimeSdk): 8 | locations = [ 9 | Location(id="Kaunas", coords=Coordinates(lat=54.900008, lng=23.957734)), 10 | Location(id="London", coords=Coordinates(lat=51.506756, lng=-0.12805)), 11 | Location(id="Bangkok", coords=Coordinates(lat=13.761866, lng=100.544818)), 12 | Location(id="Lisbon", coords=Coordinates(lat=38.721869, lng=-9.138549)), 13 | Location(id="Unsupported", coords=Coordinates(lat=68.721869, lng=-9.138549)), 14 | ] 15 | response = await sdk.supported_locations_async(locations) 16 | assert len(response.locations) == 4 17 | assert len(response.unsupported_locations) == 1 18 | -------------------------------------------------------------------------------- /tests/time_filter_fast_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from traveltimepy import Transportation, TravelTimeSdk 4 | 5 | 6 | @pytest.mark.asyncio 7 | async def test_one_to_many(sdk: TravelTimeSdk, locations): 8 | results = await sdk.time_filter_fast_async( 9 | locations=locations, 10 | search_ids={ 11 | "London center": ["Hyde Park", "ZSL London Zoo"], 12 | "ZSL London Zoo": ["Hyde Park", "London center"], 13 | }, 14 | transportation=Transportation(type="public_transport"), 15 | ) 16 | 17 | assert len(results) > 0 18 | 19 | 20 | @pytest.mark.asyncio 21 | async def test_many_to_one(sdk: TravelTimeSdk, locations): 22 | results = await sdk.time_filter_fast_async( 23 | locations=locations, 24 | search_ids={ 25 | "London center": ["Hyde Park", "ZSL London Zoo"], 26 | "ZSL London Zoo": ["Hyde Park", "London center"], 27 | }, 28 | transportation=Transportation(type="public_transport"), 29 | one_to_many=False, 30 | ) 31 | 32 | assert len(results) > 0 33 | -------------------------------------------------------------------------------- /tests/time_filter_proto_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from traveltimepy import Coordinates 4 | from traveltimepy.dto.common import PropertyProto 5 | from traveltimepy.dto.requests.time_filter_proto import ( 6 | DrivingAndPublicTransportWithDetails, 7 | ProtoTransportation, 8 | ProtoCountry, 9 | PublicTransportWithDetails, 10 | ) 11 | from traveltimepy.sdk import TravelTimeSdk 12 | 13 | 14 | @pytest.mark.asyncio 15 | async def test_one_to_many(proto_sdk: TravelTimeSdk): 16 | results = await proto_sdk.time_filter_proto_async( 17 | origin=Coordinates(lat=51.425709, lng=-0.122061), 18 | destinations=[ 19 | Coordinates(lat=51.348605, lng=-0.314783), 20 | Coordinates(lat=51.337205, lng=-0.315793), 21 | ], 22 | transportation=ProtoTransportation.DRIVING_FERRY, 23 | travel_time=7200, 24 | country=ProtoCountry.UNITED_KINGDOM, 25 | ) 26 | assert len(results.travel_times) == 2 and len(results.distances) == 0 27 | 28 | 29 | @pytest.mark.asyncio 30 | async def test_many_to_one(proto_sdk: TravelTimeSdk): 31 | results = await proto_sdk.time_filter_proto_async( 32 | origin=Coordinates(lat=51.425709, lng=-0.122061), 33 | destinations=[ 34 | Coordinates(lat=51.348605, lng=-0.314783), 35 | Coordinates(lat=51.337205, lng=-0.315793), 36 | ], 37 | transportation=ProtoTransportation.DRIVING_FERRY, 38 | travel_time=7200, 39 | country=ProtoCountry.UNITED_KINGDOM, 40 | one_to_many=False, 41 | ) 42 | assert len(results.travel_times) == 2 and len(results.distances) == 0 43 | 44 | 45 | @pytest.mark.asyncio 46 | async def test_one_to_many_with_distances(proto_sdk: TravelTimeSdk): 47 | results = await proto_sdk.time_filter_proto_async( 48 | origin=Coordinates(lat=51.425709, lng=-0.122061), 49 | destinations=[ 50 | Coordinates(lat=51.348605, lng=-0.314783), 51 | Coordinates(lat=51.337205, lng=-0.315793), 52 | ], 53 | transportation=ProtoTransportation.DRIVING_FERRY, 54 | travel_time=7200, 55 | country=ProtoCountry.UNITED_KINGDOM, 56 | properties=[PropertyProto.DISTANCE], 57 | ) 58 | assert len(results.travel_times) == 2 and len(results.distances) == 2 59 | 60 | 61 | @pytest.mark.asyncio 62 | async def test_many_to_one_with_distances(proto_sdk: TravelTimeSdk): 63 | results = await proto_sdk.time_filter_proto_async( 64 | origin=Coordinates(lat=51.425709, lng=-0.122061), 65 | destinations=[ 66 | Coordinates(lat=51.348605, lng=-0.314783), 67 | Coordinates(lat=51.337205, lng=-0.315793), 68 | ], 69 | transportation=ProtoTransportation.DRIVING_FERRY, 70 | travel_time=7200, 71 | country=ProtoCountry.UNITED_KINGDOM, 72 | one_to_many=False, 73 | properties=[PropertyProto.DISTANCE], 74 | ) 75 | assert len(results.travel_times) == 2 and len(results.distances) == 2 76 | 77 | 78 | async def test_one_to_many_pt_with_params(proto_sdk: TravelTimeSdk): 79 | results = await proto_sdk.time_filter_proto_async( 80 | origin=Coordinates(lat=51.425709, lng=-0.122061), 81 | destinations=[ 82 | Coordinates(lat=51.348605, lng=-0.314783), 83 | Coordinates(lat=51.337205, lng=-0.315793), 84 | ], 85 | transportation=PublicTransportWithDetails(walking_time_to_station=900), 86 | travel_time=7200, 87 | country=ProtoCountry.UNITED_KINGDOM, 88 | ) 89 | assert len(results.travel_times) == 2 and len(results.distances) == 0 90 | 91 | 92 | @pytest.mark.asyncio 93 | async def test_many_to_one_pt_with_params(proto_sdk: TravelTimeSdk): 94 | results = await proto_sdk.time_filter_proto_async( 95 | origin=Coordinates(lat=51.425709, lng=-0.122061), 96 | destinations=[ 97 | Coordinates(lat=51.348605, lng=-0.314783), 98 | Coordinates(lat=51.337205, lng=-0.315793), 99 | ], 100 | transportation=PublicTransportWithDetails(walking_time_to_station=900), 101 | travel_time=7200, 102 | country=ProtoCountry.UNITED_KINGDOM, 103 | one_to_many=False, 104 | ) 105 | assert len(results.travel_times) == 2 and len(results.distances) == 0 106 | 107 | 108 | async def test_one_to_many_driving_and_pt_with_params(proto_sdk: TravelTimeSdk): 109 | results = await proto_sdk.time_filter_proto_async( 110 | origin=Coordinates(lat=51.425709, lng=-0.122061), 111 | destinations=[ 112 | Coordinates(lat=51.348605, lng=-0.314783), 113 | Coordinates(lat=51.337205, lng=-0.315793), 114 | ], 115 | transportation=DrivingAndPublicTransportWithDetails( 116 | walking_time_to_station=900, driving_time_to_station=1800, parking_time=300 117 | ), 118 | travel_time=7200, 119 | country=ProtoCountry.UNITED_KINGDOM, 120 | ) 121 | assert len(results.travel_times) == 2 and len(results.distances) == 0 122 | 123 | 124 | @pytest.mark.asyncio 125 | async def test_many_to_one_driving_and_pt_with_params(proto_sdk: TravelTimeSdk): 126 | results = await proto_sdk.time_filter_proto_async( 127 | origin=Coordinates(lat=51.425709, lng=-0.122061), 128 | destinations=[ 129 | Coordinates(lat=51.348605, lng=-0.314783), 130 | Coordinates(lat=51.337205, lng=-0.315793), 131 | ], 132 | transportation=DrivingAndPublicTransportWithDetails( 133 | walking_time_to_station=900, driving_time_to_station=1800, parking_time=300 134 | ), 135 | travel_time=7200, 136 | country=ProtoCountry.UNITED_KINGDOM, 137 | one_to_many=False, 138 | ) 139 | assert len(results.travel_times) == 2 and len(results.distances) == 0 140 | -------------------------------------------------------------------------------- /tests/time_filter_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from datetime import datetime 3 | 4 | from traveltimepy import PublicTransport, TravelTimeSdk 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_departures(sdk: TravelTimeSdk, locations): 9 | results = await sdk.time_filter_async( 10 | locations=locations, 11 | search_ids={ 12 | "London center": ["Hyde Park", "ZSL London Zoo"], 13 | "ZSL London Zoo": ["Hyde Park", "London center"], 14 | }, 15 | transportation=PublicTransport(), 16 | departure_time=datetime.now(), 17 | ) 18 | assert len(results) == 2 19 | 20 | 21 | @pytest.mark.asyncio 22 | async def test_arrivals(sdk: TravelTimeSdk, locations): 23 | results = await sdk.time_filter_async( 24 | locations=locations, 25 | search_ids={ 26 | "London center": ["Hyde Park", "ZSL London Zoo"], 27 | "ZSL London Zoo": ["Hyde Park", "London center"], 28 | }, 29 | transportation=PublicTransport(), 30 | departure_time=datetime.now(), 31 | ) 32 | assert len(results) == 2 33 | -------------------------------------------------------------------------------- /tests/time_map_fast_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from traveltimepy import Transportation, TravelTimeSdk 4 | from traveltimepy.dto.common import Coordinates 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_one_to_many(sdk: TravelTimeSdk): 9 | results = await sdk.time_map_fast_async( 10 | coordinates=[ 11 | Coordinates(lat=51.507609, lng=-0.128315), 12 | Coordinates(lat=51.517609, lng=-0.138315), 13 | ], 14 | travel_time=900, 15 | transportation=Transportation(type="public_transport"), 16 | ) 17 | 18 | assert len(results) == 2 19 | 20 | 21 | @pytest.mark.asyncio 22 | async def test_many_to_one(sdk: TravelTimeSdk): 23 | results = await sdk.time_map_fast_async( 24 | coordinates=[ 25 | Coordinates(lat=51.507609, lng=-0.128315), 26 | Coordinates(lat=51.517609, lng=-0.138315), 27 | ], 28 | travel_time=900, 29 | transportation=Transportation(type="public_transport"), 30 | one_to_many=False, 31 | ) 32 | 33 | assert len(results) == 2 34 | 35 | 36 | @pytest.mark.asyncio 37 | async def test_one_to_many_geojson(sdk: TravelTimeSdk): 38 | results = await sdk.time_map_fast_geojson_async( 39 | coordinates=[ 40 | Coordinates(lat=51.507609, lng=-0.128315), 41 | Coordinates(lat=51.517609, lng=-0.138315), 42 | ], 43 | travel_time=900, 44 | transportation=Transportation(type="public_transport"), 45 | ) 46 | 47 | assert len(results.features) == 2 48 | 49 | 50 | @pytest.mark.asyncio 51 | async def test_many_to_one_geojson(sdk: TravelTimeSdk): 52 | results = await sdk.time_map_fast_geojson_async( 53 | coordinates=[ 54 | Coordinates(lat=51.507609, lng=-0.128315), 55 | Coordinates(lat=51.517609, lng=-0.138315), 56 | ], 57 | travel_time=900, 58 | transportation=Transportation(type="public_transport"), 59 | one_to_many=False, 60 | ) 61 | 62 | assert len(results.features) == 2 63 | 64 | 65 | @pytest.mark.asyncio 66 | async def test_one_to_many_wkt(sdk: TravelTimeSdk): 67 | response = await sdk.time_map_fast_wkt_async( 68 | coordinates=[ 69 | Coordinates(lat=51.507609, lng=-0.128315), 70 | Coordinates(lat=51.517609, lng=-0.138315), 71 | ], 72 | travel_time=900, 73 | transportation=Transportation(type="public_transport"), 74 | ) 75 | 76 | assert len(response.results) == 2 77 | 78 | 79 | @pytest.mark.asyncio 80 | async def test_many_to_one_wkt(sdk: TravelTimeSdk): 81 | response = await sdk.time_map_fast_wkt_async( 82 | coordinates=[ 83 | Coordinates(lat=51.507609, lng=-0.128315), 84 | Coordinates(lat=51.517609, lng=-0.138315), 85 | ], 86 | travel_time=900, 87 | transportation=Transportation(type="public_transport"), 88 | one_to_many=False, 89 | ) 90 | 91 | assert len(response.results) == 2 92 | 93 | 94 | @pytest.mark.asyncio 95 | async def test_one_to_many_wkt_no_holes(sdk: TravelTimeSdk): 96 | response = await sdk.time_map_fast_wkt_no_holes_async( 97 | coordinates=[ 98 | Coordinates(lat=51.507609, lng=-0.128315), 99 | Coordinates(lat=51.517609, lng=-0.138315), 100 | ], 101 | travel_time=900, 102 | transportation=Transportation(type="public_transport"), 103 | ) 104 | 105 | assert len(response.results) == 2 106 | 107 | 108 | @pytest.mark.asyncio 109 | async def test_many_to_one_wkt_no_holes(sdk: TravelTimeSdk): 110 | response = await sdk.time_map_fast_wkt_no_holes_async( 111 | coordinates=[ 112 | Coordinates(lat=51.507609, lng=-0.128315), 113 | Coordinates(lat=51.517609, lng=-0.138315), 114 | ], 115 | travel_time=900, 116 | transportation=Transportation(type="public_transport"), 117 | one_to_many=False, 118 | ) 119 | 120 | assert len(response.results) == 2 121 | -------------------------------------------------------------------------------- /tests/time_map_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from datetime import datetime 3 | 4 | from traveltimepy import Coordinates, Driving, LevelOfDetail, Range, TravelTimeSdk 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_departures(sdk: TravelTimeSdk): 9 | results = await sdk.time_map_async( 10 | coordinates=[ 11 | Coordinates(lat=51.507609, lng=-0.128315), 12 | Coordinates(lat=51.517609, lng=-0.138315), 13 | ], 14 | departure_time=datetime.now(), 15 | travel_time=900, 16 | transportation=Driving(), 17 | search_range=Range(enabled=True, width=1800), 18 | level_of_detail=LevelOfDetail(scale_type="simple", level="lowest"), 19 | ) 20 | assert len(results) == 2 21 | 22 | 23 | @pytest.mark.asyncio 24 | async def test_departures_geojson(sdk: TravelTimeSdk): 25 | results = await sdk.time_map_geojson_async( 26 | coordinates=[ 27 | Coordinates(lat=51.507609, lng=-0.128315), 28 | Coordinates(lat=51.517609, lng=-0.138315), 29 | ], 30 | departure_time=datetime.now(), 31 | travel_time=900, 32 | transportation=Driving(), 33 | search_range=Range(enabled=True, width=1800), 34 | level_of_detail=LevelOfDetail(scale_type="simple", level="lowest"), 35 | ) 36 | assert len(results.features) == 2 37 | 38 | 39 | @pytest.mark.asyncio 40 | async def test_departures_wkt(sdk: TravelTimeSdk): 41 | response = await sdk.time_map_wkt_async( 42 | coordinates=[ 43 | Coordinates(lat=51.507609, lng=-0.128315), 44 | Coordinates(lat=51.517609, lng=-0.138315), 45 | ], 46 | departure_time=datetime.now(), 47 | travel_time=900, 48 | transportation=Driving(), 49 | search_range=Range(enabled=True, width=1800), 50 | level_of_detail=LevelOfDetail(scale_type="simple", level="lowest"), 51 | ) 52 | assert len(response.results) == 2 53 | 54 | 55 | @pytest.mark.asyncio 56 | async def test_departures_wkt_no_holes(sdk: TravelTimeSdk): 57 | response = await sdk.time_map_wkt_no_holes_async( 58 | coordinates=[ 59 | Coordinates(lat=51.507609, lng=-0.128315), 60 | Coordinates(lat=51.517609, lng=-0.138315), 61 | ], 62 | departure_time=datetime.now(), 63 | travel_time=900, 64 | transportation=Driving(), 65 | search_range=Range(enabled=True, width=1800), 66 | level_of_detail=LevelOfDetail(scale_type="simple", level="lowest"), 67 | ) 68 | assert len(response.results) == 2 69 | 70 | 71 | @pytest.mark.asyncio 72 | async def test_arrivals(sdk: TravelTimeSdk): 73 | results = await sdk.time_map_async( 74 | coordinates=[ 75 | Coordinates(lat=51.507609, lng=-0.128315), 76 | Coordinates(lat=51.517609, lng=-0.138315), 77 | ], 78 | arrival_time=datetime.now(), 79 | travel_time=900, 80 | transportation=Driving(), 81 | search_range=Range(enabled=True, width=1800), 82 | level_of_detail=LevelOfDetail(scale_type="simple", level="lowest"), 83 | ) 84 | assert len(results) == 2 85 | 86 | 87 | @pytest.mark.asyncio 88 | async def test_arrivals_geojson(sdk: TravelTimeSdk): 89 | results = await sdk.time_map_geojson_async( 90 | coordinates=[ 91 | Coordinates(lat=51.507609, lng=-0.128315), 92 | Coordinates(lat=51.517609, lng=-0.138315), 93 | ], 94 | arrival_time=datetime.now(), 95 | travel_time=900, 96 | transportation=Driving(), 97 | search_range=Range(enabled=True, width=1800), 98 | level_of_detail=LevelOfDetail(scale_type="simple", level="lowest"), 99 | ) 100 | assert len(results.features) == 2 101 | 102 | 103 | @pytest.mark.asyncio 104 | async def test_arrivals_wkt(sdk: TravelTimeSdk): 105 | response = await sdk.time_map_wkt_async( 106 | coordinates=[ 107 | Coordinates(lat=51.507609, lng=-0.128315), 108 | Coordinates(lat=51.517609, lng=-0.138315), 109 | ], 110 | arrival_time=datetime.now(), 111 | travel_time=900, 112 | transportation=Driving(), 113 | search_range=Range(enabled=True, width=1800), 114 | level_of_detail=LevelOfDetail(scale_type="simple", level="lowest"), 115 | ) 116 | assert len(response.results) == 2 117 | 118 | 119 | @pytest.mark.asyncio 120 | async def test_arrivals_wkt_no_holes(sdk: TravelTimeSdk): 121 | response = await sdk.time_map_wkt_no_holes_async( 122 | coordinates=[ 123 | Coordinates(lat=51.507609, lng=-0.128315), 124 | Coordinates(lat=51.517609, lng=-0.138315), 125 | ], 126 | arrival_time=datetime.now(), 127 | travel_time=900, 128 | transportation=Driving(), 129 | search_range=Range(enabled=True, width=1800), 130 | level_of_detail=LevelOfDetail(scale_type="simple", level="lowest"), 131 | ) 132 | assert len(response.results) == 2 133 | 134 | 135 | @pytest.mark.asyncio 136 | async def test_union_departures(sdk: TravelTimeSdk): 137 | result = await sdk.time_map_union_async( 138 | coordinates=[ 139 | Coordinates(lat=51.507609, lng=-0.128315), 140 | Coordinates(lat=51.517609, lng=-0.138315), 141 | ], 142 | departure_time=datetime.now(), 143 | travel_time=900, 144 | transportation=Driving(), 145 | search_range=Range(enabled=True, width=1800), 146 | level_of_detail=LevelOfDetail(scale_type="simple", level="lowest"), 147 | ) 148 | assert len(result.shapes) > 0 149 | 150 | 151 | @pytest.mark.asyncio 152 | async def test_intersection_arrivals(sdk: TravelTimeSdk): 153 | result = await sdk.time_map_intersection_async( 154 | coordinates=[ 155 | Coordinates(lat=51.507609, lng=-0.128315), 156 | Coordinates(lat=51.517609, lng=-0.138315), 157 | ], 158 | arrival_time=datetime.now(), 159 | travel_time=900, 160 | transportation=Driving(), 161 | search_range=Range(enabled=True, width=1800), 162 | level_of_detail=LevelOfDetail(scale_type="simple", level="lowest"), 163 | ) 164 | assert len(result.shapes) > 0 165 | -------------------------------------------------------------------------------- /tests/wkt_parsing_test.py: -------------------------------------------------------------------------------- 1 | import pytest # noqa 2 | 3 | from traveltimepy import Coordinates 4 | from traveltimepy.wkt import ( 5 | parse_wkt, 6 | PointModel, 7 | LineStringModel, 8 | PolygonModel, 9 | MultiPointModel, 10 | MultiLineStringModel, 11 | MultiPolygonModel, 12 | ) 13 | from traveltimepy.wkt.error import ( 14 | InvalidWKTStringError, 15 | NullGeometryError, 16 | InvalidGeometryTypeError, 17 | ) 18 | 19 | point_wkt = "POINT (0 0)" 20 | line_wkt = "LINESTRING(0 0, 1 1, 2 2)" 21 | poly_wkt = "POLYGON((0 0, 0 2, 2 2, 2 0, 0 0))" 22 | mp_wkt = "MULTIPOINT(0 0, 1 1)" 23 | mls_wkt = "MULTILINESTRING((0 0, 1 1), (2 2, 3 3))" 24 | mpoly_wkt = "MULTIPOLYGON(((0 0, 0 2, 2 2, 2 0, 0 0)))" 25 | 26 | 27 | def test_parse_point(): 28 | parsed = parse_wkt(point_wkt) 29 | assert parsed == PointModel(coordinates=Coordinates(lat=0, lng=0)) 30 | 31 | 32 | def test_parse_line_string(): 33 | parsed = parse_wkt(line_wkt) 34 | assert parsed == LineStringModel( 35 | coordinates=[ 36 | PointModel(coordinates=Coordinates(lat=0.0, lng=0.0)), 37 | PointModel(coordinates=Coordinates(lat=1.0, lng=1.0)), 38 | PointModel(coordinates=Coordinates(lat=2.0, lng=2.0)), 39 | ], 40 | ) 41 | 42 | 43 | def test_parse_polygon(): 44 | parsed = parse_wkt(poly_wkt) 45 | assert parsed == PolygonModel( 46 | exterior=LineStringModel( 47 | coordinates=[ 48 | PointModel(coordinates=Coordinates(lat=0.0, lng=0.0)), 49 | PointModel(coordinates=Coordinates(lat=0.0, lng=2.0)), 50 | PointModel(coordinates=Coordinates(lat=2.0, lng=2.0)), 51 | PointModel(coordinates=Coordinates(lat=2.0, lng=0.0)), 52 | PointModel(coordinates=Coordinates(lat=0.0, lng=0.0)), 53 | ], 54 | ), 55 | interiors=[], 56 | ) 57 | 58 | 59 | def test_parse_multi_point(): 60 | parsed = parse_wkt(mp_wkt) 61 | assert parsed == MultiPointModel( 62 | coordinates=[ 63 | PointModel(coordinates=Coordinates(lat=0.0, lng=0.0)), 64 | PointModel(coordinates=Coordinates(lat=1.0, lng=1.0)), 65 | ], 66 | ) 67 | 68 | 69 | def test_parse_multi_line_string(): 70 | parsed = parse_wkt(mls_wkt) 71 | assert parsed == MultiLineStringModel( 72 | coordinates=[ 73 | LineStringModel( 74 | coordinates=[ 75 | PointModel( 76 | coordinates=Coordinates(lat=0.0, lng=0.0), 77 | ), 78 | PointModel( 79 | coordinates=Coordinates(lat=1.0, lng=1.0), 80 | ), 81 | ], 82 | ), 83 | LineStringModel( 84 | coordinates=[ 85 | PointModel( 86 | coordinates=Coordinates(lat=2.0, lng=2.0), 87 | ), 88 | PointModel( 89 | coordinates=Coordinates(lat=3.0, lng=3.0), 90 | ), 91 | ], 92 | ), 93 | ], 94 | ) 95 | 96 | 97 | def test_parse_multi_polygon(): 98 | parsed = parse_wkt(mpoly_wkt) 99 | assert parsed == MultiPolygonModel( 100 | coordinates=[ 101 | PolygonModel( 102 | exterior=LineStringModel( 103 | coordinates=[ 104 | PointModel( 105 | coordinates=Coordinates(lat=0.0, lng=0.0), 106 | ), 107 | PointModel( 108 | coordinates=Coordinates(lat=0.0, lng=2.0), 109 | ), 110 | PointModel( 111 | coordinates=Coordinates(lat=2.0, lng=2.0), 112 | ), 113 | PointModel( 114 | coordinates=Coordinates(lat=2.0, lng=0.0), 115 | ), 116 | PointModel( 117 | coordinates=Coordinates(lat=0.0, lng=0.0), 118 | ), 119 | ], 120 | ), 121 | interiors=[], 122 | ) 123 | ], 124 | ) 125 | 126 | 127 | def test_invalid_wkt_string(): 128 | with pytest.raises(InvalidWKTStringError): 129 | parse_wkt("INVALIDWKTSTRING") 130 | 131 | 132 | def test_null_geometry(): 133 | with pytest.raises(NullGeometryError): 134 | parse_wkt("POINT EMPTY") 135 | 136 | 137 | def test_unsupported_geometry_type(): 138 | unsupported_wkt = "GEOMETRYCOLLECTION(POINT(2 3),LINESTRING(2 3, 3 4))" 139 | with pytest.raises(InvalidGeometryTypeError): 140 | parse_wkt(unsupported_wkt) 141 | -------------------------------------------------------------------------------- /tests/wkt_pretty_print_test.py: -------------------------------------------------------------------------------- 1 | import pytest # noqa 2 | 3 | from traveltimepy import Coordinates 4 | from traveltimepy.wkt import ( 5 | PointModel, 6 | LineStringModel, 7 | PolygonModel, 8 | MultiPointModel, 9 | MultiLineStringModel, 10 | MultiPolygonModel, 11 | ) 12 | 13 | # Updated mock data for LineStringModel with two distinct points 14 | mock_point1 = PointModel(coordinates=Coordinates(lat=10, lng=20)) 15 | mock_point2 = PointModel(coordinates=Coordinates(lat=30, lng=40)) 16 | mock_linestring = LineStringModel(coordinates=[mock_point1, mock_point2]) 17 | mock_polygon = PolygonModel(exterior=mock_linestring, interiors=[mock_linestring]) 18 | mock_multipoint = MultiPointModel(coordinates=[mock_point1, mock_point2]) 19 | mock_multilinestring = MultiLineStringModel( 20 | coordinates=[mock_linestring, mock_linestring] 21 | ) 22 | mock_multipolygon = MultiPolygonModel(coordinates=[mock_polygon]) 23 | 24 | 25 | # Test functions 26 | def test_pointmodel_pretty_print(capsys): 27 | mock_point1.pretty_print() 28 | captured = capsys.readouterr() 29 | assert captured.out == "POINT: 10.0, 20.0\n" 30 | 31 | 32 | def test_linestringmodel_pretty_print(capsys): 33 | mock_linestring.pretty_print() 34 | captured = capsys.readouterr() 35 | assert captured.out == "LINE STRING:\n\tPOINT: 10.0, 20.0\n\tPOINT: 30.0, 40.0\n" 36 | 37 | 38 | def test_polygonmodel_pretty_print(capsys): 39 | mock_polygon.pretty_print() 40 | captured = capsys.readouterr() 41 | assert captured.out == ( 42 | "POLYGON:\n\tEXTERIOR:\n\t\tLINE STRING:\n\t\t\tPOINT: 10.0, 20.0\n\t\t\tPOINT: 30.0, 40.0\n" 43 | "\tINTERIORS:\n\t\tLINE STRING:\n\t\t\tPOINT: 10.0, 20.0\n\t\t\tPOINT: 30.0, 40.0\n" 44 | ) 45 | 46 | 47 | def test_multipointmodel_pretty_print(capsys): 48 | mock_multipoint.pretty_print() 49 | captured = capsys.readouterr() 50 | assert captured.out == "MULTIPOINT:\n\tPOINT: 10.0, 20.0\n\tPOINT: 30.0, 40.0\n" 51 | 52 | 53 | def test_multilinestringmodel_pretty_print(capsys): 54 | mock_multilinestring.pretty_print() 55 | captured = capsys.readouterr() 56 | assert captured.out == ( 57 | "MULTILINESTRING:\n\tLINE STRING:\n\t\tPOINT: 10.0, 20.0\n\t\tPOINT: 30.0, 40.0\n" 58 | "\tLINE STRING:\n\t\tPOINT: 10.0, 20.0\n\t\tPOINT: 30.0, 40.0\n" 59 | ) 60 | 61 | 62 | def test_multipolygonmodel_pretty_print(capsys): 63 | mock_multipolygon.pretty_print() 64 | captured = capsys.readouterr() 65 | assert captured.out == ( 66 | "MULTIPOLYGON:\n\tPOLYGON:\n\t\tEXTERIOR:\n\t\t\tLINE STRING:\n\t\t\t\tPOINT: 10.0, 20.0\n\t\t\t\tPOINT: 30.0, 40.0\n" 67 | "\t\tINTERIORS:\n\t\t\tLINE STRING:\n\t\t\t\tPOINT: 10.0, 20.0\n\t\t\t\tPOINT: 30.0, 40.0\n" 68 | ) 69 | -------------------------------------------------------------------------------- /tests/wkt_validate_objects_test.py: -------------------------------------------------------------------------------- 1 | import pytest # noqa 2 | 3 | from traveltimepy import Coordinates 4 | from traveltimepy.wkt import LineStringModel, PointModel 5 | 6 | mock_point1 = PointModel(coordinates=Coordinates(lat=10, lng=20)) 7 | mock_point2 = PointModel(coordinates=Coordinates(lat=30, lng=40)) 8 | 9 | 10 | def test_linestring_minimum_coordinates_valid(): 11 | # Create LineStringModel with two valid coordinates 12 | linestring = LineStringModel(coordinates=[mock_point1, mock_point2]) 13 | assert len(linestring.coordinates) == 2 14 | 15 | 16 | def test_linestring_minimum_coordinates_invalid(): 17 | # Test LineStringModel with less than two coordinates 18 | with pytest.raises(ValueError) as excinfo: 19 | LineStringModel(coordinates=[mock_point1]) 20 | assert "LineString must have at least 2 coordinates." in str(excinfo.value) 21 | -------------------------------------------------------------------------------- /traveltimepy/__init__.py: -------------------------------------------------------------------------------- 1 | """Python sdk for working with traveltime api""" 2 | 3 | from importlib.metadata import version, PackageNotFoundError 4 | 5 | try: 6 | __version__ = version(__name__) 7 | except PackageNotFoundError: 8 | # package is not installed 9 | pass 10 | 11 | from traveltimepy.dto.transportation import ( 12 | PublicTransport, 13 | Driving, 14 | Ferry, 15 | Walking, 16 | Cycling, 17 | DrivingTrain, 18 | CyclingPublicTransport, 19 | MaxChanges, 20 | ) 21 | from traveltimepy.dto.requests.time_filter_proto import ( 22 | ProtoTransportation, 23 | ProtoCountry, 24 | ) 25 | from traveltimepy.dto.common import ( 26 | Coordinates, 27 | H3Centroid, 28 | GeohashCentroid, 29 | Location, 30 | Property, 31 | CellProperty, 32 | Snapping, 33 | PropertyProto, 34 | FullRange, 35 | Range, 36 | Rectangle, 37 | LevelOfDetail, 38 | DrivingTrafficModel, 39 | ) 40 | 41 | from traveltimepy.sdk import TravelTimeSdk 42 | from traveltimepy.dto.requests.time_filter_fast import Transportation 43 | from traveltimepy.dto.requests.postcodes_zones import ZonesProperty 44 | 45 | __all__ = [ 46 | "__version__", 47 | "PublicTransport", 48 | "Driving", 49 | "Ferry", 50 | "Walking", 51 | "Cycling", 52 | "DrivingTrain", 53 | "CyclingPublicTransport", 54 | "MaxChanges", 55 | "ProtoTransportation", 56 | "ProtoCountry", 57 | "Coordinates", 58 | "H3Centroid", 59 | "GeohashCentroid", 60 | "Snapping", 61 | "Location", 62 | "Property", 63 | "CellProperty", 64 | "PropertyProto", 65 | "FullRange", 66 | "Range", 67 | "Rectangle", 68 | "LevelOfDetail", 69 | "TravelTimeSdk", 70 | "Transportation", 71 | "ZonesProperty", 72 | "DrivingTrafficModel", 73 | ] 74 | -------------------------------------------------------------------------------- /traveltimepy/accept_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class AcceptType(Enum): 5 | JSON = "application/json" 6 | WKT = "application/vnd.wkt+json" 7 | WKT_NO_HOLES = "application/vnd.wkt-no-holes+json" 8 | BOUNDING_BOXES_JSON = "application/vnd.bounding-boxes+json" 9 | GEO_JSON = "application/geo+json" 10 | OCTET_STREAM = "application/octet-stream" 11 | -------------------------------------------------------------------------------- /traveltimepy/dto/__init__.py: -------------------------------------------------------------------------------- 1 | """Dto models for interacting with traveltime api""" 2 | -------------------------------------------------------------------------------- /traveltimepy/dto/common.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, time 2 | from enum import Enum 3 | from typing import List, Union, Optional 4 | 5 | from pydantic import field_validator 6 | from typing_extensions import Literal 7 | from pydantic.main import BaseModel 8 | 9 | 10 | class Coordinates(BaseModel): 11 | lat: float 12 | lng: float 13 | 14 | @field_validator("lat") 15 | @classmethod 16 | def validate_latitude(cls, v): 17 | if not -90 <= v <= 90: 18 | raise ValueError("Latitude must be between -90 and 90.") 19 | return v 20 | 21 | @field_validator("lng") 22 | @classmethod 23 | def validate_longitude(cls, v): 24 | if not -180 <= v <= 180: 25 | raise ValueError("Longitude must be between -180 and 180.") 26 | return v 27 | 28 | 29 | class GeohashCentroid(BaseModel): 30 | geohash_centroid: str 31 | 32 | 33 | class H3Centroid(BaseModel): 34 | h3_centroid: str 35 | 36 | 37 | class Location(BaseModel): 38 | id: str 39 | coords: Coordinates 40 | 41 | def __hash__(self): 42 | return hash(self.id) 43 | 44 | 45 | class BasicPart(BaseModel): 46 | id: int 47 | mode: str 48 | directions: str 49 | distance: int 50 | travel_time: int 51 | coords: List[Coordinates] 52 | type: Literal["basic"] 53 | 54 | 55 | class RoadPart(BaseModel): 56 | id: int 57 | mode: str 58 | directions: str 59 | distance: int 60 | travel_time: int 61 | coords: List[Coordinates] 62 | type: Literal["road"] 63 | road: Optional[str] = None 64 | turn: Optional[str] = None 65 | 66 | 67 | class StartEndPart(BaseModel): 68 | id: int 69 | mode: str 70 | directions: str 71 | distance: int 72 | travel_time: int 73 | coords: List[Coordinates] 74 | type: Literal["start_end"] 75 | direction: str 76 | 77 | 78 | class PublicTransportPart(BaseModel): 79 | id: int 80 | mode: str 81 | directions: str 82 | distance: int 83 | travel_time: int 84 | coords: List[Coordinates] 85 | line: str 86 | departure_station: str 87 | arrival_station: str 88 | departs_at: time 89 | arrives_at: time 90 | num_stops: int 91 | type: Literal["public_transport"] 92 | 93 | 94 | class Route(BaseModel): 95 | departure_time: datetime 96 | arrival_time: datetime 97 | parts: List[Union[BasicPart, PublicTransportPart, StartEndPart, RoadPart]] 98 | 99 | 100 | class Ticket(BaseModel): 101 | type: str 102 | price: float 103 | currency: str 104 | 105 | 106 | class FareBreakdown(BaseModel): 107 | modes: List[str] 108 | route_part_ids: List[int] 109 | tickets: List[Ticket] 110 | 111 | 112 | class Fares(BaseModel): 113 | breakdown: List[FareBreakdown] 114 | tickets_total: List[Ticket] 115 | 116 | 117 | class Rectangle(BaseModel): 118 | min_lat: float 119 | max_lat: float 120 | min_lng: float 121 | max_lng: float 122 | 123 | def to_str(self): 124 | return f"{self.min_lat},{self.min_lng},{self.max_lat},{self.max_lng}" 125 | 126 | 127 | class Property(str, Enum): 128 | TRAVEL_TIME = "travel_time" 129 | DISTANCE = "distance" 130 | ROUTE = "route" 131 | FARES = "fares" 132 | 133 | 134 | class CellProperty(str, Enum): 135 | MIN = "min" 136 | MAX = "max" 137 | MEAN = "mean" 138 | 139 | 140 | class SnappingPenalty(str, Enum): 141 | ENABLED = "enabled" 142 | DISABLED = "disabled" 143 | 144 | 145 | class SnappingAcceptRoads(str, Enum): 146 | BOTH_DRIVABLE_AND_WALKABLE = "both_drivable_and_walkable" 147 | ANY_DRIVABLE = "any_drivable" 148 | 149 | 150 | class DrivingTrafficModel(str, Enum): 151 | OPTIMISTIC = "optimistic" 152 | BALANCED = "balanced" 153 | PESSIMISTIC = "pessimistic" 154 | 155 | 156 | class Snapping(BaseModel): 157 | penalty: Optional[SnappingPenalty] = SnappingPenalty.ENABLED 158 | accept_roads: Optional[SnappingAcceptRoads] = ( 159 | SnappingAcceptRoads.BOTH_DRIVABLE_AND_WALKABLE 160 | ) 161 | 162 | 163 | class PropertyProto(int, Enum): 164 | DISTANCE = 1 165 | 166 | 167 | class FullRange(BaseModel): 168 | enabled: bool 169 | max_results: int 170 | width: int 171 | 172 | 173 | class Range(BaseModel): 174 | enabled: bool 175 | width: int 176 | 177 | 178 | class LevelOfDetail(BaseModel): 179 | scale_type: Literal["simple", "simple_numeric", "coarse_grid"] = "simple" 180 | level: Optional[Union[int, str]] = None 181 | square_size: Optional[int] = None 182 | 183 | 184 | class PolygonsFilter(BaseModel): 185 | limit: int 186 | 187 | 188 | class RenderMode(str, Enum): 189 | APPROXIMATE_TIME_FILTER = "approximate_time_filter" 190 | ROAD_BUFFERING = "road_buffering" 191 | 192 | 193 | class TimeInfo: 194 | def __init__(self, time_value: datetime): 195 | self.value = time_value 196 | 197 | 198 | class DepartureTime(TimeInfo): 199 | pass 200 | 201 | 202 | class ArrivalTime(TimeInfo): 203 | pass 204 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/__init__.py: -------------------------------------------------------------------------------- 1 | """Module with traveltime requests models""" 2 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/distance_map.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from datetime import datetime 3 | 4 | from typing import List, Optional 5 | 6 | from pydantic.main import BaseModel 7 | 8 | from traveltimepy import ( 9 | Coordinates, 10 | PublicTransport, 11 | Driving, 12 | Ferry, 13 | Walking, 14 | Cycling, 15 | DrivingTrain, 16 | CyclingPublicTransport, 17 | LevelOfDetail, 18 | ) 19 | from traveltimepy.dto.common import PolygonsFilter, Snapping 20 | from traveltimepy.dto.requests.request import TravelTimeRequest 21 | from traveltimepy.dto.responses.time_map import TimeMapResponse 22 | from traveltimepy.itertools import split, flatten 23 | 24 | 25 | class DepartureSearch(BaseModel): 26 | id: str 27 | coords: Coordinates 28 | departure_time: datetime 29 | travel_distance: int 30 | transportation: typing.Union[ 31 | PublicTransport, 32 | Driving, 33 | Ferry, 34 | Walking, 35 | Cycling, 36 | DrivingTrain, 37 | CyclingPublicTransport, 38 | ] 39 | level_of_detail: Optional[LevelOfDetail] 40 | snapping: Optional[Snapping] 41 | polygons_filter: Optional[PolygonsFilter] 42 | no_holes: Optional[bool] 43 | 44 | 45 | class ArrivalSearch(BaseModel): 46 | id: str 47 | coords: Coordinates 48 | arrival_time: datetime 49 | travel_distance: int 50 | transportation: typing.Union[ 51 | PublicTransport, 52 | Driving, 53 | Ferry, 54 | Walking, 55 | Cycling, 56 | DrivingTrain, 57 | CyclingPublicTransport, 58 | ] 59 | level_of_detail: Optional[LevelOfDetail] 60 | snapping: Optional[Snapping] 61 | polygons_filter: Optional[PolygonsFilter] 62 | no_holes: Optional[bool] 63 | 64 | 65 | class Intersection(BaseModel): 66 | id: str 67 | search_ids: List[str] 68 | 69 | 70 | class Union(BaseModel): 71 | id: str 72 | search_ids: List[str] 73 | 74 | 75 | class DistanceMapRequest(TravelTimeRequest[TimeMapResponse]): 76 | departure_searches: List[DepartureSearch] 77 | arrival_searches: List[ArrivalSearch] 78 | unions: List[Union] 79 | intersections: List[Intersection] 80 | 81 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 82 | return [ 83 | DistanceMapRequest( 84 | departure_searches=departures, 85 | arrival_searches=arrivals, 86 | unions=self.unions, 87 | intersections=self.intersections, 88 | ) 89 | for departures, arrivals in split( 90 | self.departure_searches, self.arrival_searches, window_size 91 | ) 92 | ] 93 | 94 | def merge(self, responses: List[TimeMapResponse]) -> TimeMapResponse: 95 | if len(self.unions) != 0: 96 | return TimeMapResponse( 97 | results=list( 98 | filter( 99 | lambda res: res.search_id == "Union search", 100 | flatten([response.results for response in responses]), 101 | ) 102 | ) 103 | ) 104 | elif len(self.intersections) != 0: 105 | return TimeMapResponse( 106 | results=list( 107 | filter( 108 | lambda res: res.search_id == "Intersection search", 109 | flatten([response.results for response in responses]), 110 | ) 111 | ) 112 | ) 113 | else: 114 | return TimeMapResponse( 115 | results=sorted( 116 | flatten([response.results for response in responses]), 117 | key=lambda res: res.search_id, 118 | ) 119 | ) 120 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/geohash.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from datetime import datetime 3 | 4 | from typing import List, Optional 5 | 6 | from pydantic.main import BaseModel 7 | 8 | from traveltimepy import ( 9 | Range, 10 | PublicTransport, 11 | Driving, 12 | Ferry, 13 | Walking, 14 | Cycling, 15 | DrivingTrain, 16 | CyclingPublicTransport, 17 | ) 18 | from traveltimepy.dto.common import CellProperty, Coordinates, GeohashCentroid, Snapping 19 | from traveltimepy.dto.requests.request import TravelTimeRequest 20 | from traveltimepy.dto.responses.geohash import GeohashResponse 21 | from traveltimepy.itertools import split, flatten 22 | 23 | 24 | class DepartureSearch(BaseModel): 25 | id: str 26 | coords: typing.Union[Coordinates, GeohashCentroid] 27 | departure_time: datetime 28 | travel_time: int 29 | transportation: typing.Union[ 30 | PublicTransport, 31 | Driving, 32 | Ferry, 33 | Walking, 34 | Cycling, 35 | DrivingTrain, 36 | CyclingPublicTransport, 37 | ] 38 | range: Optional[Range] 39 | snapping: Optional[Snapping] 40 | 41 | 42 | class ArrivalSearch(BaseModel): 43 | id: str 44 | coords: typing.Union[Coordinates, GeohashCentroid] 45 | arrival_time: datetime 46 | travel_time: int 47 | transportation: typing.Union[ 48 | PublicTransport, 49 | Driving, 50 | Ferry, 51 | Walking, 52 | Cycling, 53 | DrivingTrain, 54 | CyclingPublicTransport, 55 | ] 56 | range: Optional[Range] 57 | snapping: Optional[Snapping] 58 | 59 | 60 | class Intersection(BaseModel): 61 | id: str 62 | search_ids: List[str] 63 | 64 | 65 | class Union(BaseModel): 66 | id: str 67 | search_ids: List[str] 68 | 69 | 70 | class GeohashRequest(TravelTimeRequest[GeohashResponse]): 71 | resolution: int 72 | properties: List[CellProperty] 73 | departure_searches: List[DepartureSearch] 74 | arrival_searches: List[ArrivalSearch] 75 | unions: List[Union] 76 | intersections: List[Intersection] 77 | 78 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 79 | return [ 80 | GeohashRequest( 81 | resolution=self.resolution, 82 | properties=self.properties, 83 | departure_searches=departures, 84 | arrival_searches=arrivals, 85 | unions=self.unions, 86 | intersections=self.intersections, 87 | ) 88 | for departures, arrivals in split( 89 | self.departure_searches, self.arrival_searches, window_size 90 | ) 91 | ] 92 | 93 | def merge(self, responses: List[GeohashResponse]) -> GeohashResponse: 94 | if len(self.unions) != 0: 95 | return GeohashResponse( 96 | results=list( 97 | filter( 98 | lambda res: res.search_id == "Union search", 99 | flatten([response.results for response in responses]), 100 | ) 101 | ) 102 | ) 103 | elif len(self.intersections) != 0: 104 | return GeohashResponse( 105 | results=list( 106 | filter( 107 | lambda res: res.search_id == "Intersection search", 108 | flatten([response.results for response in responses]), 109 | ) 110 | ) 111 | ) 112 | else: 113 | return GeohashResponse( 114 | results=sorted( 115 | flatten([response.results for response in responses]), 116 | key=lambda res: res.search_id, 117 | ) 118 | ) 119 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/geohash_fast.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | import typing 3 | 4 | from pydantic import BaseModel 5 | 6 | from traveltimepy.dto.common import ( 7 | CellProperty, 8 | Coordinates, 9 | GeohashCentroid, 10 | Snapping, 11 | ) 12 | from traveltimepy.dto.requests.request import TravelTimeRequest 13 | from traveltimepy.dto.responses.geohash import GeohashResponse 14 | from traveltimepy.itertools import split, flatten 15 | from traveltimepy.dto.requests.time_filter_fast import Transportation 16 | 17 | 18 | class Search(BaseModel): 19 | id: str 20 | coords: typing.Union[Coordinates, GeohashCentroid] 21 | transportation: Transportation 22 | travel_time: int 23 | arrival_time_period: str 24 | snapping: Optional[Snapping] 25 | 26 | 27 | class ArrivalSearches(BaseModel): 28 | many_to_one: List[Search] 29 | one_to_many: List[Search] 30 | 31 | 32 | class GeohashFastRequest(TravelTimeRequest[GeohashResponse]): 33 | resolution: int 34 | properties: List[CellProperty] 35 | arrival_searches: ArrivalSearches 36 | 37 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 38 | return [ 39 | GeohashFastRequest( 40 | resolution=self.resolution, 41 | properties=self.properties, 42 | arrival_searches=ArrivalSearches( 43 | one_to_many=one_to_many, many_to_one=many_to_one 44 | ), 45 | ) 46 | for one_to_many, many_to_one in split( 47 | self.arrival_searches.one_to_many, 48 | self.arrival_searches.many_to_one, 49 | window_size, 50 | ) 51 | ] 52 | 53 | def merge(self, responses: List[GeohashResponse]) -> GeohashResponse: 54 | return GeohashResponse( 55 | results=flatten([response.results for response in responses]) 56 | ) 57 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/h3.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from datetime import datetime 3 | 4 | from typing import List, Optional 5 | 6 | from pydantic.main import BaseModel 7 | 8 | from traveltimepy import ( 9 | Range, 10 | PublicTransport, 11 | Driving, 12 | Ferry, 13 | Walking, 14 | Cycling, 15 | DrivingTrain, 16 | CyclingPublicTransport, 17 | ) 18 | from traveltimepy.dto.common import CellProperty, Coordinates, H3Centroid, Snapping 19 | from traveltimepy.dto.requests.request import TravelTimeRequest 20 | from traveltimepy.dto.responses.h3 import H3Response 21 | from traveltimepy.itertools import split, flatten 22 | 23 | 24 | class DepartureSearch(BaseModel): 25 | id: str 26 | coords: typing.Union[Coordinates, H3Centroid] 27 | departure_time: datetime 28 | travel_time: int 29 | transportation: typing.Union[ 30 | PublicTransport, 31 | Driving, 32 | Ferry, 33 | Walking, 34 | Cycling, 35 | DrivingTrain, 36 | CyclingPublicTransport, 37 | ] 38 | range: Optional[Range] 39 | snapping: Optional[Snapping] 40 | 41 | 42 | class ArrivalSearch(BaseModel): 43 | id: str 44 | coords: typing.Union[Coordinates, H3Centroid] 45 | arrival_time: datetime 46 | travel_time: int 47 | transportation: typing.Union[ 48 | PublicTransport, 49 | Driving, 50 | Ferry, 51 | Walking, 52 | Cycling, 53 | DrivingTrain, 54 | CyclingPublicTransport, 55 | ] 56 | range: Optional[Range] 57 | snapping: Optional[Snapping] 58 | 59 | 60 | class Intersection(BaseModel): 61 | id: str 62 | search_ids: List[str] 63 | 64 | 65 | class Union(BaseModel): 66 | id: str 67 | search_ids: List[str] 68 | 69 | 70 | class H3Request(TravelTimeRequest[H3Response]): 71 | resolution: int 72 | properties: List[CellProperty] 73 | departure_searches: List[DepartureSearch] 74 | arrival_searches: List[ArrivalSearch] 75 | unions: List[Union] 76 | intersections: List[Intersection] 77 | 78 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 79 | return [ 80 | H3Request( 81 | resolution=self.resolution, 82 | properties=self.properties, 83 | departure_searches=departures, 84 | arrival_searches=arrivals, 85 | unions=self.unions, 86 | intersections=self.intersections, 87 | ) 88 | for departures, arrivals in split( 89 | self.departure_searches, self.arrival_searches, window_size 90 | ) 91 | ] 92 | 93 | def merge(self, responses: List[H3Response]) -> H3Response: 94 | if len(self.unions) != 0: 95 | return H3Response( 96 | results=list( 97 | filter( 98 | lambda res: res.search_id == "Union search", 99 | flatten([response.results for response in responses]), 100 | ) 101 | ) 102 | ) 103 | elif len(self.intersections) != 0: 104 | return H3Response( 105 | results=list( 106 | filter( 107 | lambda res: res.search_id == "Intersection search", 108 | flatten([response.results for response in responses]), 109 | ) 110 | ) 111 | ) 112 | else: 113 | return H3Response( 114 | results=sorted( 115 | flatten([response.results for response in responses]), 116 | key=lambda res: res.search_id, 117 | ) 118 | ) 119 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/h3_fast.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | import typing 3 | 4 | from pydantic import BaseModel 5 | 6 | from traveltimepy.dto.common import ( 7 | CellProperty, 8 | Coordinates, 9 | H3Centroid, 10 | Snapping, 11 | ) 12 | from traveltimepy.dto.requests.request import TravelTimeRequest 13 | from traveltimepy.dto.responses.h3 import H3Response 14 | from traveltimepy.itertools import split, flatten 15 | from traveltimepy.dto.requests.time_filter_fast import Transportation 16 | 17 | 18 | class Search(BaseModel): 19 | id: str 20 | coords: typing.Union[Coordinates, H3Centroid] 21 | transportation: Transportation 22 | travel_time: int 23 | arrival_time_period: str 24 | snapping: Optional[Snapping] 25 | 26 | 27 | class ArrivalSearches(BaseModel): 28 | many_to_one: List[Search] 29 | one_to_many: List[Search] 30 | 31 | 32 | class H3FastRequest(TravelTimeRequest[H3Response]): 33 | resolution: int 34 | properties: List[CellProperty] 35 | arrival_searches: ArrivalSearches 36 | 37 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 38 | return [ 39 | H3FastRequest( 40 | resolution=self.resolution, 41 | properties=self.properties, 42 | arrival_searches=ArrivalSearches( 43 | one_to_many=one_to_many, many_to_one=many_to_one 44 | ), 45 | ) 46 | for one_to_many, many_to_one in split( 47 | self.arrival_searches.one_to_many, 48 | self.arrival_searches.many_to_one, 49 | window_size, 50 | ) 51 | ] 52 | 53 | def merge(self, responses: List[H3Response]) -> H3Response: 54 | return H3Response(results=flatten([response.results for response in responses])) 55 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/postcodes.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List, Union, Optional 3 | 4 | from pydantic import BaseModel 5 | 6 | from traveltimepy.dto.common import Coordinates, Property, FullRange 7 | from traveltimepy.dto.requests.request import TravelTimeRequest 8 | from traveltimepy.dto.responses.postcodes import PostcodesResponse 9 | from traveltimepy.itertools import split, flatten 10 | from traveltimepy.dto.transportation import ( 11 | PublicTransport, 12 | Driving, 13 | Ferry, 14 | Walking, 15 | Cycling, 16 | DrivingTrain, 17 | CyclingPublicTransport, 18 | ) 19 | 20 | 21 | class ArrivalSearch(BaseModel): 22 | id: str 23 | coords: Coordinates 24 | travel_time: int 25 | arrival_time: datetime 26 | transportation: Union[ 27 | PublicTransport, 28 | Driving, 29 | Ferry, 30 | Walking, 31 | Cycling, 32 | DrivingTrain, 33 | CyclingPublicTransport, 34 | ] 35 | properties: List[Property] 36 | range: Optional[FullRange] 37 | 38 | 39 | class DepartureSearch(BaseModel): 40 | id: str 41 | coords: Coordinates 42 | travel_time: int 43 | departure_time: datetime 44 | transportation: Union[ 45 | PublicTransport, 46 | Driving, 47 | Ferry, 48 | Walking, 49 | Cycling, 50 | DrivingTrain, 51 | CyclingPublicTransport, 52 | ] 53 | properties: List[Property] 54 | range: Optional[FullRange] 55 | 56 | 57 | class PostcodesRequest(TravelTimeRequest[PostcodesResponse]): 58 | departure_searches: List[DepartureSearch] 59 | arrival_searches: List[ArrivalSearch] 60 | 61 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 62 | return [ 63 | PostcodesRequest(departure_searches=departures, arrival_searches=arrivals) 64 | for departures, arrivals in split( 65 | self.departure_searches, self.arrival_searches, window_size 66 | ) 67 | ] 68 | 69 | def merge(self, responses: List[PostcodesResponse]) -> PostcodesResponse: 70 | return PostcodesResponse( 71 | results=sorted( 72 | flatten([response.results for response in responses]), 73 | key=lambda res: res.search_id, 74 | ) 75 | ) 76 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/postcodes_zones.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from enum import Enum 3 | from typing import List, Union, Optional 4 | 5 | from pydantic import BaseModel 6 | 7 | from traveltimepy.dto.common import Coordinates, FullRange 8 | from traveltimepy.dto.requests.request import TravelTimeRequest 9 | from traveltimepy.dto.responses.zones import ( 10 | PostcodesDistrictsResponse, 11 | PostcodesSectorsResponse, 12 | ) 13 | from traveltimepy.itertools import split, flatten 14 | from traveltimepy.dto.transportation import ( 15 | PublicTransport, 16 | Driving, 17 | Ferry, 18 | Walking, 19 | Cycling, 20 | DrivingTrain, 21 | CyclingPublicTransport, 22 | ) 23 | 24 | 25 | class ZonesProperty(str, Enum): 26 | TRAVEL_TIME_REACHABLE = "travel_time_reachable" 27 | TRAVEL_TIME_ALL = "travel_time_all" 28 | COVERAGE = "coverage" 29 | 30 | 31 | class ArrivalSearch(BaseModel): 32 | id: str 33 | coords: Coordinates 34 | travel_time: int 35 | arrival_time: datetime 36 | reachable_postcodes_threshold: float 37 | transportation: Union[ 38 | PublicTransport, 39 | Driving, 40 | Ferry, 41 | Walking, 42 | Cycling, 43 | DrivingTrain, 44 | CyclingPublicTransport, 45 | ] 46 | properties: List[ZonesProperty] 47 | range: Optional[FullRange] 48 | 49 | 50 | class DepartureSearch(BaseModel): 51 | id: str 52 | coords: Coordinates 53 | travel_time: int 54 | departure_time: datetime 55 | reachable_postcodes_threshold: float 56 | transportation: Union[ 57 | PublicTransport, 58 | Driving, 59 | Ferry, 60 | Walking, 61 | Cycling, 62 | DrivingTrain, 63 | CyclingPublicTransport, 64 | ] 65 | properties: List[ZonesProperty] 66 | range: Optional[FullRange] 67 | 68 | 69 | class PostcodesSectorsRequest(TravelTimeRequest[PostcodesSectorsResponse]): 70 | departure_searches: List[DepartureSearch] 71 | arrival_searches: List[ArrivalSearch] 72 | 73 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 74 | return [ 75 | PostcodesSectorsRequest( 76 | departure_searches=departures, arrival_searches=arrivals 77 | ) 78 | for departures, arrivals in split( 79 | self.departure_searches, self.arrival_searches, window_size 80 | ) 81 | ] 82 | 83 | def merge( 84 | self, responses: List[PostcodesSectorsResponse] 85 | ) -> PostcodesSectorsResponse: 86 | return PostcodesSectorsResponse( 87 | results=sorted( 88 | flatten([response.results for response in responses]), 89 | key=lambda res: res.search_id, 90 | ) 91 | ) 92 | 93 | 94 | class PostcodesDistrictsRequest(TravelTimeRequest[PostcodesDistrictsResponse]): 95 | departure_searches: List[DepartureSearch] 96 | arrival_searches: List[ArrivalSearch] 97 | 98 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 99 | return [ 100 | PostcodesDistrictsRequest( 101 | departure_searches=departures, arrival_searches=arrivals 102 | ) 103 | for departures, arrivals in split( 104 | self.departure_searches, self.arrival_searches, window_size 105 | ) 106 | ] 107 | 108 | def merge( 109 | self, responses: List[PostcodesDistrictsResponse] 110 | ) -> PostcodesDistrictsResponse: 111 | return PostcodesDistrictsResponse( 112 | results=sorted( 113 | flatten([response.results for response in responses]), 114 | key=lambda res: res.search_id, 115 | ) 116 | ) 117 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/request.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from typing import List, TypeVar, Generic 5 | 6 | from pydantic import BaseModel 7 | 8 | T = TypeVar("T") 9 | 10 | 11 | class TravelTimeRequest(ABC, BaseModel, Generic[T]): 12 | @abstractmethod 13 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 14 | pass 15 | 16 | @abstractmethod 17 | def merge(self, responses: List[T]) -> T: 18 | pass 19 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/routes.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List, Optional, Union 3 | 4 | from pydantic.main import BaseModel 5 | 6 | from traveltimepy.dto.common import Location, Property, FullRange, Snapping 7 | from traveltimepy.dto.transportation import ( 8 | PublicTransport, 9 | Driving, 10 | Ferry, 11 | Walking, 12 | Cycling, 13 | DrivingTrain, 14 | CyclingPublicTransport, 15 | ) 16 | from traveltimepy.dto.requests.request import TravelTimeRequest 17 | from traveltimepy.dto.responses.routes import RoutesResponse 18 | from traveltimepy.itertools import split, flatten 19 | 20 | 21 | class ArrivalSearch(BaseModel): 22 | id: str 23 | departure_location_ids: List[str] 24 | arrival_location_id: str 25 | arrival_time: datetime 26 | transportation: Union[ 27 | PublicTransport, 28 | Driving, 29 | Ferry, 30 | Walking, 31 | Cycling, 32 | DrivingTrain, 33 | CyclingPublicTransport, 34 | ] 35 | properties: List[Property] 36 | range: Optional[FullRange] 37 | snapping: Optional[Snapping] 38 | 39 | 40 | class DepartureSearch(BaseModel): 41 | id: str 42 | arrival_location_ids: List[str] 43 | departure_location_id: str 44 | departure_time: datetime 45 | transportation: Union[ 46 | PublicTransport, 47 | Driving, 48 | Ferry, 49 | Walking, 50 | Cycling, 51 | DrivingTrain, 52 | CyclingPublicTransport, 53 | ] 54 | properties: List[Property] 55 | range: Optional[FullRange] 56 | snapping: Optional[Snapping] 57 | 58 | 59 | class RoutesRequest(TravelTimeRequest[RoutesResponse]): 60 | locations: List[Location] 61 | departure_searches: List[DepartureSearch] 62 | arrival_searches: List[ArrivalSearch] 63 | 64 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 65 | return [ 66 | RoutesRequest( 67 | locations=self.locations, 68 | departure_searches=departures, 69 | arrival_searches=arrivals, 70 | ) 71 | for departures, arrivals in split( 72 | self.departure_searches, self.arrival_searches, window_size 73 | ) 74 | ] 75 | 76 | def merge(self, responses: List[RoutesResponse]) -> RoutesResponse: 77 | return RoutesResponse( 78 | results=flatten([response.results for response in responses]) 79 | ) 80 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/supported_locations.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from traveltimepy.dto.common import Location 4 | from traveltimepy.dto.requests.request import TravelTimeRequest 5 | from traveltimepy.dto.responses.supported_locations import SupportedLocationsResponse 6 | from traveltimepy.itertools import flatten 7 | 8 | 9 | class SupportedLocationsRequest(TravelTimeRequest[SupportedLocationsResponse]): 10 | locations: List[Location] 11 | 12 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 13 | return [SupportedLocationsRequest(locations=self.locations)] 14 | 15 | def merge( 16 | self, responses: List[SupportedLocationsResponse] 17 | ) -> SupportedLocationsResponse: 18 | return SupportedLocationsResponse( 19 | locations=flatten([response.locations for response in responses]), 20 | unsupported_locations=flatten( 21 | [response.unsupported_locations for response in responses] 22 | ), 23 | ) 24 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/time_filter.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List, Optional, Union 3 | 4 | from pydantic.main import BaseModel 5 | 6 | from traveltimepy.dto.common import Location, FullRange, Property, Snapping 7 | from traveltimepy.dto.requests.request import TravelTimeRequest 8 | from traveltimepy.dto.responses.time_filter import TimeFilterResponse 9 | from traveltimepy.itertools import split, flatten 10 | from traveltimepy.dto.transportation import ( 11 | PublicTransport, 12 | Driving, 13 | Ferry, 14 | Walking, 15 | Cycling, 16 | DrivingTrain, 17 | CyclingPublicTransport, 18 | ) 19 | 20 | 21 | class ArrivalSearch(BaseModel): 22 | id: str 23 | departure_location_ids: List[str] 24 | arrival_location_id: str 25 | arrival_time: datetime 26 | travel_time: int 27 | transportation: Union[ 28 | PublicTransport, 29 | Driving, 30 | Ferry, 31 | Walking, 32 | Cycling, 33 | DrivingTrain, 34 | CyclingPublicTransport, 35 | ] 36 | properties: List[Property] 37 | range: Optional[FullRange] 38 | snapping: Optional[Snapping] 39 | 40 | 41 | class DepartureSearch(BaseModel): 42 | id: str 43 | arrival_location_ids: List[str] 44 | departure_location_id: str 45 | departure_time: datetime 46 | travel_time: int 47 | transportation: Union[ 48 | PublicTransport, 49 | Driving, 50 | Ferry, 51 | Walking, 52 | Cycling, 53 | DrivingTrain, 54 | CyclingPublicTransport, 55 | ] 56 | properties: List[Property] 57 | range: Optional[FullRange] 58 | snapping: Optional[Snapping] 59 | 60 | 61 | class TimeFilterRequest(TravelTimeRequest[TimeFilterResponse]): 62 | locations: List[Location] 63 | departure_searches: List[DepartureSearch] 64 | arrival_searches: List[ArrivalSearch] 65 | 66 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 67 | return [ 68 | TimeFilterRequest( 69 | locations=self.locations, 70 | departure_searches=departures, 71 | arrival_searches=arrivals, 72 | ) 73 | for departures, arrivals in split( 74 | self.departure_searches, self.arrival_searches, window_size 75 | ) 76 | ] 77 | 78 | def merge(self, responses: List[TimeFilterResponse]) -> TimeFilterResponse: 79 | return TimeFilterResponse( 80 | results=flatten([response.results for response in responses]) 81 | ) 82 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/time_filter_fast.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from typing_extensions import Literal 3 | 4 | from pydantic import BaseModel 5 | 6 | from traveltimepy.dto.common import Location, Property, Snapping 7 | from traveltimepy.dto.requests.request import TravelTimeRequest 8 | from traveltimepy.dto.responses.time_filter_fast import TimeFilterFastResponse 9 | from traveltimepy.itertools import split, flatten 10 | 11 | 12 | class Transportation(BaseModel): 13 | type: Literal[ 14 | "public_transport", 15 | "driving", 16 | "cycling", 17 | "walking", 18 | "walking+ferry", 19 | "cycling+ferry", 20 | "driving+ferry", 21 | "driving+public_transport", 22 | ] 23 | 24 | 25 | class OneToMany(BaseModel): 26 | id: str 27 | departure_location_id: str 28 | arrival_location_ids: List[str] 29 | transportation: Transportation 30 | travel_time: int 31 | arrival_time_period: str 32 | properties: List[Property] 33 | snapping: Optional[Snapping] 34 | 35 | 36 | class ManyToOne(BaseModel): 37 | id: str 38 | arrival_location_id: str 39 | departure_location_ids: List[str] 40 | transportation: Transportation 41 | travel_time: int 42 | arrival_time_period: str 43 | properties: List[Property] 44 | snapping: Optional[Snapping] 45 | 46 | 47 | class ArrivalSearches(BaseModel): 48 | many_to_one: List[ManyToOne] 49 | one_to_many: List[OneToMany] 50 | 51 | 52 | class TimeFilterFastRequest(TravelTimeRequest[TimeFilterFastResponse]): 53 | locations: List[Location] 54 | arrival_searches: ArrivalSearches 55 | 56 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 57 | return [ 58 | TimeFilterFastRequest( 59 | locations=self.locations, 60 | arrival_searches=ArrivalSearches( 61 | one_to_many=one_to_many, many_to_one=many_to_one 62 | ), 63 | ) 64 | for one_to_many, many_to_one in split( 65 | self.arrival_searches.one_to_many, 66 | self.arrival_searches.many_to_one, 67 | window_size, 68 | ) 69 | ] 70 | 71 | def merge(self, responses: List[TimeFilterFastResponse]) -> TimeFilterFastResponse: 72 | return TimeFilterFastResponse( 73 | results=flatten([response.results for response in responses]) 74 | ) 75 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/time_filter_proto.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | from typing import ClassVar, Optional 4 | 5 | 6 | @dataclass 7 | class TransportationInfo: 8 | code: int 9 | name: str 10 | 11 | 12 | class ProtoTransportation(Enum): 13 | PUBLIC_TRANSPORT = TransportationInfo(0, "pt") 14 | DRIVING = TransportationInfo(1, "driving") 15 | DRIVING_AND_PUBLIC_TRANSPORT = TransportationInfo(2, "pt") 16 | DRIVING_FERRY = TransportationInfo(3, "driving+ferry") 17 | WALKING = TransportationInfo(4, "walking") 18 | CYCLING = TransportationInfo(5, "cycling") 19 | CYCLING_FERRY = TransportationInfo(6, "cycling+ferry") 20 | WALKING_FERRY = TransportationInfo(7, "walking+ferry") 21 | 22 | 23 | @dataclass 24 | class PublicTransportWithDetails: 25 | 26 | walking_time_to_station: Optional[int] = None 27 | """Limit on walking path duration. Must be > 0 and <= 1800""" 28 | 29 | TYPE: ClassVar[ProtoTransportation] = ProtoTransportation.PUBLIC_TRANSPORT 30 | 31 | 32 | @dataclass 33 | class DrivingAndPublicTransportWithDetails: 34 | 35 | walking_time_to_station: Optional[int] = None 36 | """Limit on walking path duration. Must be > 0 and <= 1800""" 37 | 38 | driving_time_to_station: Optional[int] = None 39 | """Limit on driving path duration. Must be > 0 and <= 1800""" 40 | 41 | parking_time: Optional[int] = None 42 | """ 43 | Constant penalty to simulate finding a parking spot in seconds. 44 | Cannot be negative. 45 | Must be less than the overall travel time limit. 46 | """ 47 | 48 | TYPE: ClassVar[ProtoTransportation] = ( 49 | ProtoTransportation.DRIVING_AND_PUBLIC_TRANSPORT 50 | ) 51 | 52 | 53 | class ProtoCountry(str, Enum): 54 | NETHERLANDS = "nl" 55 | AUSTRIA = "at" 56 | UNITED_KINGDOM = "uk" 57 | BELGIUM = "be" 58 | GERMANY = "de" 59 | FRANCE = "fr" 60 | IRELAND = "ie" 61 | LITHUANIA = "lt" 62 | UNITED_STATES = "us" 63 | SOUTH_AFRICA = "za" 64 | ROMANIA = "ro" 65 | PORTUGAL = "pt" 66 | PHILIPPINES = "ph" 67 | NEW_ZEALAND = "nz" 68 | NORWAY = "no" 69 | LATVIA = "lv" 70 | JAPAN = "jp" 71 | INDIA = "in" 72 | INDONESIA = "id" 73 | HUNGARY = "hu" 74 | GREECE = "gr" 75 | FINLAND = "fi" 76 | DENMARK = "dk" 77 | CANADA = "ca" 78 | AUSTRALIA = "au" 79 | SINGAPORE = "sg" 80 | SWITZERLAND = "ch" 81 | SPAIN = "es" 82 | ITALY = "it" 83 | POLAND = "pl" 84 | SWEDEN = "se" 85 | LIECHTENSTEIN = "li" 86 | MEXICO = "mx" 87 | SAUDI_ARABIA = "sa" 88 | SERBIA = "rs" 89 | SLOVENIA = "si" 90 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/time_map.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from datetime import datetime 3 | 4 | from typing import List, Optional 5 | 6 | from pydantic.main import BaseModel 7 | 8 | from traveltimepy import ( 9 | Coordinates, 10 | Range, 11 | PublicTransport, 12 | Driving, 13 | Ferry, 14 | Walking, 15 | Cycling, 16 | DrivingTrain, 17 | CyclingPublicTransport, 18 | LevelOfDetail, 19 | ) 20 | from traveltimepy.dto.common import PolygonsFilter, RenderMode, Snapping 21 | from traveltimepy.dto.requests.request import TravelTimeRequest 22 | from traveltimepy.dto.responses.time_map import TimeMapResponse 23 | from traveltimepy.itertools import split, flatten 24 | 25 | 26 | class DepartureSearch(BaseModel): 27 | id: str 28 | coords: Coordinates 29 | departure_time: datetime 30 | travel_time: int 31 | transportation: typing.Union[ 32 | PublicTransport, 33 | Driving, 34 | Ferry, 35 | Walking, 36 | Cycling, 37 | DrivingTrain, 38 | CyclingPublicTransport, 39 | ] 40 | range: Optional[Range] 41 | level_of_detail: Optional[LevelOfDetail] 42 | snapping: Optional[Snapping] 43 | polygons_filter: Optional[PolygonsFilter] 44 | remove_water_bodies: Optional[bool] 45 | render_mode: Optional[RenderMode] 46 | 47 | 48 | class ArrivalSearch(BaseModel): 49 | id: str 50 | coords: Coordinates 51 | arrival_time: datetime 52 | travel_time: int 53 | transportation: typing.Union[ 54 | PublicTransport, 55 | Driving, 56 | Ferry, 57 | Walking, 58 | Cycling, 59 | DrivingTrain, 60 | CyclingPublicTransport, 61 | ] 62 | range: Optional[Range] 63 | level_of_detail: Optional[LevelOfDetail] 64 | snapping: Optional[Snapping] 65 | polygons_filter: Optional[PolygonsFilter] 66 | remove_water_bodies: Optional[bool] 67 | render_mode: Optional[RenderMode] 68 | 69 | 70 | class Intersection(BaseModel): 71 | id: str 72 | search_ids: List[str] 73 | 74 | 75 | class Union(BaseModel): 76 | id: str 77 | search_ids: List[str] 78 | 79 | 80 | class TimeMapRequest(TravelTimeRequest[TimeMapResponse]): 81 | departure_searches: List[DepartureSearch] 82 | arrival_searches: List[ArrivalSearch] 83 | unions: List[Union] 84 | intersections: List[Intersection] 85 | 86 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 87 | return [ 88 | TimeMapRequest( 89 | departure_searches=departures, 90 | arrival_searches=arrivals, 91 | unions=self.unions, 92 | intersections=self.intersections, 93 | ) 94 | for departures, arrivals in split( 95 | self.departure_searches, self.arrival_searches, window_size 96 | ) 97 | ] 98 | 99 | def merge(self, responses: List[TimeMapResponse]) -> TimeMapResponse: 100 | if len(self.unions) != 0: 101 | return TimeMapResponse( 102 | results=list( 103 | filter( 104 | lambda res: res.search_id == "Union search", 105 | flatten([response.results for response in responses]), 106 | ) 107 | ) 108 | ) 109 | elif len(self.intersections) != 0: 110 | return TimeMapResponse( 111 | results=list( 112 | filter( 113 | lambda res: res.search_id == "Intersection search", 114 | flatten([response.results for response in responses]), 115 | ) 116 | ) 117 | ) 118 | else: 119 | return TimeMapResponse( 120 | results=sorted( 121 | flatten([response.results for response in responses]), 122 | key=lambda res: res.search_id, 123 | ) 124 | ) 125 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/time_map_fast.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | from traveltimepy.dto.common import ( 6 | Coordinates, 7 | LevelOfDetail, 8 | PolygonsFilter, 9 | RenderMode, 10 | Snapping, 11 | ) 12 | from traveltimepy.dto.requests.request import TravelTimeRequest 13 | from traveltimepy.dto.responses.time_map import TimeMapResponse 14 | from traveltimepy.itertools import split, flatten 15 | from traveltimepy.dto.requests.time_filter_fast import Transportation 16 | 17 | 18 | class Search(BaseModel): 19 | id: str 20 | coords: Coordinates 21 | transportation: Transportation 22 | travel_time: int 23 | arrival_time_period: str 24 | level_of_detail: Optional[LevelOfDetail] 25 | snapping: Optional[Snapping] 26 | polygons_filter: Optional[PolygonsFilter] 27 | render_mode: Optional[RenderMode] 28 | 29 | 30 | class ArrivalSearches(BaseModel): 31 | many_to_one: List[Search] 32 | one_to_many: List[Search] 33 | 34 | 35 | class TimeMapFastRequest(TravelTimeRequest[TimeMapResponse]): 36 | arrival_searches: ArrivalSearches 37 | 38 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 39 | return [ 40 | TimeMapFastRequest( 41 | arrival_searches=ArrivalSearches( 42 | one_to_many=one_to_many, many_to_one=many_to_one 43 | ), 44 | ) 45 | for one_to_many, many_to_one in split( 46 | self.arrival_searches.one_to_many, 47 | self.arrival_searches.many_to_one, 48 | window_size, 49 | ) 50 | ] 51 | 52 | def merge(self, responses: List[TimeMapResponse]) -> TimeMapResponse: 53 | return TimeMapResponse( 54 | results=flatten([response.results for response in responses]) 55 | ) 56 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/time_map_fast_geojson.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from geojson_pydantic import FeatureCollection 3 | 4 | from traveltimepy.dto.requests.request import TravelTimeRequest 5 | from traveltimepy.dto.requests.time_map_fast import ArrivalSearches 6 | from traveltimepy.itertools import split, flatten 7 | 8 | 9 | class TimeMapFastGeojsonRequest(TravelTimeRequest[FeatureCollection]): 10 | arrival_searches: ArrivalSearches 11 | 12 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 13 | return [ 14 | TimeMapFastGeojsonRequest( 15 | arrival_searches=ArrivalSearches( 16 | one_to_many=one_to_many, many_to_one=many_to_one 17 | ), 18 | ) 19 | for one_to_many, many_to_one in split( 20 | self.arrival_searches.one_to_many, 21 | self.arrival_searches.many_to_one, 22 | window_size, 23 | ) 24 | ] 25 | 26 | def merge(self, responses: List[FeatureCollection]) -> FeatureCollection: 27 | merged_features = flatten([response.features for response in responses]) 28 | return FeatureCollection(type="FeatureCollection", features=merged_features) 29 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/time_map_fast_wkt.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from traveltimepy.dto.requests.request import TravelTimeRequest 3 | from traveltimepy.dto.requests.time_map_fast import ArrivalSearches 4 | from traveltimepy.dto.responses.time_map_wkt import TimeMapWKTResponse 5 | from traveltimepy.itertools import split, flatten 6 | 7 | 8 | class TimeMapFastWKTRequest(TravelTimeRequest[TimeMapWKTResponse]): 9 | arrival_searches: ArrivalSearches 10 | 11 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 12 | return [ 13 | TimeMapFastWKTRequest( 14 | arrival_searches=ArrivalSearches( 15 | one_to_many=one_to_many, many_to_one=many_to_one 16 | ), 17 | ) 18 | for one_to_many, many_to_one in split( 19 | self.arrival_searches.one_to_many, 20 | self.arrival_searches.many_to_one, 21 | window_size, 22 | ) 23 | ] 24 | 25 | def merge(self, responses: List[TimeMapWKTResponse]) -> TimeMapWKTResponse: 26 | return TimeMapWKTResponse( 27 | results=sorted( 28 | flatten([response.results for response in responses]), 29 | key=lambda res: res.search_id, 30 | ) 31 | ) 32 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/time_map_geojson.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from geojson_pydantic import FeatureCollection 4 | from traveltimepy.dto.requests.request import TravelTimeRequest 5 | from traveltimepy.dto.requests.time_map import ( 6 | DepartureSearch, 7 | ArrivalSearch, 8 | ) 9 | from traveltimepy.itertools import split, flatten 10 | 11 | 12 | class TimeMapRequestGeojson(TravelTimeRequest[FeatureCollection]): 13 | departure_searches: List[DepartureSearch] 14 | arrival_searches: List[ArrivalSearch] 15 | 16 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 17 | return [ 18 | TimeMapRequestGeojson( 19 | departure_searches=departures, 20 | arrival_searches=arrivals, 21 | ) 22 | for departures, arrivals in split( 23 | self.departure_searches, self.arrival_searches, window_size 24 | ) 25 | ] 26 | 27 | def merge(self, responses: List[FeatureCollection]) -> FeatureCollection: 28 | merged_features = flatten([response.features for response in responses]) 29 | return FeatureCollection(type="FeatureCollection", features=merged_features) 30 | -------------------------------------------------------------------------------- /traveltimepy/dto/requests/time_map_wkt.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from traveltimepy.dto.requests.request import TravelTimeRequest 3 | from traveltimepy.dto.requests.time_map import DepartureSearch, ArrivalSearch 4 | from traveltimepy.dto.responses.time_map_wkt import TimeMapWKTResponse 5 | from traveltimepy.itertools import split, flatten 6 | 7 | 8 | class TimeMapWKTRequest(TravelTimeRequest[TimeMapWKTResponse]): 9 | departure_searches: List[DepartureSearch] 10 | arrival_searches: List[ArrivalSearch] 11 | 12 | def split_searches(self, window_size: int) -> List[TravelTimeRequest]: 13 | return [ 14 | TimeMapWKTRequest(departure_searches=departures, arrival_searches=arrivals) 15 | for departures, arrivals in split( 16 | self.departure_searches, self.arrival_searches, window_size 17 | ) 18 | ] 19 | 20 | def merge(self, responses: List[TimeMapWKTResponse]) -> TimeMapWKTResponse: 21 | return TimeMapWKTResponse( 22 | results=sorted( 23 | flatten([response.results for response in responses]), 24 | key=lambda res: res.search_id, 25 | ) 26 | ) 27 | -------------------------------------------------------------------------------- /traveltimepy/dto/responses/__init__.py: -------------------------------------------------------------------------------- 1 | """Module with traveltime responses models""" 2 | -------------------------------------------------------------------------------- /traveltimepy/dto/responses/error.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | from pydantic.main import BaseModel 4 | 5 | 6 | class ResponseError(BaseModel): 7 | error_code: int 8 | description: str 9 | documentation_link: str 10 | additional_info: Dict[str, List[str]] 11 | -------------------------------------------------------------------------------- /traveltimepy/dto/responses/geohash.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from pydantic.main import BaseModel 4 | 5 | 6 | class Properties(BaseModel): 7 | min: Optional[int] = None 8 | max: Optional[int] = None 9 | mean: Optional[int] = None 10 | 11 | 12 | class Cell(BaseModel): 13 | id: str 14 | properties: Properties 15 | 16 | 17 | class GeohashResult(BaseModel): 18 | search_id: str 19 | cells: List[Cell] 20 | 21 | 22 | class GeohashResponse(BaseModel): 23 | results: List[GeohashResult] 24 | -------------------------------------------------------------------------------- /traveltimepy/dto/responses/h3.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from pydantic.main import BaseModel 4 | 5 | 6 | class Properties(BaseModel): 7 | min: Optional[int] = None 8 | max: Optional[int] = None 9 | mean: Optional[int] = None 10 | 11 | 12 | class Cell(BaseModel): 13 | id: str 14 | properties: Properties 15 | 16 | 17 | class H3Result(BaseModel): 18 | search_id: str 19 | cells: List[Cell] 20 | 21 | 22 | class H3Response(BaseModel): 23 | results: List[H3Result] 24 | -------------------------------------------------------------------------------- /traveltimepy/dto/responses/map_info.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List, Optional 3 | 4 | from pydantic.main import BaseModel 5 | 6 | 7 | class PublicTransport(BaseModel): 8 | date_start: datetime 9 | date_end: datetime 10 | 11 | 12 | class Features(BaseModel): 13 | fares: bool 14 | postcodes: bool 15 | public_transport: Optional[PublicTransport] = None 16 | 17 | 18 | class Map(BaseModel): 19 | name: str 20 | features: Features 21 | 22 | 23 | class MapInfoResponse(BaseModel): 24 | maps: List[Map] 25 | -------------------------------------------------------------------------------- /traveltimepy/dto/responses/postcodes.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class Property(BaseModel): 7 | travel_time: Optional[int] = None 8 | distance: Optional[int] = None 9 | 10 | 11 | class Postcode(BaseModel): 12 | code: str 13 | properties: List[Property] 14 | 15 | 16 | class PostcodesResult(BaseModel): 17 | search_id: str 18 | postcodes: List[Postcode] 19 | 20 | 21 | class PostcodesResponse(BaseModel): 22 | results: List[PostcodesResult] 23 | -------------------------------------------------------------------------------- /traveltimepy/dto/responses/routes.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from pydantic.main import BaseModel 4 | 5 | from traveltimepy.dto.common import Fares, Route 6 | 7 | 8 | class Property(BaseModel): 9 | travel_time: Optional[int] = None 10 | fares: Optional[Fares] = None 11 | distance: Optional[int] = None 12 | route: Optional[Route] = None 13 | 14 | 15 | class Location(BaseModel): 16 | id: str 17 | properties: List[Property] 18 | 19 | 20 | class RoutesResult(BaseModel): 21 | search_id: str 22 | locations: List[Location] 23 | unreachable: List[str] 24 | 25 | 26 | class RoutesResponse(BaseModel): 27 | results: List[RoutesResult] 28 | -------------------------------------------------------------------------------- /traveltimepy/dto/responses/supported_locations.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class SupportedLocation(BaseModel): 7 | id: str 8 | map_name: str 9 | additional_map_names: List[str] 10 | 11 | 12 | class SupportedLocationsResponse(BaseModel): 13 | locations: List[SupportedLocation] 14 | unsupported_locations: List[str] 15 | -------------------------------------------------------------------------------- /traveltimepy/dto/responses/time_filter.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from pydantic.main import BaseModel 4 | 5 | from traveltimepy.dto.common import Route, Fares 6 | 7 | 8 | class DistanceBreakdown(BaseModel): 9 | mode: str 10 | distance: int 11 | 12 | 13 | class Property(BaseModel): 14 | travel_time: int 15 | distance: Optional[int] = None 16 | distance_breakdown: Optional[List[DistanceBreakdown]] = None 17 | fares: Optional[Fares] = None 18 | route: Optional[Route] = None 19 | 20 | 21 | class Location(BaseModel): 22 | id: str 23 | properties: List[Property] 24 | 25 | 26 | class TimeFilterResult(BaseModel): 27 | search_id: str 28 | locations: List[Location] 29 | unreachable: List[str] 30 | 31 | 32 | class TimeFilterResponse(BaseModel): 33 | results: List[TimeFilterResult] 34 | -------------------------------------------------------------------------------- /traveltimepy/dto/responses/time_filter_fast.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from pydantic import BaseModel 3 | 4 | 5 | class Ticket(BaseModel): 6 | type: str 7 | price: float 8 | currency: str 9 | 10 | 11 | class Fares(BaseModel): 12 | tickets_total: List[Ticket] 13 | 14 | 15 | class Properties(BaseModel): 16 | travel_time: int 17 | distance: Optional[int] = None 18 | fares: Optional[Fares] = None 19 | 20 | 21 | class Location(BaseModel): 22 | id: str 23 | properties: Properties 24 | 25 | 26 | class TimeFilterFastResult(BaseModel): 27 | search_id: str 28 | locations: List[Location] 29 | unreachable: List[str] 30 | 31 | 32 | class TimeFilterFastResponse(BaseModel): 33 | results: List[TimeFilterFastResult] 34 | -------------------------------------------------------------------------------- /traveltimepy/dto/responses/time_filter_proto.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class TimeFilterProtoResponse(BaseModel): 7 | travel_times: List[int] 8 | distances: List[int] 9 | -------------------------------------------------------------------------------- /traveltimepy/dto/responses/time_map.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pydantic.main import BaseModel 4 | 5 | from traveltimepy.dto.common import Coordinates 6 | 7 | 8 | class Shape(BaseModel): 9 | shell: List[Coordinates] 10 | holes: List[List[Coordinates]] 11 | 12 | 13 | class TimeMapResult(BaseModel): 14 | search_id: str 15 | shapes: List[Shape] 16 | 17 | 18 | class TimeMapResponse(BaseModel): 19 | results: List[TimeMapResult] 20 | -------------------------------------------------------------------------------- /traveltimepy/dto/responses/time_map_wkt.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, List, Union, Dict, Any, TypeVar, Optional 2 | from pydantic import field_validator, BaseModel, Field 3 | 4 | from traveltimepy.wkt import WKTObject, parse_wkt 5 | from traveltimepy.wkt.helper import print_indented 6 | 7 | Props = TypeVar("Props", bound=Union[Dict[str, Any], BaseModel]) 8 | 9 | 10 | class TimeMapWKTResult(BaseModel, Generic[Props]): 11 | search_id: str 12 | shape: WKTObject 13 | properties: Optional[Props] = Field(None) 14 | 15 | @field_validator("shape", mode="before") 16 | @classmethod 17 | def transform_shape(cls, shape: str) -> WKTObject: 18 | return parse_wkt(shape) 19 | 20 | def pretty_print(self, indent_level=0): 21 | print_indented(f"SEARCH ID: {self.search_id}", indent_level) 22 | self.shape.pretty_print(indent_level) 23 | print_indented(f"PROPERTIES: {self.properties}", indent_level) 24 | 25 | 26 | class TimeMapWKTResponse(BaseModel): 27 | results: List[TimeMapWKTResult] 28 | 29 | def pretty_print(self, indent_level=0): 30 | print_indented("TIME-MAP WKT RESPONSE:", indent_level) 31 | for result in self.results: 32 | result.pretty_print(indent_level + 1) 33 | print() 34 | -------------------------------------------------------------------------------- /traveltimepy/dto/responses/zones.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class TravelTime(BaseModel): 7 | min: int 8 | max: int 9 | mean: int 10 | median: int 11 | 12 | 13 | class Properties(BaseModel): 14 | travel_time_reachable: Optional[TravelTime] = None 15 | travel_time_all: Optional[TravelTime] = None 16 | coverage: Optional[float] = None 17 | 18 | 19 | class Zone(BaseModel): 20 | code: str 21 | properties: Properties 22 | 23 | 24 | class PostcodesSectorsResult(BaseModel): 25 | search_id: str 26 | sectors: List[Zone] 27 | 28 | 29 | class PostcodesDistrictsResult(BaseModel): 30 | search_id: str 31 | districts: List[Zone] 32 | 33 | 34 | class PostcodesSectorsResponse(BaseModel): 35 | results: List[PostcodesSectorsResult] 36 | 37 | 38 | class PostcodesDistrictsResponse(BaseModel): 39 | results: List[PostcodesDistrictsResult] 40 | -------------------------------------------------------------------------------- /traveltimepy/dto/transportation.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from typing_extensions import Literal 3 | 4 | from pydantic.main import BaseModel 5 | from pydantic import model_validator 6 | 7 | from traveltimepy.dto.common import DrivingTrafficModel 8 | 9 | 10 | class MaxChanges(BaseModel): 11 | enabled: bool 12 | limit: int 13 | 14 | 15 | class Driving(BaseModel): 16 | type: Literal["driving"] = "driving" 17 | disable_border_crossing: Optional[bool] = None 18 | traffic_model: Optional[DrivingTrafficModel] = None 19 | 20 | 21 | class Walking(BaseModel): 22 | type: Literal["walking"] = "walking" 23 | 24 | 25 | class Cycling(BaseModel): 26 | type: Literal["cycling"] = "cycling" 27 | 28 | 29 | class Ferry(BaseModel): 30 | type: Literal["ferry", "cycling+ferry", "driving+ferry"] = "ferry" 31 | boarding_time: Optional[int] = None 32 | traffic_model: Optional[DrivingTrafficModel] = None 33 | 34 | @model_validator(mode="after") 35 | def check_traffic_model(self): 36 | if self.type != "driving+ferry" and self.traffic_model: 37 | raise ValueError( 38 | '"traffic_model" cannot be specified when type is not "driving+ferry"' 39 | ) 40 | return self 41 | 42 | 43 | class DrivingTrain(BaseModel): 44 | type: Literal["driving+train"] = "driving+train" 45 | pt_change_delay: Optional[int] = None 46 | driving_time_to_station: Optional[int] = None 47 | parking_time: Optional[int] = None 48 | walking_time: Optional[int] = None 49 | max_changes: Optional[MaxChanges] = None 50 | traffic_model: Optional[DrivingTrafficModel] = None 51 | 52 | 53 | class PublicTransport(BaseModel): 54 | type: Literal["public_transport", "train", "bus", "coach"] = "public_transport" 55 | pt_change_delay: Optional[int] = None 56 | walking_time: Optional[int] = None 57 | max_changes: Optional[MaxChanges] = None 58 | 59 | 60 | class CyclingPublicTransport(BaseModel): 61 | type: Literal["cycling+public_transport"] = "cycling+public_transport" 62 | walking_time: Optional[int] = None 63 | pt_change_delay: Optional[int] = None 64 | cycling_time_to_station: Optional[int] = None 65 | parking_time: Optional[int] = None 66 | boarding_time: Optional[int] = None 67 | max_changes: Optional[MaxChanges] = None 68 | -------------------------------------------------------------------------------- /traveltimepy/errors.py: -------------------------------------------------------------------------------- 1 | class ApiError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /traveltimepy/http.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | from dataclasses import dataclass 4 | from typing import TypeVar, Type, Dict, Optional 5 | 6 | from aiohttp import ClientSession, ClientResponse, TCPConnector, ClientTimeout 7 | from pydantic import BaseModel 8 | from traveltimepy.dto.requests.request import TravelTimeRequest 9 | 10 | from traveltimepy.dto.responses.error import ResponseError 11 | from traveltimepy.errors import ApiError 12 | from aiohttp_retry import RetryClient, ExponentialRetry 13 | from aiolimiter import AsyncLimiter 14 | 15 | T = TypeVar("T", bound=BaseModel) 16 | DEFAULT_SPLIT_SIZE = 10 17 | 18 | 19 | @dataclass 20 | class SdkParams: 21 | host: str 22 | proto_host: str 23 | limit_per_host: int 24 | rate_limit: int 25 | time_window: int 26 | retry_attempts: int 27 | timeout: int 28 | 29 | 30 | async def send_post_request_async( 31 | client: RetryClient, 32 | response_class: Type[T], 33 | url: str, 34 | headers: Dict[str, str], 35 | request: TravelTimeRequest, 36 | rate_limit: AsyncLimiter, 37 | ) -> T: 38 | async with rate_limit: 39 | async with client.post( 40 | url=url, headers=headers, data=request.model_dump_json() 41 | ) as resp: 42 | return await _process_response(response_class, resp) 43 | 44 | 45 | async def send_post_async( 46 | response_class: Type[T], 47 | path: str, 48 | headers: Dict[str, str], 49 | request: TravelTimeRequest, 50 | sdk_params: SdkParams, 51 | ) -> T: 52 | window_size = _window_size(sdk_params.rate_limit) 53 | async with ClientSession( 54 | timeout=ClientTimeout(total=sdk_params.timeout), 55 | connector=TCPConnector(ssl=False, limit_per_host=sdk_params.limit_per_host), 56 | ) as session: 57 | retry_options = ExponentialRetry(attempts=sdk_params.retry_attempts) 58 | async with RetryClient( 59 | client_session=session, retry_options=retry_options 60 | ) as client: 61 | rate_limit = AsyncLimiter( 62 | sdk_params.rate_limit // window_size, sdk_params.time_window 63 | ) 64 | tasks = [ 65 | send_post_request_async( 66 | client, 67 | response_class, 68 | f"https://{sdk_params.host}/v4/{path}", 69 | headers, 70 | part, 71 | rate_limit, 72 | ) 73 | for part in request.split_searches(window_size) 74 | ] 75 | responses = await asyncio.gather(*tasks) 76 | return request.merge(responses) 77 | 78 | 79 | def _window_size(rate_limit: int): 80 | if rate_limit >= DEFAULT_SPLIT_SIZE: 81 | return DEFAULT_SPLIT_SIZE 82 | else: 83 | return rate_limit 84 | 85 | 86 | async def send_get_async( 87 | response_class: Type[T], 88 | path: str, 89 | headers: Dict[str, str], 90 | sdk_params: SdkParams, 91 | params: Optional[Dict[str, str]], 92 | ) -> T: 93 | async with ClientSession( 94 | timeout=ClientTimeout(total=sdk_params.timeout), 95 | connector=TCPConnector(ssl=False), 96 | ) as session: 97 | retry_options = ExponentialRetry(attempts=sdk_params.retry_attempts) 98 | async with RetryClient( 99 | client_session=session, retry_options=retry_options 100 | ) as client: 101 | async with client.get( 102 | url=f"https://{sdk_params.host}/v4/{path}", 103 | headers=headers, 104 | params=params, 105 | ) as resp: 106 | return await _process_response(response_class, resp) 107 | 108 | 109 | async def _process_response(response_class: Type[T], response: ClientResponse) -> T: 110 | text = await response.text() 111 | json_data = json.loads(text) 112 | if response.status != 200: 113 | parsed = ResponseError.model_validate_json(json.dumps(json_data)) 114 | msg = ( 115 | f"Travel Time API request failed: {parsed.description}\n" 116 | f"Error code: {parsed.error_code}\n" 117 | f"Additional info: {parsed.additional_info}\n" 118 | f"<{parsed.documentation_link}>\n" 119 | ) 120 | raise ApiError(msg) 121 | else: 122 | return response_class.model_validate(json_data) 123 | -------------------------------------------------------------------------------- /traveltimepy/itertools.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from typing import List, TypeVar, Tuple, Optional 3 | 4 | T = TypeVar("T") 5 | R = TypeVar("R") 6 | 7 | 8 | def sliding(values: List[T], window_size: int) -> List[List[T]]: 9 | return [values[i : i + window_size] for i in range(0, len(values), window_size)] 10 | 11 | 12 | def split( 13 | left: List[T], right: List[R], window_size: int 14 | ) -> List[Tuple[List[T], List[R]]]: 15 | return list( 16 | itertools.zip_longest( 17 | sliding(left, window_size), sliding(right, window_size), fillvalue=[] 18 | ) 19 | ) 20 | 21 | 22 | def join_opt(values: Optional[List[str]], sep: str) -> Optional[str]: 23 | return sep.join(values) if values is not None and len(values) != 0 else None 24 | 25 | 26 | def flatten(list_of_lists: List[List[T]]) -> List[T]: 27 | return list(itertools.chain.from_iterable(list_of_lists)) 28 | -------------------------------------------------------------------------------- /traveltimepy/proto_http.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from aiohttp import ( 4 | ClientSession, 5 | ClientResponse, 6 | BasicAuth, 7 | TCPConnector, 8 | ClientTimeout, 9 | ) 10 | 11 | import TimeFilterFastResponse_pb2 # type: ignore 12 | import TimeFilterFastRequest_pb2 # type: ignore 13 | from traveltimepy.dto.responses.time_filter_proto import TimeFilterProtoResponse 14 | from traveltimepy.errors import ApiError 15 | 16 | 17 | async def send_proto_async( 18 | url: str, 19 | headers: Dict[str, str], 20 | data: TimeFilterFastRequest_pb2.TimeFilterFastRequest, # type: ignore 21 | app_id: str, 22 | api_key: str, 23 | timeout: int, 24 | ) -> TimeFilterProtoResponse: 25 | async with ClientSession( 26 | timeout=ClientTimeout(total=timeout), connector=TCPConnector(ssl=False) 27 | ) as session: 28 | async with session.post( 29 | url=url, 30 | headers=headers, 31 | data=data.SerializeToString(), 32 | auth=BasicAuth(app_id, api_key), 33 | ) as resp: 34 | return await _process_response(resp) 35 | 36 | 37 | async def _process_response(response: ClientResponse) -> TimeFilterProtoResponse: 38 | content = await response.read() 39 | if response.status != 200: 40 | error_code = response.headers.get("X-ERROR-CODE", "Unknown") 41 | error_details = response.headers.get("X-ERROR-DETAILS", "No details provided") 42 | error_message = response.headers.get("X-ERROR-MESSAGE", "No message provided") 43 | 44 | msg = ( 45 | f"Travel Time API proto request failed with error code: {response.status}\n" 46 | f"X-ERROR-CODE: {error_code}\n" 47 | f"X-ERROR-DETAILS: {error_details}\n" 48 | f"X-ERROR-MESSAGE: {error_message}" 49 | ) 50 | 51 | raise ApiError(msg) 52 | else: 53 | response_body = TimeFilterFastResponse_pb2.TimeFilterFastResponse() # type: ignore 54 | response_body.ParseFromString(content) 55 | return TimeFilterProtoResponse( 56 | travel_times=response_body.properties.travelTimes[:], 57 | distances=response_body.properties.distances[:], 58 | ) 59 | -------------------------------------------------------------------------------- /traveltimepy/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traveltime-dev/traveltime-python-sdk/e25cb67f9162d3f723a02a8d10162c96b85c5a47/traveltimepy/py.typed -------------------------------------------------------------------------------- /traveltimepy/sdk.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List, Optional, Dict, Union 3 | 4 | from traveltimepy.dto.common import ( 5 | CellProperty, 6 | GeohashCentroid, 7 | H3Centroid, 8 | Location, 9 | Coordinates, 10 | PolygonsFilter, 11 | Rectangle, 12 | Property, 13 | FullRange, 14 | Range, 15 | LevelOfDetail, 16 | PropertyProto, 17 | DepartureTime, 18 | ArrivalTime, 19 | RenderMode, 20 | Snapping, 21 | ) 22 | from traveltimepy.dto.requests import geohash_fast, h3_fast, time_map_fast 23 | from traveltimepy.dto.responses.geohash import GeohashResponse, GeohashResult 24 | from traveltimepy.dto.responses.h3 import H3Response, H3Result 25 | from traveltimepy.dto.responses.time_map_wkt import ( 26 | TimeMapWKTResponse, 27 | ) 28 | from traveltimepy.dto.responses.time_filter_proto import TimeFilterProtoResponse 29 | from traveltimepy.dto.transportation import ( 30 | PublicTransport, 31 | Driving, 32 | Ferry, 33 | Walking, 34 | Cycling, 35 | DrivingTrain, 36 | CyclingPublicTransport, 37 | ) 38 | from traveltimepy.dto.requests.postcodes_zones import ZonesProperty 39 | from traveltimepy.dto.requests.time_filter_proto import ( 40 | DrivingAndPublicTransportWithDetails, 41 | ProtoCountry, 42 | ProtoTransportation, 43 | PublicTransportWithDetails, 44 | ) 45 | from traveltimepy.dto.requests.time_filter_fast import Transportation 46 | from traveltimepy.errors import ApiError 47 | 48 | from traveltimepy import __version__ 49 | from traveltimepy.accept_type import AcceptType 50 | from traveltimepy.itertools import join_opt 51 | from traveltimepy.dto.requests.supported_locations import SupportedLocationsRequest 52 | 53 | from traveltimepy.dto.responses.map_info import MapInfoResponse, Map 54 | from traveltimepy.dto.responses.postcodes import PostcodesResponse, PostcodesResult 55 | from traveltimepy.dto.responses.routes import RoutesResponse, RoutesResult 56 | from traveltimepy.dto.responses.supported_locations import SupportedLocationsResponse 57 | from traveltimepy.dto.responses.time_filter import TimeFilterResponse, TimeFilterResult 58 | from traveltimepy.dto.responses.time_filter_fast import ( 59 | TimeFilterFastResponse, 60 | TimeFilterFastResult, 61 | ) 62 | from traveltimepy.dto.responses.time_map import TimeMapResponse, TimeMapResult 63 | from traveltimepy.dto.responses.zones import ( 64 | PostcodesDistrictsResponse, 65 | PostcodesSectorsResponse, 66 | PostcodesDistrictsResult, 67 | PostcodesSectorsResult, 68 | ) 69 | 70 | from traveltimepy.mapper import ( 71 | create_distance_map, 72 | create_geohash, 73 | create_geohash_fast, 74 | create_geohash_intersection, 75 | create_geohash_union, 76 | create_h3, 77 | create_h3_fast, 78 | create_h3_intersection, 79 | create_h3_union, 80 | create_time_filter, 81 | create_time_filter_fast, 82 | create_postcodes, 83 | create_districts, 84 | create_sectors, 85 | create_routes, 86 | create_proto_request, 87 | create_time_map, 88 | create_time_map_intersection, 89 | create_time_map_fast, 90 | create_time_map_fast_geojson, 91 | create_time_map_fast_wkt, 92 | create_time_map_union, 93 | create_time_map_geojson, 94 | create_time_map_wkt, 95 | ) 96 | 97 | from traveltimepy.proto_http import send_proto_async 98 | from traveltimepy.http import ( 99 | send_get_async, 100 | send_post_async, 101 | SdkParams, 102 | ) 103 | 104 | from geojson_pydantic import FeatureCollection 105 | 106 | 107 | class TravelTimeSdk: 108 | def __init__( 109 | self, 110 | app_id: str, 111 | api_key: str, 112 | limit_per_host: int = 2, 113 | rate_limit: int = 60, 114 | time_window: int = 60, 115 | retry_attempts: int = 2, 116 | host: str = "api.traveltimeapp.com", 117 | proto_host: str = "proto.api.traveltimeapp.com", 118 | timeout: int = 300, 119 | user_agent: Optional[str] = None, 120 | ) -> None: 121 | self._app_id = app_id 122 | self._api_key = api_key 123 | 124 | if user_agent is not None: 125 | self._user_agent = user_agent 126 | else: 127 | self._user_agent = f"Travel Time Python SDK {__version__}" 128 | 129 | self._sdk_params = SdkParams( 130 | host, 131 | proto_host, 132 | limit_per_host, 133 | rate_limit, 134 | time_window, 135 | retry_attempts, 136 | timeout, 137 | ) 138 | 139 | async def time_filter_async( 140 | self, 141 | locations: List[Location], 142 | search_ids: Dict[str, List[str]], 143 | transportation: Union[ 144 | PublicTransport, 145 | Driving, 146 | Ferry, 147 | Walking, 148 | Cycling, 149 | DrivingTrain, 150 | CyclingPublicTransport, 151 | ], 152 | properties: Optional[List[Property]] = None, 153 | departure_time: Optional[datetime] = None, 154 | arrival_time: Optional[datetime] = None, 155 | travel_time: int = 3600, 156 | range: Optional[FullRange] = None, 157 | snapping: Optional[Snapping] = None, 158 | v4_endpoint_path: Optional[str] = None, 159 | ) -> List[TimeFilterResult]: 160 | time_info = get_time_info(departure_time, arrival_time) 161 | 162 | resp = await send_post_async( 163 | TimeFilterResponse, 164 | v4_endpoint_path or "time-filter", 165 | self._headers(AcceptType.JSON), 166 | create_time_filter( 167 | locations, 168 | search_ids, 169 | transportation, 170 | properties, 171 | time_info, 172 | travel_time, 173 | range, 174 | snapping, 175 | ), 176 | self._sdk_params, 177 | ) 178 | 179 | return resp.results 180 | 181 | async def map_info_async( 182 | self, 183 | v4_endpoint_path: Optional[str] = None, 184 | ) -> List[Map]: 185 | res = await send_get_async( 186 | MapInfoResponse, 187 | v4_endpoint_path or "map-info", 188 | self._headers(AcceptType.JSON), 189 | self._sdk_params, 190 | None, 191 | ) 192 | return res.maps 193 | 194 | async def geocoding_async( 195 | self, 196 | query: str, 197 | limit: Optional[int] = None, 198 | within_countries: Optional[List[str]] = None, 199 | format_name: Optional[bool] = None, 200 | format_exclude_country: Optional[bool] = None, 201 | bounds: Optional[Rectangle] = None, 202 | v4_endpoint_path: Optional[str] = None, 203 | ) -> FeatureCollection: 204 | return await send_get_async( 205 | FeatureCollection, 206 | v4_endpoint_path or "geocoding/search", 207 | self._headers(AcceptType.JSON), 208 | self._sdk_params, 209 | self._geocoding_params( 210 | query, 211 | limit, 212 | within_countries, 213 | format_name, 214 | format_exclude_country, 215 | bounds, 216 | ), 217 | ) 218 | 219 | async def geocoding_reverse_async( 220 | self, 221 | lat: float, 222 | lng: float, 223 | v4_endpoint_path: Optional[str] = None, 224 | ) -> FeatureCollection: 225 | return await send_get_async( 226 | FeatureCollection, 227 | v4_endpoint_path or "geocoding/reverse", 228 | self._headers(AcceptType.JSON), 229 | self._sdk_params, 230 | self._geocoding_reverse_params(lat, lng), 231 | ) 232 | 233 | async def supported_locations_async( 234 | self, 235 | locations: List[Location], 236 | v4_endpoint_path: Optional[str] = None, 237 | ) -> SupportedLocationsResponse: 238 | return await send_post_async( 239 | SupportedLocationsResponse, 240 | v4_endpoint_path or "supported-locations", 241 | self._headers(AcceptType.JSON), 242 | SupportedLocationsRequest(locations=locations), 243 | self._sdk_params, 244 | ) 245 | 246 | async def time_map_fast_async( 247 | self, 248 | coordinates: List[Coordinates], 249 | transportation: time_map_fast.Transportation, 250 | travel_time: int = 3600, 251 | one_to_many: bool = True, 252 | level_of_detail: Optional[LevelOfDetail] = None, 253 | snapping: Optional[Snapping] = None, 254 | polygons_filter: Optional[PolygonsFilter] = None, 255 | render_mode: Optional[RenderMode] = None, 256 | v4_endpoint_path: Optional[str] = None, 257 | ) -> List[TimeMapResult]: 258 | resp = await send_post_async( 259 | TimeMapResponse, 260 | v4_endpoint_path or "time-map/fast", 261 | self._headers(AcceptType.JSON), 262 | create_time_map_fast( 263 | coordinates, 264 | transportation, 265 | travel_time, 266 | level_of_detail, 267 | snapping, 268 | polygons_filter, 269 | render_mode, 270 | one_to_many, 271 | ), 272 | self._sdk_params, 273 | ) 274 | return resp.results 275 | 276 | async def time_map_fast_geojson_async( 277 | self, 278 | coordinates: List[Coordinates], 279 | transportation: time_map_fast.Transportation, 280 | travel_time: int = 3600, 281 | one_to_many: bool = True, 282 | level_of_detail: Optional[LevelOfDetail] = None, 283 | snapping: Optional[Snapping] = None, 284 | polygons_filter: Optional[PolygonsFilter] = None, 285 | render_mode: Optional[RenderMode] = None, 286 | v4_endpoint_path: Optional[str] = None, 287 | ) -> FeatureCollection: 288 | resp = await send_post_async( 289 | FeatureCollection, 290 | v4_endpoint_path or "time-map/fast", 291 | self._headers(AcceptType.GEO_JSON), 292 | create_time_map_fast_geojson( 293 | coordinates, 294 | transportation, 295 | travel_time, 296 | level_of_detail, 297 | snapping, 298 | polygons_filter, 299 | render_mode, 300 | one_to_many, 301 | ), 302 | self._sdk_params, 303 | ) 304 | return resp 305 | 306 | async def time_map_fast_wkt_async( 307 | self, 308 | coordinates: List[Coordinates], 309 | transportation: time_map_fast.Transportation, 310 | travel_time: int = 3600, 311 | one_to_many: bool = True, 312 | level_of_detail: Optional[LevelOfDetail] = None, 313 | snapping: Optional[Snapping] = None, 314 | polygons_filter: Optional[PolygonsFilter] = None, 315 | render_mode: Optional[RenderMode] = None, 316 | v4_endpoint_path: Optional[str] = None, 317 | ) -> TimeMapWKTResponse: 318 | resp = await send_post_async( 319 | TimeMapWKTResponse, 320 | v4_endpoint_path or "time-map/fast", 321 | self._headers(AcceptType.WKT), 322 | create_time_map_fast_wkt( 323 | coordinates, 324 | transportation, 325 | travel_time, 326 | level_of_detail, 327 | snapping, 328 | polygons_filter, 329 | render_mode, 330 | one_to_many, 331 | ), 332 | self._sdk_params, 333 | ) 334 | return resp 335 | 336 | async def time_map_fast_wkt_no_holes_async( 337 | self, 338 | coordinates: List[Coordinates], 339 | transportation: time_map_fast.Transportation, 340 | travel_time: int = 3600, 341 | one_to_many: bool = True, 342 | level_of_detail: Optional[LevelOfDetail] = None, 343 | snapping: Optional[Snapping] = None, 344 | polygons_filter: Optional[PolygonsFilter] = None, 345 | render_mode: Optional[RenderMode] = None, 346 | v4_endpoint_path: Optional[str] = None, 347 | ) -> TimeMapWKTResponse: 348 | resp = await send_post_async( 349 | TimeMapWKTResponse, 350 | v4_endpoint_path or "time-map/fast", 351 | self._headers(AcceptType.WKT_NO_HOLES), 352 | create_time_map_fast_wkt( 353 | coordinates, 354 | transportation, 355 | travel_time, 356 | level_of_detail, 357 | snapping, 358 | polygons_filter, 359 | render_mode, 360 | one_to_many, 361 | ), 362 | self._sdk_params, 363 | ) 364 | return resp 365 | 366 | async def h3_fast_async( 367 | self, 368 | coordinates: List[Union[Coordinates, H3Centroid]], 369 | transportation: h3_fast.Transportation, 370 | properties: List[CellProperty], 371 | resolution: int, 372 | travel_time: int = 3600, 373 | one_to_many: bool = True, 374 | snapping: Optional[Snapping] = None, 375 | v4_endpoint_path: Optional[str] = None, 376 | ) -> List[H3Result]: 377 | resp = await send_post_async( 378 | H3Response, 379 | v4_endpoint_path or "h3/fast", 380 | self._headers(AcceptType.JSON), 381 | create_h3_fast( 382 | coordinates=coordinates, 383 | transportation=transportation, 384 | properties=properties, 385 | resolution=resolution, 386 | travel_time=travel_time, 387 | snapping=snapping, 388 | one_to_many=one_to_many, 389 | ), 390 | self._sdk_params, 391 | ) 392 | return resp.results 393 | 394 | async def geohash_fast_async( 395 | self, 396 | coordinates: List[Union[Coordinates, GeohashCentroid]], 397 | transportation: geohash_fast.Transportation, 398 | properties: List[CellProperty], 399 | resolution: int, 400 | travel_time: int = 3600, 401 | one_to_many: bool = True, 402 | snapping: Optional[Snapping] = None, 403 | v4_endpoint_path: Optional[str] = None, 404 | ) -> List[GeohashResult]: 405 | resp = await send_post_async( 406 | GeohashResponse, 407 | v4_endpoint_path or "geohash/fast", 408 | self._headers(AcceptType.JSON), 409 | create_geohash_fast( 410 | coordinates=coordinates, 411 | transportation=transportation, 412 | properties=properties, 413 | resolution=resolution, 414 | travel_time=travel_time, 415 | snapping=snapping, 416 | one_to_many=one_to_many, 417 | ), 418 | self._sdk_params, 419 | ) 420 | return resp.results 421 | 422 | async def time_filter_fast_async( 423 | self, 424 | locations: List[Location], 425 | search_ids: Dict[str, List[str]], 426 | transportation: Transportation, 427 | travel_time: int = 3600, 428 | properties: Optional[List[Property]] = None, 429 | one_to_many: bool = True, 430 | snapping: Optional[Snapping] = None, 431 | v4_endpoint_path: Optional[str] = None, 432 | ) -> List[TimeFilterFastResult]: 433 | resp = await send_post_async( 434 | TimeFilterFastResponse, 435 | v4_endpoint_path or "time-filter/fast", 436 | self._headers(AcceptType.JSON), 437 | create_time_filter_fast( 438 | locations, 439 | search_ids, 440 | transportation, 441 | travel_time, 442 | properties, 443 | one_to_many, 444 | snapping, 445 | ), 446 | self._sdk_params, 447 | ) 448 | return resp.results 449 | 450 | async def postcodes_async( 451 | self, 452 | coordinates: List[Coordinates], 453 | transportation: Union[ 454 | PublicTransport, 455 | Driving, 456 | Ferry, 457 | Walking, 458 | Cycling, 459 | DrivingTrain, 460 | CyclingPublicTransport, 461 | ], 462 | departure_time: Optional[datetime] = None, 463 | arrival_time: Optional[datetime] = None, 464 | travel_time: int = 1800, 465 | properties: Optional[List[Property]] = None, 466 | range: Optional[FullRange] = None, 467 | v4_endpoint_path: Optional[str] = None, 468 | ) -> List[PostcodesResult]: 469 | time_info = get_time_info(departure_time, arrival_time) 470 | 471 | resp = await send_post_async( 472 | PostcodesResponse, 473 | v4_endpoint_path or "time-filter/postcodes", 474 | self._headers(AcceptType.JSON), 475 | create_postcodes( 476 | coordinates, 477 | time_info, 478 | transportation, 479 | travel_time, 480 | properties, 481 | range, 482 | ), 483 | self._sdk_params, 484 | ) 485 | return resp.results 486 | 487 | async def postcodes_districts_async( 488 | self, 489 | coordinates: List[Coordinates], 490 | transportation: Union[ 491 | PublicTransport, 492 | Driving, 493 | Ferry, 494 | Walking, 495 | Cycling, 496 | DrivingTrain, 497 | CyclingPublicTransport, 498 | ], 499 | travel_time: int = 1800, 500 | departure_time: Optional[datetime] = None, 501 | arrival_time: Optional[datetime] = None, 502 | reachable_postcodes_threshold=0.1, 503 | properties: Optional[List[ZonesProperty]] = None, 504 | range: Optional[FullRange] = None, 505 | v4_endpoint_path: Optional[str] = None, 506 | ) -> List[PostcodesDistrictsResult]: 507 | time_info = get_time_info(departure_time, arrival_time) 508 | 509 | res = await send_post_async( 510 | PostcodesDistrictsResponse, 511 | v4_endpoint_path or "time-filter/postcode-districts", 512 | self._headers(AcceptType.JSON), 513 | create_districts( 514 | coordinates, 515 | transportation, 516 | travel_time, 517 | time_info, 518 | reachable_postcodes_threshold, 519 | properties, 520 | range, 521 | ), 522 | self._sdk_params, 523 | ) 524 | return res.results 525 | 526 | async def postcodes_sectors_async( 527 | self, 528 | coordinates: List[Coordinates], 529 | transportation: Union[ 530 | PublicTransport, 531 | Driving, 532 | Ferry, 533 | Walking, 534 | Cycling, 535 | DrivingTrain, 536 | CyclingPublicTransport, 537 | ], 538 | travel_time: int = 1800, 539 | departure_time: Optional[datetime] = None, 540 | arrival_time: Optional[datetime] = None, 541 | reachable_postcodes_threshold=0.1, 542 | properties: Optional[List[ZonesProperty]] = None, 543 | range: Optional[FullRange] = None, 544 | v4_endpoint_path: Optional[str] = None, 545 | ) -> List[PostcodesSectorsResult]: 546 | time_info = get_time_info(departure_time, arrival_time) 547 | 548 | resp = await send_post_async( 549 | PostcodesSectorsResponse, 550 | v4_endpoint_path or "time-filter/postcode-sectors", 551 | self._headers(AcceptType.JSON), 552 | create_sectors( 553 | coordinates, 554 | transportation, 555 | travel_time, 556 | time_info, 557 | reachable_postcodes_threshold, 558 | properties, 559 | range, 560 | ), 561 | self._sdk_params, 562 | ) 563 | return resp.results 564 | 565 | async def routes_async( 566 | self, 567 | locations: List[Location], 568 | search_ids: Dict[str, List[str]], 569 | transportation: Union[ 570 | PublicTransport, 571 | Driving, 572 | Ferry, 573 | Walking, 574 | Cycling, 575 | DrivingTrain, 576 | CyclingPublicTransport, 577 | ], 578 | departure_time: Optional[datetime] = None, 579 | arrival_time: Optional[datetime] = None, 580 | properties: Optional[List[Property]] = None, 581 | range: Optional[FullRange] = None, 582 | snapping: Optional[Snapping] = None, 583 | v4_endpoint_path: Optional[str] = None, 584 | ) -> List[RoutesResult]: 585 | time_info = get_time_info(departure_time, arrival_time) 586 | 587 | resp = await send_post_async( 588 | RoutesResponse, 589 | v4_endpoint_path or "routes", 590 | self._headers(AcceptType.JSON), 591 | create_routes( 592 | locations, 593 | search_ids, 594 | transportation, 595 | time_info, 596 | properties, 597 | range, 598 | snapping, 599 | ), 600 | self._sdk_params, 601 | ) 602 | return resp.results 603 | 604 | async def time_filter_proto_async( 605 | self, 606 | origin: Coordinates, 607 | destinations: List[Coordinates], 608 | country: ProtoCountry, 609 | transportation: Union[ 610 | ProtoTransportation, 611 | PublicTransportWithDetails, 612 | DrivingAndPublicTransportWithDetails, 613 | ], 614 | travel_time: int, 615 | one_to_many: bool = True, 616 | properties: Optional[List[PropertyProto]] = None, 617 | ) -> TimeFilterProtoResponse: 618 | if isinstance(transportation, ProtoTransportation): 619 | transportationMode = transportation.value.name 620 | else: 621 | transportationMode = transportation.TYPE.value.name 622 | 623 | resp = await send_proto_async( 624 | f"https://{self._sdk_params.proto_host}/api/v2/{country.value}/time-filter/fast/{transportationMode}", 625 | # noqa 626 | self._proto_headers(), 627 | create_proto_request( 628 | origin, 629 | destinations, 630 | transportation, 631 | properties, 632 | travel_time, 633 | one_to_many, 634 | ), 635 | self._app_id, 636 | self._api_key, 637 | self._sdk_params.timeout, 638 | ) 639 | return resp 640 | 641 | async def time_map_intersection_async( 642 | self, 643 | coordinates: List[Coordinates], 644 | transportation: Union[ 645 | PublicTransport, 646 | Driving, 647 | Ferry, 648 | Walking, 649 | Cycling, 650 | DrivingTrain, 651 | CyclingPublicTransport, 652 | ], 653 | departure_time: Optional[datetime] = None, 654 | arrival_time: Optional[datetime] = None, 655 | travel_time: int = 3600, 656 | search_range: Optional[Range] = None, 657 | level_of_detail: Optional[LevelOfDetail] = None, 658 | snapping: Optional[Snapping] = None, 659 | polygons_filter: Optional[PolygonsFilter] = None, 660 | remove_water_bodies: Optional[bool] = None, 661 | render_mode: Optional[RenderMode] = None, 662 | v4_endpoint_path: Optional[str] = None, 663 | ) -> TimeMapResult: 664 | time_info = get_time_info(departure_time, arrival_time) 665 | 666 | resp = await send_post_async( 667 | TimeMapResponse, 668 | v4_endpoint_path or "time-map", 669 | self._headers(AcceptType.JSON), 670 | create_time_map_intersection( 671 | coordinates, 672 | transportation, 673 | travel_time, 674 | time_info, 675 | search_range, 676 | level_of_detail, 677 | snapping, 678 | polygons_filter, 679 | remove_water_bodies, 680 | render_mode, 681 | ), 682 | self._sdk_params, 683 | ) 684 | return resp.results[0] 685 | 686 | # intersection_async was renamed to time_map_intersection_async. Keeping this for legacy users 687 | async def intersection_async( 688 | self, 689 | coordinates: List[Coordinates], 690 | transportation: Union[ 691 | PublicTransport, 692 | Driving, 693 | Ferry, 694 | Walking, 695 | Cycling, 696 | DrivingTrain, 697 | CyclingPublicTransport, 698 | ], 699 | departure_time: Optional[datetime] = None, 700 | arrival_time: Optional[datetime] = None, 701 | travel_time: int = 3600, 702 | search_range: Optional[Range] = None, 703 | level_of_detail: Optional[LevelOfDetail] = None, 704 | snapping: Optional[Snapping] = None, 705 | polygons_filter: Optional[PolygonsFilter] = None, 706 | remove_water_bodies: Optional[bool] = None, 707 | render_mode: Optional[RenderMode] = None, 708 | ) -> TimeMapResult: 709 | resp = await self.time_map_intersection_async( 710 | coordinates=coordinates, 711 | transportation=transportation, 712 | departure_time=departure_time, 713 | arrival_time=arrival_time, 714 | travel_time=travel_time, 715 | search_range=search_range, 716 | level_of_detail=level_of_detail, 717 | snapping=snapping, 718 | polygons_filter=polygons_filter, 719 | remove_water_bodies=remove_water_bodies, 720 | render_mode=render_mode, 721 | ) 722 | return resp 723 | 724 | async def time_map_union_async( 725 | self, 726 | coordinates: List[Coordinates], 727 | transportation: Union[ 728 | PublicTransport, 729 | Driving, 730 | Ferry, 731 | Walking, 732 | Cycling, 733 | DrivingTrain, 734 | CyclingPublicTransport, 735 | ], 736 | departure_time: Optional[datetime] = None, 737 | arrival_time: Optional[datetime] = None, 738 | travel_time: int = 3600, 739 | search_range: Optional[Range] = None, 740 | level_of_detail: Optional[LevelOfDetail] = None, 741 | snapping: Optional[Snapping] = None, 742 | polygons_filter: Optional[PolygonsFilter] = None, 743 | remove_water_bodies: Optional[bool] = None, 744 | render_mode: Optional[RenderMode] = None, 745 | v4_endpoint_path: Optional[str] = None, 746 | ) -> TimeMapResult: 747 | time_info = get_time_info(departure_time, arrival_time) 748 | 749 | resp = await send_post_async( 750 | TimeMapResponse, 751 | v4_endpoint_path or "time-map", 752 | self._headers(AcceptType.JSON), 753 | create_time_map_union( 754 | coordinates, 755 | transportation, 756 | travel_time, 757 | time_info, 758 | search_range, 759 | level_of_detail, 760 | snapping, 761 | polygons_filter, 762 | remove_water_bodies, 763 | render_mode, 764 | ), 765 | self._sdk_params, 766 | ) 767 | 768 | return resp.results[0] 769 | 770 | # union_async was renamed to time_map_union_async. Keeping this for legacy users 771 | async def union_async( 772 | self, 773 | coordinates: List[Coordinates], 774 | transportation: Union[ 775 | PublicTransport, 776 | Driving, 777 | Ferry, 778 | Walking, 779 | Cycling, 780 | DrivingTrain, 781 | CyclingPublicTransport, 782 | ], 783 | departure_time: Optional[datetime] = None, 784 | arrival_time: Optional[datetime] = None, 785 | travel_time: int = 3600, 786 | search_range: Optional[Range] = None, 787 | level_of_detail: Optional[LevelOfDetail] = None, 788 | snapping: Optional[Snapping] = None, 789 | polygons_filter: Optional[PolygonsFilter] = None, 790 | remove_water_bodies: Optional[bool] = None, 791 | render_mode: Optional[RenderMode] = None, 792 | ) -> TimeMapResult: 793 | resp = await self.time_map_union_async( 794 | coordinates=coordinates, 795 | transportation=transportation, 796 | departure_time=departure_time, 797 | arrival_time=arrival_time, 798 | travel_time=travel_time, 799 | search_range=search_range, 800 | level_of_detail=level_of_detail, 801 | snapping=snapping, 802 | polygons_filter=polygons_filter, 803 | remove_water_bodies=remove_water_bodies, 804 | render_mode=render_mode, 805 | ) 806 | return resp 807 | 808 | async def time_map_async( 809 | self, 810 | coordinates: List[Coordinates], 811 | transportation: Union[ 812 | PublicTransport, 813 | Driving, 814 | Ferry, 815 | Walking, 816 | Cycling, 817 | DrivingTrain, 818 | CyclingPublicTransport, 819 | ], 820 | departure_time: Optional[datetime] = None, 821 | arrival_time: Optional[datetime] = None, 822 | travel_time: int = 3600, 823 | search_range: Optional[Range] = None, 824 | level_of_detail: Optional[LevelOfDetail] = None, 825 | snapping: Optional[Snapping] = None, 826 | polygons_filter: Optional[PolygonsFilter] = None, 827 | remove_water_bodies: Optional[bool] = None, 828 | render_mode: Optional[RenderMode] = None, 829 | v4_endpoint_path: Optional[str] = None, 830 | ) -> List[TimeMapResult]: 831 | time_info = get_time_info(departure_time, arrival_time) 832 | 833 | resp = await send_post_async( 834 | TimeMapResponse, 835 | v4_endpoint_path or "time-map", 836 | self._headers(AcceptType.JSON), 837 | create_time_map( 838 | coordinates, 839 | transportation, 840 | travel_time, 841 | time_info, 842 | search_range, 843 | level_of_detail, 844 | snapping, 845 | polygons_filter, 846 | remove_water_bodies, 847 | render_mode, 848 | ), 849 | self._sdk_params, 850 | ) 851 | return resp.results 852 | 853 | async def time_map_geojson_async( 854 | self, 855 | coordinates: List[Coordinates], 856 | transportation: Union[ 857 | PublicTransport, 858 | Driving, 859 | Ferry, 860 | Walking, 861 | Cycling, 862 | DrivingTrain, 863 | CyclingPublicTransport, 864 | ], 865 | departure_time: Optional[datetime] = None, 866 | arrival_time: Optional[datetime] = None, 867 | travel_time: int = 3600, 868 | search_range: Optional[Range] = None, 869 | level_of_detail: Optional[LevelOfDetail] = None, 870 | snapping: Optional[Snapping] = None, 871 | polygons_filter: Optional[PolygonsFilter] = None, 872 | remove_water_bodies: Optional[bool] = None, 873 | render_mode: Optional[RenderMode] = None, 874 | v4_endpoint_path: Optional[str] = None, 875 | ) -> FeatureCollection: 876 | time_info = get_time_info(departure_time, arrival_time) 877 | 878 | resp = await send_post_async( 879 | FeatureCollection, 880 | v4_endpoint_path or "time-map", 881 | self._headers(AcceptType.GEO_JSON), 882 | create_time_map_geojson( 883 | coordinates, 884 | transportation, 885 | travel_time, 886 | time_info, 887 | search_range, 888 | level_of_detail, 889 | snapping, 890 | polygons_filter, 891 | remove_water_bodies, 892 | render_mode, 893 | ), 894 | self._sdk_params, 895 | ) 896 | return resp 897 | 898 | async def time_map_wkt_async( 899 | self, 900 | coordinates: List[Coordinates], 901 | transportation: Union[ 902 | PublicTransport, 903 | Driving, 904 | Ferry, 905 | Walking, 906 | Cycling, 907 | DrivingTrain, 908 | CyclingPublicTransport, 909 | ], 910 | departure_time: Optional[datetime] = None, 911 | arrival_time: Optional[datetime] = None, 912 | travel_time: int = 3600, 913 | search_range: Optional[Range] = None, 914 | level_of_detail: Optional[LevelOfDetail] = None, 915 | snapping: Optional[Snapping] = None, 916 | polygons_filter: Optional[PolygonsFilter] = None, 917 | remove_water_bodies: Optional[bool] = None, 918 | render_mode: Optional[RenderMode] = None, 919 | v4_endpoint_path: Optional[str] = None, 920 | ) -> TimeMapWKTResponse: 921 | time_info = get_time_info(departure_time, arrival_time) 922 | 923 | resp = await send_post_async( 924 | TimeMapWKTResponse, 925 | v4_endpoint_path or "time-map", 926 | self._headers(AcceptType.WKT), 927 | create_time_map_wkt( 928 | coordinates, 929 | transportation, 930 | travel_time, 931 | time_info, 932 | search_range, 933 | level_of_detail, 934 | snapping, 935 | polygons_filter, 936 | remove_water_bodies, 937 | render_mode, 938 | ), 939 | self._sdk_params, 940 | ) 941 | return resp 942 | 943 | async def time_map_wkt_no_holes_async( 944 | self, 945 | coordinates: List[Coordinates], 946 | transportation: Union[ 947 | PublicTransport, 948 | Driving, 949 | Ferry, 950 | Walking, 951 | Cycling, 952 | DrivingTrain, 953 | CyclingPublicTransport, 954 | ], 955 | departure_time: Optional[datetime] = None, 956 | arrival_time: Optional[datetime] = None, 957 | travel_time: int = 3600, 958 | search_range: Optional[Range] = None, 959 | level_of_detail: Optional[LevelOfDetail] = None, 960 | snapping: Optional[Snapping] = None, 961 | polygons_filter: Optional[PolygonsFilter] = None, 962 | remove_water_bodies: Optional[bool] = None, 963 | render_mode: Optional[RenderMode] = None, 964 | v4_endpoint_path: Optional[str] = None, 965 | ) -> TimeMapWKTResponse: 966 | time_info = get_time_info(departure_time, arrival_time) 967 | 968 | resp = await send_post_async( 969 | TimeMapWKTResponse, 970 | v4_endpoint_path or "time-map", 971 | self._headers(AcceptType.WKT_NO_HOLES), 972 | create_time_map_wkt( 973 | coordinates, 974 | transportation, 975 | travel_time, 976 | time_info, 977 | search_range, 978 | level_of_detail, 979 | snapping, 980 | polygons_filter, 981 | remove_water_bodies, 982 | render_mode, 983 | ), 984 | self._sdk_params, 985 | ) 986 | return resp 987 | 988 | async def h3_intersection_async( 989 | self, 990 | coordinates: List[Union[Coordinates, H3Centroid]], 991 | transportation: Union[ 992 | PublicTransport, 993 | Driving, 994 | Ferry, 995 | Walking, 996 | Cycling, 997 | DrivingTrain, 998 | CyclingPublicTransport, 999 | ], 1000 | resolution: int, 1001 | properties: List[CellProperty] = [], 1002 | departure_time: Optional[datetime] = None, 1003 | arrival_time: Optional[datetime] = None, 1004 | travel_time: int = 3600, 1005 | search_range: Optional[Range] = None, 1006 | snapping: Optional[Snapping] = None, 1007 | v4_endpoint_path: Optional[str] = None, 1008 | ) -> H3Result: 1009 | time_info = get_time_info(departure_time, arrival_time) 1010 | 1011 | resp = await send_post_async( 1012 | H3Response, 1013 | v4_endpoint_path or "h3", 1014 | self._headers(AcceptType.JSON), 1015 | create_h3_intersection( 1016 | coordinates=coordinates, 1017 | transportation=transportation, 1018 | resolution=resolution, 1019 | properties=properties, 1020 | travel_time=travel_time, 1021 | time_info=time_info, 1022 | search_range=search_range, 1023 | snapping=snapping, 1024 | ), 1025 | self._sdk_params, 1026 | ) 1027 | return resp.results[0] 1028 | 1029 | async def h3_union_async( 1030 | self, 1031 | coordinates: List[Union[Coordinates, H3Centroid]], 1032 | transportation: Union[ 1033 | PublicTransport, 1034 | Driving, 1035 | Ferry, 1036 | Walking, 1037 | Cycling, 1038 | DrivingTrain, 1039 | CyclingPublicTransport, 1040 | ], 1041 | resolution: int, 1042 | properties: List[CellProperty] = [], 1043 | departure_time: Optional[datetime] = None, 1044 | arrival_time: Optional[datetime] = None, 1045 | travel_time: int = 3600, 1046 | search_range: Optional[Range] = None, 1047 | snapping: Optional[Snapping] = None, 1048 | v4_endpoint_path: Optional[str] = None, 1049 | ) -> H3Result: 1050 | time_info = get_time_info(departure_time, arrival_time) 1051 | 1052 | resp = await send_post_async( 1053 | H3Response, 1054 | v4_endpoint_path or "h3", 1055 | self._headers(AcceptType.JSON), 1056 | create_h3_union( 1057 | coordinates=coordinates, 1058 | transportation=transportation, 1059 | resolution=resolution, 1060 | properties=properties, 1061 | travel_time=travel_time, 1062 | time_info=time_info, 1063 | search_range=search_range, 1064 | snapping=snapping, 1065 | ), 1066 | self._sdk_params, 1067 | ) 1068 | 1069 | return resp.results[0] 1070 | 1071 | async def h3_async( 1072 | self, 1073 | coordinates: List[Union[Coordinates, H3Centroid]], 1074 | transportation: Union[ 1075 | PublicTransport, 1076 | Driving, 1077 | Ferry, 1078 | Walking, 1079 | Cycling, 1080 | DrivingTrain, 1081 | CyclingPublicTransport, 1082 | ], 1083 | resolution: int, 1084 | properties: List[CellProperty] = [], 1085 | departure_time: Optional[datetime] = None, 1086 | arrival_time: Optional[datetime] = None, 1087 | travel_time: int = 3600, 1088 | search_range: Optional[Range] = None, 1089 | snapping: Optional[Snapping] = None, 1090 | v4_endpoint_path: Optional[str] = None, 1091 | ) -> List[H3Result]: 1092 | time_info = get_time_info(departure_time, arrival_time) 1093 | 1094 | resp = await send_post_async( 1095 | H3Response, 1096 | v4_endpoint_path or "h3", 1097 | self._headers(AcceptType.JSON), 1098 | create_h3( 1099 | coordinates=coordinates, 1100 | transportation=transportation, 1101 | resolution=resolution, 1102 | properties=properties, 1103 | travel_time=travel_time, 1104 | time_info=time_info, 1105 | search_range=search_range, 1106 | snapping=snapping, 1107 | ), 1108 | self._sdk_params, 1109 | ) 1110 | return resp.results 1111 | 1112 | async def geohash_intersection_async( 1113 | self, 1114 | coordinates: List[Union[Coordinates, GeohashCentroid]], 1115 | transportation: Union[ 1116 | PublicTransport, 1117 | Driving, 1118 | Ferry, 1119 | Walking, 1120 | Cycling, 1121 | DrivingTrain, 1122 | CyclingPublicTransport, 1123 | ], 1124 | resolution: int, 1125 | properties: List[CellProperty] = [], 1126 | departure_time: Optional[datetime] = None, 1127 | arrival_time: Optional[datetime] = None, 1128 | travel_time: int = 3600, 1129 | search_range: Optional[Range] = None, 1130 | snapping: Optional[Snapping] = None, 1131 | v4_endpoint_path: Optional[str] = None, 1132 | ) -> GeohashResult: 1133 | time_info = get_time_info(departure_time, arrival_time) 1134 | 1135 | resp = await send_post_async( 1136 | GeohashResponse, 1137 | v4_endpoint_path or "geohash", 1138 | self._headers(AcceptType.JSON), 1139 | create_geohash_intersection( 1140 | coordinates=coordinates, 1141 | transportation=transportation, 1142 | resolution=resolution, 1143 | properties=properties, 1144 | travel_time=travel_time, 1145 | time_info=time_info, 1146 | search_range=search_range, 1147 | snapping=snapping, 1148 | ), 1149 | self._sdk_params, 1150 | ) 1151 | return resp.results[0] 1152 | 1153 | async def geohash_union_async( 1154 | self, 1155 | coordinates: List[Union[Coordinates, GeohashCentroid]], 1156 | transportation: Union[ 1157 | PublicTransport, 1158 | Driving, 1159 | Ferry, 1160 | Walking, 1161 | Cycling, 1162 | DrivingTrain, 1163 | CyclingPublicTransport, 1164 | ], 1165 | resolution: int, 1166 | properties: List[CellProperty] = [], 1167 | departure_time: Optional[datetime] = None, 1168 | arrival_time: Optional[datetime] = None, 1169 | travel_time: int = 3600, 1170 | search_range: Optional[Range] = None, 1171 | snapping: Optional[Snapping] = None, 1172 | v4_endpoint_path: Optional[str] = None, 1173 | ) -> GeohashResult: 1174 | time_info = get_time_info(departure_time, arrival_time) 1175 | 1176 | resp = await send_post_async( 1177 | GeohashResponse, 1178 | v4_endpoint_path or "geohash", 1179 | self._headers(AcceptType.JSON), 1180 | create_geohash_union( 1181 | coordinates=coordinates, 1182 | transportation=transportation, 1183 | resolution=resolution, 1184 | properties=properties, 1185 | travel_time=travel_time, 1186 | time_info=time_info, 1187 | search_range=search_range, 1188 | snapping=snapping, 1189 | ), 1190 | self._sdk_params, 1191 | ) 1192 | 1193 | return resp.results[0] 1194 | 1195 | async def geohash_async( 1196 | self, 1197 | coordinates: List[Union[Coordinates, GeohashCentroid]], 1198 | transportation: Union[ 1199 | PublicTransport, 1200 | Driving, 1201 | Ferry, 1202 | Walking, 1203 | Cycling, 1204 | DrivingTrain, 1205 | CyclingPublicTransport, 1206 | ], 1207 | resolution: int, 1208 | properties: List[CellProperty] = [], 1209 | departure_time: Optional[datetime] = None, 1210 | arrival_time: Optional[datetime] = None, 1211 | travel_time: int = 3600, 1212 | search_range: Optional[Range] = None, 1213 | snapping: Optional[Snapping] = None, 1214 | v4_endpoint_path: Optional[str] = None, 1215 | ) -> List[GeohashResult]: 1216 | time_info = get_time_info(departure_time, arrival_time) 1217 | 1218 | resp = await send_post_async( 1219 | GeohashResponse, 1220 | v4_endpoint_path or "geohash", 1221 | self._headers(AcceptType.JSON), 1222 | create_geohash( 1223 | coordinates=coordinates, 1224 | transportation=transportation, 1225 | resolution=resolution, 1226 | properties=properties, 1227 | travel_time=travel_time, 1228 | time_info=time_info, 1229 | search_range=search_range, 1230 | snapping=snapping, 1231 | ), 1232 | self._sdk_params, 1233 | ) 1234 | return resp.results 1235 | 1236 | async def distance_map_async( 1237 | self, 1238 | coordinates: List[Coordinates], 1239 | transportation: Union[ 1240 | PublicTransport, 1241 | Driving, 1242 | Ferry, 1243 | Walking, 1244 | Cycling, 1245 | DrivingTrain, 1246 | CyclingPublicTransport, 1247 | ], 1248 | departure_time: Optional[datetime] = None, 1249 | arrival_time: Optional[datetime] = None, 1250 | travel_distance: int = 900, 1251 | level_of_detail: Optional[LevelOfDetail] = None, 1252 | snapping: Optional[Snapping] = None, 1253 | polygons_filter: Optional[PolygonsFilter] = None, 1254 | no_holes: Optional[bool] = None, 1255 | v4_endpoint_path: Optional[str] = None, 1256 | ) -> List[TimeMapResult]: 1257 | time_info = get_time_info(departure_time, arrival_time) 1258 | resp = await send_post_async( 1259 | TimeMapResponse, 1260 | v4_endpoint_path or "distance-map", 1261 | self._headers(AcceptType.JSON), 1262 | create_distance_map( 1263 | coordinates, 1264 | transportation, 1265 | travel_distance, 1266 | time_info, 1267 | level_of_detail, 1268 | snapping, 1269 | polygons_filter, 1270 | no_holes, 1271 | ), 1272 | self._sdk_params, 1273 | ) 1274 | return resp.results 1275 | 1276 | @staticmethod 1277 | def _geocoding_reverse_params(lat: float, lng: float) -> Dict[str, str]: 1278 | full_query = {"lat": lat, "lng": lng} 1279 | return { 1280 | key: str(value) for (key, value) in full_query.items() if value is not None 1281 | } 1282 | 1283 | @staticmethod 1284 | def _geocoding_params( 1285 | query: str, 1286 | limit: Optional[int] = None, 1287 | within_countries: Optional[List[str]] = None, 1288 | format_name: Optional[bool] = None, 1289 | format_exclude_country: Optional[bool] = None, 1290 | bounds: Optional[Rectangle] = None, 1291 | ) -> Dict[str, str]: 1292 | full_query = { 1293 | "query": query, 1294 | "limit": limit, 1295 | "within.country": join_opt(within_countries, ","), 1296 | "format.name": format_name, 1297 | "format.exclude.country": format_exclude_country, 1298 | "bounds": bounds.to_str() if bounds is not None else bounds, 1299 | } 1300 | return { 1301 | key: str(value) for (key, value) in full_query.items() if value is not None 1302 | } 1303 | 1304 | @staticmethod 1305 | def _proto_headers() -> Dict[str, str]: 1306 | return { 1307 | "Content-Type": AcceptType.OCTET_STREAM.value, 1308 | "User-Agent": f"Travel Time Python SDK {__version__}", 1309 | } 1310 | 1311 | def _headers(self, accept_type: AcceptType) -> Dict[str, str]: 1312 | return { 1313 | "X-Application-Id": self._app_id, 1314 | "X-Api-Key": self._api_key, 1315 | "User-Agent": self._user_agent, 1316 | "Content-Type": "application/json", 1317 | "Accept": accept_type.value, 1318 | } 1319 | 1320 | 1321 | def get_time_info(departure_time: Optional[datetime], arrival_time: Optional[datetime]): 1322 | if not departure_time and not arrival_time: 1323 | raise ApiError("either arrival_time or departure_time has to be specified") 1324 | 1325 | if departure_time and arrival_time: 1326 | raise ApiError("arrival_time and departure_time cannot be both specified") 1327 | 1328 | if departure_time: 1329 | return DepartureTime(departure_time) 1330 | elif arrival_time: 1331 | return ArrivalTime(arrival_time) 1332 | -------------------------------------------------------------------------------- /traveltimepy/wkt/__init__.py: -------------------------------------------------------------------------------- 1 | """Module with WKT wrapper for shapely""" 2 | 3 | from traveltimepy.wkt.parsing import ( 4 | parse_wkt, 5 | ) 6 | 7 | from traveltimepy.wkt.geometries import ( 8 | WKTObject, 9 | PointModel, 10 | LineStringModel, 11 | PolygonModel, 12 | MultiLineStringModel, 13 | MultiPointModel, 14 | MultiPolygonModel, 15 | GeometryType, 16 | ) 17 | 18 | __all__ = [ 19 | "WKTObject", 20 | "PointModel", 21 | "LineStringModel", 22 | "PolygonModel", 23 | "MultiPointModel", 24 | "MultiLineStringModel", 25 | "MultiPolygonModel", 26 | "GeometryType", 27 | "parse_wkt", 28 | ] 29 | -------------------------------------------------------------------------------- /traveltimepy/wkt/constants.py: -------------------------------------------------------------------------------- 1 | from shapely.geometry import ( 2 | Point, 3 | LineString, 4 | Polygon, 5 | MultiPoint, 6 | MultiLineString, 7 | MultiPolygon, 8 | ) 9 | 10 | SUPPORTED_GEOMETRY_TYPES = [ 11 | Point, 12 | LineString, 13 | Polygon, 14 | MultiPoint, 15 | MultiLineString, 16 | MultiPolygon, 17 | ] 18 | -------------------------------------------------------------------------------- /traveltimepy/wkt/error.py: -------------------------------------------------------------------------------- 1 | class InvalidWKTStringError(Exception): 2 | def __init__(self, wkt_str): 3 | super().__init__(f"Invalid WKT string: {wkt_str}") 4 | 5 | 6 | class InvalidFunctionError(Exception): 7 | def __init__(self, geometry): 8 | super().__init__(f"No function found for geometry type: {type(geometry)}") 9 | 10 | 11 | class InvalidGeometryTypeError(Exception): 12 | def __init__(self, geometry): 13 | super().__init__(f"Invalid or unsupported geometry type: {type(geometry)}") 14 | 15 | 16 | class NullGeometryError(Exception): 17 | def __init__(self): 18 | super().__init__("Null, undefined or empty geometry returned") 19 | -------------------------------------------------------------------------------- /traveltimepy/wkt/geometries.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from enum import Enum 3 | from typing import List 4 | 5 | from pydantic import field_validator, BaseModel 6 | 7 | from traveltimepy import Coordinates 8 | from traveltimepy.wkt.helper import print_indented 9 | 10 | 11 | class GeometryType(Enum): 12 | POINT = "Point" 13 | LINESTRING = "LineString" 14 | POLYGON = "Polygon" 15 | MULTIPOINT = "MultiPoint" 16 | MULTILINESTRING = "MultiLineString" 17 | MULTIPOLYGON = "MultiPolygon" 18 | 19 | 20 | class WKTObject(BaseModel, ABC): 21 | type: GeometryType 22 | 23 | @abstractmethod 24 | def pretty_print(self, indent_level=0): 25 | raise NotImplementedError("Subclasses must implement this method.") 26 | 27 | 28 | class PointModel(WKTObject): 29 | coordinates: Coordinates 30 | 31 | def __init__(self, **data): 32 | super().__init__(type=GeometryType.POINT, **data) 33 | 34 | def pretty_print(self, indent_level=0): 35 | print_indented( 36 | f"POINT: {self.coordinates.lat}, {self.coordinates.lng}", indent_level 37 | ) 38 | 39 | 40 | class LineStringModel(WKTObject): 41 | coordinates: List[PointModel] 42 | 43 | def __init__(self, **data): 44 | super().__init__(type=GeometryType.LINESTRING, **data) 45 | 46 | @field_validator("coordinates") 47 | @classmethod 48 | def check_minimum_coordinates(cls, coords): 49 | if len(coords) < 2: 50 | raise ValueError("LineString must have at least 2 coordinates.") 51 | return coords 52 | 53 | def pretty_print(self, indent_level=0): 54 | print_indented("LINE STRING:", indent_level) 55 | for point in self.coordinates: 56 | point.pretty_print(indent_level + 1) 57 | 58 | 59 | class PolygonModel(WKTObject): 60 | exterior: LineStringModel 61 | interiors: List[LineStringModel] 62 | 63 | def __init__(self, **data): 64 | super().__init__(type=GeometryType.POLYGON, **data) 65 | 66 | def pretty_print(self, indent_level=0): 67 | print_indented("POLYGON:", indent_level) 68 | print_indented("EXTERIOR:", indent_level + 1) 69 | self.exterior.pretty_print(indent_level + 2) 70 | if self.interiors: 71 | print_indented("INTERIORS:", indent_level + 1) 72 | for interior in self.interiors: 73 | interior.pretty_print(indent_level + 2) 74 | 75 | 76 | class MultiPointModel(WKTObject): 77 | coordinates: List[PointModel] 78 | 79 | def __init__(self, **data): 80 | super().__init__(type=GeometryType.MULTIPOINT, **data) 81 | 82 | def pretty_print(self, indent_level=0): 83 | print_indented("MULTIPOINT:", indent_level) 84 | for point in self.coordinates: 85 | point.pretty_print(indent_level + 1) 86 | 87 | 88 | class MultiLineStringModel(WKTObject): 89 | coordinates: List[LineStringModel] 90 | 91 | def __init__(self, **data): 92 | super().__init__(type=GeometryType.MULTILINESTRING, **data) 93 | 94 | def pretty_print(self, indent_level=0): 95 | print_indented("MULTILINESTRING:", indent_level) 96 | for linestring in self.coordinates: 97 | linestring.pretty_print(indent_level + 1) 98 | 99 | 100 | class MultiPolygonModel(WKTObject): 101 | coordinates: List[PolygonModel] 102 | 103 | def __init__(self, **data): 104 | super().__init__(type=GeometryType.MULTIPOLYGON, **data) 105 | 106 | def pretty_print(self, indent_level=0): 107 | print_indented("MULTIPOLYGON:", indent_level) 108 | for polygon in self.coordinates: 109 | polygon.pretty_print(indent_level + 1) 110 | -------------------------------------------------------------------------------- /traveltimepy/wkt/helper.py: -------------------------------------------------------------------------------- 1 | def print_indented(text, indent_level=0): 2 | indentation = "\t" * indent_level 3 | print(f"{indentation}{text}") 4 | -------------------------------------------------------------------------------- /traveltimepy/wkt/parsing.py: -------------------------------------------------------------------------------- 1 | from functools import singledispatch 2 | from typing import Union 3 | 4 | from shapely import wkt, GEOSException 5 | from shapely.geometry import ( 6 | Point, 7 | LineString, 8 | Polygon, 9 | MultiPoint, 10 | MultiLineString, 11 | MultiPolygon, 12 | ) 13 | from shapely.geometry.base import BaseGeometry 14 | 15 | from traveltimepy import Coordinates 16 | from traveltimepy.wkt.geometries import ( 17 | PointModel, 18 | LineStringModel, 19 | PolygonModel, 20 | MultiPointModel, 21 | MultiLineStringModel, 22 | MultiPolygonModel, 23 | ) 24 | from traveltimepy.wkt.constants import ( 25 | SUPPORTED_GEOMETRY_TYPES, 26 | ) 27 | 28 | from traveltimepy.wkt.error import ( 29 | InvalidWKTStringError, 30 | NullGeometryError, 31 | InvalidFunctionError, 32 | InvalidGeometryTypeError, 33 | ) 34 | 35 | 36 | def _check_empty(geometry): 37 | if geometry.is_empty or geometry is None: 38 | raise NullGeometryError() 39 | 40 | 41 | @singledispatch 42 | def _parse_geometry(geometry: BaseGeometry): 43 | raise InvalidFunctionError(geometry) 44 | 45 | 46 | @_parse_geometry.register 47 | def _parse_point(geometry: Point) -> PointModel: 48 | return PointModel(coordinates=Coordinates(lat=geometry.y, lng=geometry.x)) 49 | 50 | 51 | @_parse_geometry.register 52 | def _parse_line_string(geometry: LineString) -> LineStringModel: 53 | point_models = [ 54 | PointModel(coordinates=Coordinates(lat=lat, lng=lng)) 55 | for lat, lng in geometry.coords 56 | ] 57 | return LineStringModel(coordinates=point_models) 58 | 59 | 60 | @_parse_geometry.register 61 | def _parse_polygon(geometry: Polygon) -> PolygonModel: 62 | exterior_points = [ 63 | PointModel(coordinates=Coordinates(lat=lat, lng=lng)) 64 | for lat, lng in geometry.exterior.coords 65 | ] 66 | interiors_points = [ 67 | [ 68 | PointModel(coordinates=Coordinates(lat=lat, lng=lng)) 69 | for lat, lng in interior.coords 70 | ] 71 | for interior in list(geometry.interiors) 72 | ] 73 | exterior_line = LineStringModel(coordinates=exterior_points) 74 | interior_lines = [ 75 | LineStringModel(coordinates=points) for points in interiors_points 76 | ] 77 | return PolygonModel(exterior=exterior_line, interiors=interior_lines) 78 | 79 | 80 | @_parse_geometry.register 81 | def _parse_multi_point(geometry: MultiPoint) -> MultiPointModel: 82 | point_models = [ 83 | PointModel(coordinates=Coordinates(lat=point.y, lng=point.x)) 84 | for point in geometry.geoms 85 | ] 86 | return MultiPointModel(coordinates=point_models) 87 | 88 | 89 | @_parse_geometry.register 90 | def _parse_multi_line_string(geometry: MultiLineString) -> MultiLineStringModel: 91 | line_strings = [ 92 | LineStringModel( 93 | coordinates=[ 94 | PointModel( 95 | coordinates=Coordinates(lat=point[0], lng=point[1]), 96 | ) 97 | for point in linestring.coords 98 | ], 99 | ) 100 | for linestring in geometry.geoms 101 | ] 102 | return MultiLineStringModel(coordinates=line_strings) 103 | 104 | 105 | @_parse_geometry.register 106 | def _parse_multi_polygon(geometry: MultiPolygon) -> MultiPolygonModel: 107 | polygons = [ 108 | PolygonModel( 109 | exterior=LineStringModel( 110 | coordinates=[ 111 | PointModel( 112 | coordinates=Coordinates(lat=point[0], lng=point[1]), 113 | ) 114 | for point in polygon.exterior.coords 115 | ], 116 | ), 117 | interiors=[ 118 | LineStringModel( 119 | coordinates=[ 120 | PointModel( 121 | coordinates=Coordinates(lat=point[0], lng=point[1]), 122 | ) 123 | for point in interior.coords 124 | ], 125 | ) 126 | for interior in list(polygon.interiors) 127 | ], 128 | ) 129 | for polygon in geometry.geoms 130 | ] 131 | return MultiPolygonModel(coordinates=polygons) 132 | 133 | 134 | def parse_wkt( 135 | wkt_str: str, 136 | ) -> Union[ 137 | PointModel, 138 | LineStringModel, 139 | PolygonModel, 140 | MultiPointModel, 141 | MultiLineStringModel, 142 | MultiPolygonModel, 143 | ]: 144 | try: 145 | geometry = wkt.loads(wkt_str) 146 | except GEOSException: 147 | raise InvalidWKTStringError(wkt_str) 148 | 149 | _check_empty(geometry) 150 | 151 | if type(geometry) not in SUPPORTED_GEOMETRY_TYPES: 152 | raise InvalidGeometryTypeError(geometry) 153 | 154 | return _parse_geometry(geometry) 155 | --------------------------------------------------------------------------------