├── .adr-dir ├── .codecov.yml ├── .darglint ├── .github └── workflows │ ├── binder.yml │ ├── main.yml │ ├── release.yml │ └── stale.yml ├── .gitignore ├── .isort.cfg ├── .ort.yml ├── .proclamation.json ├── .readthedocs.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── RELEASE.md ├── binder ├── environment.yml └── postBuild ├── changes ├── docs │ └── .gitignore ├── features │ └── .gitignore ├── misc │ └── .gitignore └── tests │ └── .gitignore ├── ci └── env │ └── environment.yml ├── docs ├── adr │ ├── 0001-record-architecture-decisions.md │ └── 0002-use-modern-project-tooling.md ├── notebooks │ ├── README.md │ ├── building_numbers.ipynb │ ├── country_names.ipynb │ ├── data │ │ ├── chicago_parks.geo.json │ │ ├── test.csv │ │ ├── test.geojson │ │ └── xyz_configuration.conf │ ├── demo.ipynb │ ├── interactive_map_layer_introduction.ipynb │ ├── spaces_class_example.ipynb │ ├── talk_pyml_geopython_2020.ipynb │ ├── visual_clipping.ipynb │ ├── xyz_pro_features_examples.ipynb │ └── xyzspaces_configurations.ipynb └── source │ ├── Makefile │ ├── blogs_talks.rst │ ├── changelog.rst │ ├── conf.py │ ├── contributing.rst │ ├── examples.rst │ ├── index.rst │ ├── install.rst │ ├── logging.rst │ ├── make.bat │ ├── prerequisites.rst │ ├── tests.rst │ ├── xyzspaces.__version__.rst │ ├── xyzspaces._compact.rst │ ├── xyzspaces.apis.rst │ ├── xyzspaces.auth.rst │ ├── xyzspaces.config.default.rst │ ├── xyzspaces.config.rst │ ├── xyzspaces.curl.rst │ ├── xyzspaces.datasets.rst │ ├── xyzspaces.exceptions.rst │ ├── xyzspaces.iml.apis.aaa_oauth2_api.rst │ ├── xyzspaces.iml.apis.api.rst │ ├── xyzspaces.iml.apis.data_config_api.rst │ ├── xyzspaces.iml.apis.data_interactive_api.rst │ ├── xyzspaces.iml.apis.lookup_api.rst │ ├── xyzspaces.iml.apis.rst │ ├── xyzspaces.iml.auth.rst │ ├── xyzspaces.iml.catalog.rst │ ├── xyzspaces.iml.credentials.rst │ ├── xyzspaces.iml.exceptions.rst │ ├── xyzspaces.iml.layer.rst │ ├── xyzspaces.iml.rst │ ├── xyzspaces.logconf.rst │ ├── xyzspaces.rst │ ├── xyzspaces.spaces.rst │ ├── xyzspaces.tools.rst │ └── xyzspaces.utils.rst ├── images ├── building_numbers.gif └── example_map.png ├── requirements.txt ├── requirements_dev.txt ├── scripts ├── README.md ├── build_docs.sh ├── check_xyz_platform.py └── walk_spaces.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── config │ ├── __init__.py │ └── test_config.py ├── curl │ ├── __init__.py │ └── test_curl.py ├── data │ ├── countries.geo.json │ ├── example.gpx │ ├── large_data.geojson │ ├── stations-32633.zip │ ├── stations.zip │ ├── test.csv │ ├── test.geojson │ ├── test.kml │ ├── test.pbf │ ├── test.wkt │ ├── test_altitude.csv │ └── test_altitude.geojson ├── hub │ ├── __init__.py │ ├── conftest.py │ ├── test_aliases.py │ ├── test_api.py │ ├── test_datasets.py │ ├── test_exceptions.py │ ├── test_logconf.py │ ├── test_misc.py │ ├── test_proxy.py │ └── test_search.py ├── iml │ ├── __init__.py │ ├── conftest.py │ ├── data │ │ └── dummy_credentials.properties │ ├── test_api.py │ ├── test_credentials.py │ ├── test_exceptions.py │ ├── test_iml.py │ ├── test_layer.py │ └── test_lookup_api.py ├── project │ ├── __init__.py │ ├── conftest.py │ └── test_projects.py ├── space │ ├── __init__.py │ ├── conftest.py │ └── test_space_objects.py ├── token │ ├── __init__.py │ ├── conftest.py │ ├── test_auth.py │ └── test_basic.py ├── tools │ ├── __init__.py │ └── test_tools.py └── utils │ ├── __init__.py │ └── test_utils.py └── xyzspaces ├── LICENSE ├── __init__.py ├── __version__.py ├── _compact.py ├── apis.py ├── auth.py ├── config ├── __init__.py ├── default.py └── logconfig.json ├── curl.py ├── datasets ├── LICENSE ├── __init__.py ├── chicago_parks.geo.json └── countries.geo.json ├── exceptions.py ├── iml ├── __init__.py ├── apis │ ├── __init__.py │ ├── aaa_oauth2_api.py │ ├── api.py │ ├── data_config_api.py │ ├── data_interactive_api.py │ └── lookup_api.py ├── auth.py ├── catalog.py ├── credentials.py ├── exceptions.py └── layer.py ├── logconf.py ├── spaces.py ├── tools.py └── utils.py /.adr-dir: -------------------------------------------------------------------------------- 1 | docs/adr 2 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto # auto compares coverage to the previous base commit 6 | threshold: 1% 7 | branches: 8 | - master 9 | comment: 10 | layout: "reach, diff, flags, files" # Remove items here to change the format -------------------------------------------------------------------------------- /.darglint: -------------------------------------------------------------------------------- 1 | [darglint] 2 | strictness=short 3 | docstring_style=sphinx 4 | ignore_regex=^test_(.*)|^_(.*) 5 | -------------------------------------------------------------------------------- /.github/workflows/binder.yml: -------------------------------------------------------------------------------- 1 | name: Binder build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | trigger-binder-build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: s-weigand/trigger-mybinder-build@v1 12 | with: 13 | target-repo: heremaps/xyz-spaces-python 14 | debug: true 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | run: 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ubuntu-latest] 19 | python-version: [3.9] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - name: Setup conda 25 | uses: conda-incubator/setup-miniconda@v2 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | activate-environment: test 29 | mamba-version: "*" 30 | channels: conda-forge 31 | 32 | - name: Conda install dependencies 33 | shell: bash -l {0} 34 | run: | 35 | conda env update --file ci/env/environment.yml --name test 36 | 37 | - name: Lint check 38 | shell: bash -l {0} 39 | run: | 40 | make clean 41 | make lint 42 | make typing 43 | 44 | - name: Test check 45 | shell: bash -l {0} 46 | env: 47 | XYZ_TOKEN: ${{ secrets.XYZ_TOKEN }} 48 | HERE_USER: ${{ secrets.HERE_USER }} 49 | HERE_PASSWORD: ${{ secrets.HERE_PASSWORD }} 50 | HERE_USER_ID: ${{ secrets.HERE_USER_ID }} 51 | HERE_CLIENT_ID: ${{ secrets.HERE_CLIENT_ID }} 52 | HERE_ACCESS_KEY_ID: ${{ secrets.HERE_ACCESS_KEY_ID }} 53 | HERE_ACCESS_KEY_SECRET: ${{ secrets.HERE_ACCESS_KEY_SECRET }} 54 | HERE_TOKEN_ENDPOINT_URL: ${{ secrets.HERE_TOKEN_ENDPOINT_URL }} 55 | run: | 56 | pytest -v --durations=10 --cov=xyzspaces tests --cov-report=xml 57 | 58 | - name: Upload coverage to Codecov 59 | if: github.ref == 'refs/heads/master' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.9' 60 | uses: codecov/codecov-action@v1 61 | with: 62 | file: ./coverage.xml 63 | flags: unittests 64 | verbose: true 65 | 66 | - name: Build Docs and Changelog (Only on Linux for saving time) 67 | if: matrix.os == 'ubuntu-latest' 68 | shell: bash -l {0} 69 | run: | 70 | make draft_changelog 71 | sh scripts/build_docs.sh 72 | 73 | - name: Install xyzspaces 74 | shell: bash -l {0} 75 | run: | 76 | pip install .[dev] 77 | python -c "import xyzspaces; print(xyzspaces.__version__)" 78 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up Python 3.8 12 | uses: actions/setup-python@v2 13 | with: 14 | python-version: 3.8 15 | - name: Install tools 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install setuptools wheel twine 19 | - name: Build the package 20 | run: | 21 | python setup.py sdist bdist_wheel 22 | - name: Publish package 23 | uses: pypa/gh-action-pypi-publish@master 24 | with: 25 | user: __token__ 26 | password: ${{ secrets.PYPI_TOKEN }} 27 | verbose: true -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "30 1 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v1 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'Stale issue message' 17 | stale-pr-message: 'Stale pull request message' 18 | stale-issue-label: 'no-issue-activity' 19 | stale-pr-label: 'no-pr-activity' 20 | days-before-close: -1 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .coverage 3 | __pycache__ 4 | .mypy_cache/ 5 | .DS_Store 6 | htmlcov/ 7 | .pytest_cache/ 8 | _build/ 9 | .ipynb_checkpoints/ 10 | .venv 11 | .vscode 12 | .debug.env 13 | xyz.log 14 | .coverage.* 15 | xyzspaces.egg-info/ 16 | build/ 17 | dist/ -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length=90 3 | multi_line_output=3 4 | include_trailing_comma=True 5 | force_grid_wrap=0 6 | use_parentheses=True 7 | -------------------------------------------------------------------------------- /.ort.yml: -------------------------------------------------------------------------------- 1 | --- 2 | excludes: 3 | paths: 4 | - pattern: "requirements_dev.txt" 5 | reason: "BUILD_TOOL_OF" 6 | comment: "Optional dependencies only used for development / testing." 7 | - pattern: "tests/**" 8 | reason: "TEST_OF" 9 | comment: "Directory only used for testing." 10 | -------------------------------------------------------------------------------- /.proclamation.json: -------------------------------------------------------------------------------- 1 | { 2 | "#": "This is a config file for Proclamation, the changelog combiner: https://gitlab.com/ryanpavlik/proclamation", 3 | "SPDX-License-Identifier: CC0-1.0": "", 4 | "SPDX-FileCopyrightText: 2020 Collabora, Ltd. and the Proclamation contributors": "", 5 | "$schema": "https://gitlab.com/ryanpavlik/proclamation/-/raw/master/proclamation.schema.json", 6 | "project_name": "xyzspaces", 7 | "base_url": "https://github.com/heremaps/xyz-spaces-python", 8 | "news_filename": "CHANGELOG.md", 9 | "sections": { 10 | "Features": { 11 | "directory": "changes/features" 12 | }, 13 | "Documentation": { 14 | "directory": "changes/docs" 15 | }, 16 | "Tests": { 17 | "directory": "changes/tests" 18 | }, 19 | "Misc": { 20 | "directory": "changes/misc" 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | builder: html 11 | configuration: docs/source/conf.py 12 | 13 | # Build documentation with MkDocs 14 | #mkdocs: 15 | # configuration: mkdocs.yml 16 | 17 | # Optionally build your docs in additional formats such as PDF 18 | formats: 19 | - pdf 20 | 21 | # Optionally set the version of Python and requirements required to build your docs 22 | python: 23 | version: 3.7 24 | install: 25 | - requirements: requirements.txt 26 | - requirements: requirements_dev.txt 27 | - method: pip 28 | path: . 29 | extra_requirements: 30 | - dev -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | Do not edit this file manually! It is updated from manually written text fragments 4 | in the [changes](https://github.com/heremaps/xyz-spaces-python/tree/master/changes) 5 | directory of this repository! 6 | 7 | ## xyzspaces 0.7.2 (2021-08-18) 8 | 9 | - Features 10 | - Made geopandas, turfpy, geobuf as optional dependencies of the xyzspaces 11 | 12 | 13 | ## xyzspaces 0.7.1 (2021-08-10) 14 | 15 | - Fixed minor issues 16 | 17 | ## xyzspaces 0.7.0 (2021-08-10) 18 | 19 | - Features 20 | - Added support for Interactive Map Layers. 21 | ([#117](https://github.com/heremaps/xyz-spaces-python/pull/117)) 22 | 23 | ## xyzspaces 0.6.0 (2021-06-17) 24 | 25 | - Features 26 | - Added support custom base URL of Data Hub APIs for self-hosted Data Hub. 27 | ([#113](https://github.com/heremaps/xyz-spaces-python/pull/113)) 28 | - Added support for launch datahub space invader 29 | ([commit](https://github.com/heremaps/xyz-spaces-python/commit/6863eb479355ce659a2cab3c8aa5442868c3070e)) 30 | - Fixed feature_id gettting skipped from GeoPandas data 31 | ([commit](https://github.com/heremaps/xyz-spaces-python/commit/52117691084c41e5aea27ae77a69a91148d178c3)) 32 | 33 | ## xyzspaces 0.5.0 (2021-02-01) 34 | 35 | - Features 36 | - Added functionality to `clone` Space 37 | ([#93](https://github.com/heremaps/xyz-spaces-python/pull/93)) 38 | - Handle HTTP 429 responses with `backoff` package. 39 | ([#95](https://github.com/heremaps/xyz-spaces-python/pull/95)) 40 | - support the new `force2D` parameter for all the APIs used to read features. 41 | ([#96](https://github.com/heremaps/xyz-spaces-python/pull/96)) 42 | - Add support for new mode and vizSampling params in HubApi.get_space_tile. 43 | ([#101](https://github.com/heremaps/xyz-spaces-python/pull/101)) 44 | - Documentation 45 | - Start collecting/documenting architecture decision records (ADRs). 46 | ([#90](https://github.com/heremaps/xyz-spaces-python/pull/90)) 47 | - Add support for executable code in Sphinx documentation. 48 | ([#99](https://github.com/heremaps/xyz-spaces-python/pull/99)) 49 | - Misc 50 | - Migrated CI from Travis to GitHub Actions. 51 | ([#111](https://github.com/heremaps/xyz-spaces-python/pull/111)) 52 | 53 | ## xyzspaces 0.4.0 (2020-09-18) 54 | 55 | - Features 56 | - Added feature to upload data from `kml` file to the space. 57 | ([#49](https://github.com/heremaps/xyz-spaces-python/pull/49)) 58 | - Added `limit` param to method `iter_feature` to control numer of features to 59 | iterate in single call. 60 | ([#52](https://github.com/heremaps/xyz-spaces-python/pull/52)) 61 | - Fixed encoding and projections issue for `shapefile` upload. 62 | ([#54](https://github.com/heremaps/xyz-spaces-python/pull/54)) 63 | - Enabled property search while searching features in bounding box. 64 | ([#56](https://github.com/heremaps/xyz-spaces-python/pull/56)) 65 | - Added feature to upload data from `geobuff` file to the space. 66 | ([#57](https://github.com/heremaps/xyz-spaces-python/pull/57)) 67 | - Remove duplicate features for `spatial_search_geometry` with division 68 | ([#58](https://github.com/heremaps/xyz-spaces-python/pull/58)) 69 | - Enabled property search while searching features in tile. 70 | ([#61](https://github.com/heremaps/xyz-spaces-python/pull/61)) 71 | - Remove duplicate features while add to space using `add_features` and also 72 | added a `mutation` parameter to mutate input features or not. 73 | ([#64](https://github.com/heremaps/xyz-spaces-python/pull/64)) 74 | - `description` is optional when creating the space. 75 | ([#68](https://github.com/heremaps/xyz-spaces-python/pull/68)) 76 | - Added feature to upload data from `Geopandas Dataframe` file to the space. 77 | ([#71](https://github.com/heremaps/xyz-spaces-python/pull/71)) 78 | - Enabled reading space data as Geopandas Dataframe. 79 | ([#72](https://github.com/heremaps/xyz-spaces-python/pull/72)) 80 | - Improved performance of CSV upload. 81 | ([#77](https://github.com/heremaps/xyz-spaces-python/pull/77)) 82 | - Improvement in the performance of `add_features_geojson` 83 | ([#79](https://github.com/heremaps/xyz-spaces-python/pull/79)) 84 | - Changes to convert shape file with projection of different type to EPSG:4326 85 | ([#83](https://github.com/heremaps/xyz-spaces-python/pull/83)) 86 | - Documentation 87 | - New notebook illustrating spatial search on MS US building footprints dataset. 88 | ([#62](https://github.com/heremaps/xyz-spaces-python/pull/62)) 89 | 90 | ## xyzspaces 0.3.2 (2020-08-19) 91 | 92 | - Features 93 | - Added feature to upload data from `shapefile` to the space. 94 | ([#40](https://github.com/heremaps/xyz-spaces-python/pull/40)) 95 | - Added feature to upload data from `WKT` file to the space. 96 | ([#41](https://github.com/heremaps/xyz-spaces-python/pull/41)) 97 | - Added feature to upload data from `gpx` file to the space. 98 | ([#42](https://github.com/heremaps/xyz-spaces-python/pull/42)) 99 | - Optimized the `spatial search` to search features from large geometries. 100 | ([#44](https://github.com/heremaps/xyz-spaces-python/pull/44)) 101 | - Misc 102 | - Added Binder support to the repository. 103 | ([#28](https://github.com/heremaps/xyz-spaces-python/pull/28)) 104 | - Added **clientId** in query params of the Hub API requests. 105 | ([#36](https://github.com/heremaps/xyz-spaces-python/pull/36)) 106 | - Updated `__version__` attribute now it can be used as `from xyzspaces import 107 | __version__`. 108 | ([#38](https://github.com/heremaps/xyz-spaces-python/pull/38)) 109 | 110 | ## xyzspaces 0.3.1 (2020-07-24) 111 | 112 | - Misc 113 | - Minor changes to README 114 | ([0.3.1](https://github.com/heremaps/xyz-spaces-python/releases/tag/0.3.1)) 115 | 116 | ## xyzspaces 0.3.0 (2020-07-24) 117 | 118 | - Misc 119 | - First public release 120 | ([0.3.0](https://github.com/heremaps/xyz-spaces-python/releases/tag/0.3)) 121 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to XYZ Spaces for Python 2 | 3 | Thank you for taking the time to contribute. 4 | 5 | The following is a set of guidelines for contributing to this package. 6 | These are mostly guidelines, not rules. Use your best judgement and feel free to propose 7 | changes to this document in a pull request. 8 | 9 | ## Coding Guidelines 10 | 1. Lint your code contributions as per [pep8 guidelines](https://www.python.org/dev/peps/pep-0008/). 11 | 12 | To help you out, we have included a `Makefile` in the root directory which supports the commands below: 13 | 14 | Autoformat code using black: 15 | 16 | ```bash 17 | make black 18 | ``` 19 | 20 | Check for typing errors: 21 | 22 | ```bash 23 | make typing 24 | ``` 25 | 26 | Check for linting errors: 27 | 28 | ```bash 29 | make lint 30 | ``` 31 | 32 | Run tests and coverage: 33 | 34 | ```bash 35 | make test 36 | ``` 37 | 38 | 2. Sort the imports in each python file as per [pep8 guidelines](https://www.python.org/dev/peps/pep-0008/#imports) 39 | Please execute the isort utility to have the imports sorted auto-magically. 40 | 41 | #### Scripts 42 | 43 | The scripts below are present in [/scripts](./scripts) directory: 44 | 45 | - `build_docs.sh` to create internal API reference documentation 46 | - `check_xyz_platform.py` runs a smoke test to check if the XYZ platform is alive and kicking 47 | - `walk_spaces.py` can serve as a template for building a maintenance script walking overall spaces 48 | 49 | #### Notebooks 50 | 51 | Example notebooks are provided in [/docs/notebooks](./docs/notebooks). 52 | 53 | ## Signing each Commit 54 | 55 | When you file a pull request, we ask that you sign off the 56 | [Developer Certificate of Origin](https://developercertificate.org/) (DCO) in each commit. 57 | Any Pull Request with commits that are not signed off will be rejected by the 58 | [DCO check](https://probot.github.io/apps/dco/). 59 | 60 | A DCO is a lightweight way to confirm that a contributor wrote or otherwise has the right 61 | to submit code or documentation to a project. Simply add `Signed-off-by` as shown in the example below 62 | to indicate that you agree with the DCO. 63 | 64 | The git flag `-s` can be used to sign a commit: 65 | 66 | ```bash 67 | git commit -s -m 'README.md: Fix minor spelling mistake' 68 | ``` 69 | 70 | The result is a signed commit message: 71 | 72 | ``` 73 | README.md: Fix minor spelling mistake 74 | 75 | Signed-off-by: John Doe 76 | ``` 77 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.md 2 | include README.md 3 | include LICENSE 4 | include Makefile 5 | include requirements*.txt 6 | include xyzspaces/datasets/* 7 | include docs/notebooks/README.md docs/notebooks/*.ipynb 8 | include scripts/* 9 | exclude ort.yml 10 | include xyzspaces/config/logconfig.json 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build install typing lint test 2 | 3 | all: black build install typing lint test 4 | 5 | black: 6 | black -l 90 xyzspaces tests docs/notebooks 7 | isort --atomic . 8 | 9 | build: 10 | python3 -m pip install -r requirements.txt 11 | 12 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 13 | 14 | clean-build: ## remove build artifacts 15 | rm -fr build/ 16 | rm -fr dist/ 17 | rm -fr .eggs/ 18 | find . -name '*.egg-info' -exec rm -fr {} + 19 | find . -name '*.egg' -exec rm -f {} + 20 | rm -fr docs/source/_build 21 | 22 | clean-pyc: ## remove Python file artifacts 23 | find . -name '*.pyc' -exec rm -f {} + 24 | find . -name '*.pyo' -exec rm -f {} + 25 | find . -name '*~' -exec rm -f {} + 26 | find . -name '__pycache__' -exec rm -fr {} + 27 | 28 | clean-test: ## remove test and coverage artifacts 29 | rm -fr .tox/ 30 | rm -f .coverage* 31 | rm -fr htmlcov/ 32 | rm -fr .mypy_cache 33 | rm -fr .pytest_cache 34 | find . -name '.ipynb_checkpoints' -exec rm -fr {} + 35 | rm -f xyz.log 36 | 37 | dockerize: 38 | jupyter-repo2docker --user-id 1000 --env XYZ_TOKEN=${XYZ_TOKEN} . 39 | 40 | install: 41 | python3 -m pip install -e . 42 | 43 | typing: 44 | pytest -v -s --mypy xyzspaces 45 | 46 | lint: 47 | isort --check --diff xyzspaces tests 48 | flake8 -v --statistics --count . 49 | black -l 90 --diff --check xyzspaces tests docs/notebooks 50 | 51 | test: 52 | pytest -v -s --durations=10 --cov=xyzspaces tests 53 | coverage html 54 | 55 | draft_changelog: 56 | proclamation draft $(shell python -c "import xyzspaces; print(xyzspaces.__version__)") 57 | 58 | build_changelog: 59 | proclamation build -d -o $(shell python -c "import xyzspaces; print(xyzspaces.__version__)") 60 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release process 2 | 3 | This document describes the release process of xyzspaces, and is mostly intended for package maintainers. 4 | 5 | 6 | ## Preparation 7 | 8 | The following are mandatory pre-release steps to bring the repository into a proper shape: 9 | 10 | - Increment `__version__` variable in [xyzspaces/__version__.py](xyzspaces/__version__.py) as desired. 11 | - Make sure all tests listed in `CONTRIBUTING.md` pass successfully. 12 | - Make sure badges appear as expected in the [README.md on GitHub](https://github.com/heremaps/xyz-spaces-python/blob/master/README.md). 13 | - Run `make build_changelog` to collect changes and prepend them to `CHANGELOG.md`, then edit this file manually if needed and commit these changes. 14 | 15 | 16 | ## Release on PyPI 17 | 18 | - Create a new release in the GitHub UI by clicking on [Draft a new release](https://github.com/heremaps/xyz-spaces-python/releases/new) button, then update the tag version and release description. 19 | - Click on the `Publish release` button to release the [package on PyPI](https://pypi.org/project/xyzspaces). 20 | - Once released verify that `pip install xyzspaces` does indeed install the latest release. 21 | 22 | 23 | ## Release on Anaconda's conda-forge channel 24 | 25 | - Go to the [xyzspaces-feedstock](https://github.com/conda-forge/xyzspaces-feedstock) repository. 26 | - Create a new release branch and update `version`, `url`, `sha256` hash of the released tar and dependencies in [meta.yml](https://github.com/conda-forge/xyzspaces-feedstock/blob/master/recipe/meta.yaml) 27 | - Verify `requirements` in [meta.yml](https://github.com/conda-forge/xyzspaces-feedstock/blob/master/recipe/meta.yaml) 28 | - Raise a PR for this release branch and merge the changes in master. 29 | - It can take hours for a new release to [appear on Anaconda.org](https://anaconda.org/conda-forge/xyzspaces). 30 | - Once available verify that `conda install -c conda-forge xyzspaces` does indeed install the latest release. 31 | -------------------------------------------------------------------------------- /binder/environment.yml: -------------------------------------------------------------------------------- 1 | name: xyzspaces 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - bqplot 6 | - here-map-widget-for-jupyter>=1.1.1 7 | - ipyleaflet 8 | - sidecar 9 | - pip 10 | - pip: 11 | - "-r ../requirements.txt" 12 | - "-r ../requirements_dev.txt" 13 | - "-e ../" 14 | -------------------------------------------------------------------------------- /binder/postBuild: -------------------------------------------------------------------------------- 1 | jupyter labextension install \ 2 | @jupyter-widgets/jupyterlab-manager \ 3 | jupyter-leaflet \ 4 | jupyterlab-python-file \ 5 | @jupyterlab/geojson-extension \ 6 | @jupyter-widgets/jupyterlab-sidecar \ 7 | @mflevine/jupyterlab_html 8 | 9 | # Notebooks with extensions that auto-run code must be "trusted" to work the first time 10 | jupyter trust docs/notebooks/*.ipynb 11 | 12 | jupyter lab build --dev-build=False --minimize=False 13 | -------------------------------------------------------------------------------- /changes/docs/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | -------------------------------------------------------------------------------- /changes/features/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | -------------------------------------------------------------------------------- /changes/misc/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | -------------------------------------------------------------------------------- /changes/tests/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | -------------------------------------------------------------------------------- /ci/env/environment.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - pip 6 | - backoff>=1.10.0 7 | - geojson 8 | - requests 9 | - geopandas 10 | - turfpy>=0.0.3 11 | - geobuf 12 | - ijson>=3.1.1 13 | - pyhocon 14 | - requests-oauthlib 15 | - pip: 16 | - "-r ../../requirements.txt" 17 | - "-r ../../requirements_dev.txt" -------------------------------------------------------------------------------- /docs/adr/0001-record-architecture-decisions.md: -------------------------------------------------------------------------------- 1 | # 1. Record architecture decisions 2 | 3 | Date: 2020-09-26 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We need to record the architectural decisions made on this project. 12 | 13 | ## Decision 14 | 15 | We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). 16 | 17 | ## Consequences 18 | 19 | See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). 20 | -------------------------------------------------------------------------------- /docs/adr/0002-use-modern-project-tooling.md: -------------------------------------------------------------------------------- 1 | # 2. Use modern project tooling 2 | 3 | Date: 2020-09-26 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | This project started when Python 3.8 was the latest version. Therefore we want to make use of an existing, modern software-engineering toolset that helps both, developers produce a feature-rich, high-quality package more quickly, as well as users get more value out of this package for their individual use cases. 12 | 13 | ## Decision(s) 14 | 15 | - This project supports Python 3.x only, starting with 3.6 now but perhaps 3.7 in the near future. 16 | - The code base makes use of type annotations wherever useful and possible without making the code too complicated. 17 | - It follows a test driven development style, and aims at a high test coverage of a minimum 90% of all lines. 18 | - It makes use of external services for checking code quality. 19 | - It uses established services for hosting the project's code base, documentation and releases. 20 | - It uses an established service to execute/test the code online. 21 | - It documents all source code (except for the testsuite) with docstrings for moduls, classes and methods/functions. 22 | - It declares the signatures of all callables with all params inside the docstrings. 23 | - It adopts a coding style as defined by PEP8 and/or Black. 24 | - It makes it easy to build a changelog automatically. 25 | - It contains enough examples for new users to obtain a quick understanding of the features of this package. 26 | - It contains a prosaic documentation explaining additional context and possible use-cases for this package. 27 | - It tests changes to the code inside a CI/CD environment before they are accepted. 28 | 29 | ## Consequences 30 | 31 | Positive effects to be expected: 32 | 33 | - The code base can make use of modern Python features. 34 | - It allowes to find errors earlier because of a mix of tests (dynamic at runtime and static due to type-checking). 35 | - It is possible to automatically generate an API reference for the code including type information. 36 | - It follows established coding conventions as they matured within the Python community. 37 | - Maintaining the code becomes easier for existing developers. 38 | - Contributing to the code becomes easier for new developers. 39 | 40 | Negative effects that might need attention at some point: 41 | 42 | - A testsuite trying to cover most of the code might slow down CI/CD, especially when run for multiple Python versions. 43 | - Care is needed for tests running concurently (for different Python versions, say) to not interfere with each other, e.g. because the operate on the same XYZ space. 44 | -------------------------------------------------------------------------------- /docs/notebooks/README.md: -------------------------------------------------------------------------------- 1 | # Jupyter Notebooks with XYZ Spaces Examples 2 | 3 | Th XYZ Spaces for Python project includes several [Jupyter notebooks](https://jupyter-notebook.readthedocs.io/en/stable/) with examples. 4 | 5 | ## Prerequisites 6 | 7 | Before you can use the example notebooks, make sure your system meets the following prerequisities: 8 | 9 | - A Python installation, 3.6+ recommended, with the `pip` command available to install dependencies. 10 | - A HERE developer account, free and available under [HERE Developer Portal](https://developer.here.com). 11 | - An XYZ API access token from your XYZ Hub server or the [XYZ portal](https://www.here.xyz) (see also its [Getting started](https://www.here.xyz/getting-started/) section) in an environment variable named `XYZ_TOKEN` which you can set like this (with a valid value, of course): 12 | 13 | ```bash 14 | export XYZ_TOKEN="MY-FANCY-XYZ-TOKEN" 15 | ``` 16 | 17 | If you prefer, you can alternatively provide this token as a parameter in your code. 18 | 19 | In order to be able run the Jupyter notebooks in this directory, you may need to install some third-party dependencies 20 | on your system. 21 | 22 | 1. Copy the text below to a file named `viz.txt`. 23 | 24 | ``` 25 | bqplot 26 | ipyleaflet 27 | ipyrest 28 | ipywidgets>=7.5.1 29 | jupyterlab>=2.1 30 | sidecar 31 | ``` 32 | 33 | 34 | 2. Run the command: 35 | 36 | ```bash 37 | pip install -r viz.txt 38 | ``` 39 | 40 | ## Running the Examples 41 | 42 | You can run all the examples in this repository locally with or without Docker, as described below. 43 | 44 | ### Using Docker 45 | 46 | If you are using Docker, please follow the instructions in `binder/README.md` to create a requirements file. Then 47 | install a tool named *repo2docker* that runs this repository in a docker container: 48 | 49 | ```bash 50 | pip install jupyter-repo2docker 51 | jupyter-repo2docker --user-id 1000 --env XYZ_TOKEN=$XYZ_TOKEN . 52 | ``` 53 | 54 | (`--user-id 1000` is a workaround for a buglet in *repo2docker* when running in networks with many users) 55 | 56 | This will generate some output ending with something like this: 57 | 58 | ``` 59 | [...] 60 | To access the notebook, open in a web browser: 61 | http://127.0.0.1:53186/?token=722b9fc8781ca0aef1b34b47645ff37c1d523a9e1a3766f4 62 | ``` 63 | 64 | Now you can open the link at the bottom in a browser to run a local Jupyter server. Once the server is running, click on the `demo.ipynb` file to open the demo notebook and execute the cells at your convenience. 65 | 66 | ### Without Docker 67 | 68 | If you want to install it all locally you can run: 69 | 70 | ```bash 71 | pip install -e . 72 | bash binder/postBuild 73 | jupyter lab docs/notebooks/demo.ipynb 74 | ``` 75 | 76 | With or without Docker, when you execute all cells of the demo notebook, you should see a map similar to the image below. 77 | 78 | ![Example map from xyzspaces demo.ipynb notebook](../../images/example_map.png) 79 | -------------------------------------------------------------------------------- /docs/notebooks/country_names.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Country Names Map\n", 8 | "\n", 9 | "This loads a GeoJSON with all country borders which are then shown on a map according to their names starting with a certain letter to be chosen with a slider in the corner of the map." 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "from functools import partial\n", 19 | "from string import ascii_uppercase\n", 20 | "\n", 21 | "from ipywidgets import IntSlider, Layout\n", 22 | "from ipyleaflet import (\n", 23 | " basemaps,\n", 24 | " Map,\n", 25 | " GeoJSON,\n", 26 | " LayersControl,\n", 27 | " FullScreenControl,\n", 28 | " WidgetControl,\n", 29 | ")\n", 30 | "\n", 31 | "from xyzspaces.datasets import get_countries_data" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "def add_countries(a_map, features):\n", 41 | " \"Add a list of GeoJSON objects to a map.\"\n", 42 | " other_layers = [l for l in a_map.layers if type(l) != GeoJSON]\n", 43 | " a_map.layers = tuple(other_layers)\n", 44 | " for f in features:\n", 45 | " gj_layer = GeoJSON(data=f, name=f[\"properties\"][\"name\"])\n", 46 | " a_map.add_layer(gj_layer)" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": null, 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "def slider_moved(event, the_map=None, the_features=None):\n", 56 | " \"Callback for slider events.\"\n", 57 | " if event.type != \"change\" or type(event.new) != int:\n", 58 | " return\n", 59 | " n = event.new\n", 60 | " assert 0 <= n < 26\n", 61 | " ch = ascii_uppercase[n]\n", 62 | " features = [\n", 63 | " f\n", 64 | " for f in the_features[\"features\"]\n", 65 | " if f[\"properties\"][\"name\"].upper().startswith(ch)\n", 66 | " # if ch in f[\"properties\"][\"name\"].upper()\n", 67 | " ]\n", 68 | " event[\"owner\"].description = f\"{len(features)} x {ch}\"\n", 69 | " add_countries(the_map, features)\n", 70 | " names = [f[\"properties\"][\"name\"] for f in features]\n", 71 | " return names" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": null, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "countries = get_countries_data()\n", 81 | "\n", 82 | "m = Map(zoom=2, basemap=basemaps.OpenStreetMap[\"Mapnik\"])\n", 83 | "m += FullScreenControl(position=\"topleft\")\n", 84 | "m += LayersControl(position=\"topright\")\n", 85 | "\n", 86 | "layout = Layout(width=\"500px\")\n", 87 | "letter_slider = IntSlider(min=0, max=25, step=1, value=1, description=\" \", layout=layout)\n", 88 | "letter_slider.observe(partial(slider_moved, the_map=m, the_features=countries))\n", 89 | "widget_control1 = WidgetControl(\n", 90 | " widget=letter_slider, position=\"bottomleft\", layout=layout\n", 91 | ")\n", 92 | "m += widget_control1\n", 93 | "\n", 94 | "# This is needed to trigger the first slider change event,\n", 95 | "# or the map would be empty at the start:\n", 96 | "letter_slider.value = 0\n", 97 | "\n", 98 | "m" 99 | ] 100 | } 101 | ], 102 | "metadata": { 103 | "kernelspec": { 104 | "display_name": "Python 3", 105 | "language": "python", 106 | "name": "python3" 107 | }, 108 | "language_info": { 109 | "codemirror_mode": { 110 | "name": "ipython", 111 | "version": 3 112 | }, 113 | "file_extension": ".py", 114 | "mimetype": "text/x-python", 115 | "name": "python", 116 | "nbconvert_exporter": "python", 117 | "pygments_lexer": "ipython3", 118 | "version": "3.7.7" 119 | } 120 | }, 121 | "nbformat": 4, 122 | "nbformat_minor": 4 123 | } 124 | -------------------------------------------------------------------------------- /docs/notebooks/data/chicago_parks.geo.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": [ 3 | { 4 | "type": "Feature", 5 | "id": "LP", 6 | "properties": { 7 | "title": "Lincoln Park", 8 | "description": "A northside park that is home to the Lincoln Park Zoo", 9 | "area": "1200" 10 | }, 11 | "geometry": { 12 | "coordinates": [ 13 | -87.637596, 14 | 41.940403, 15 | 4.0 16 | ], 17 | "type": "Point" 18 | } 19 | }, 20 | { 21 | "type": "Feature", 22 | "id": "BP", 23 | "properties": { 24 | "title": "Burnham Park", 25 | "description": "A lakefront park on Chicago's south side", 26 | "area": "598" 27 | }, 28 | "geometry": { 29 | "coordinates": [ 30 | -87.603735, 31 | 41.829985, 32 | 0.0 33 | ], 34 | "type": "Point" 35 | } 36 | }, 37 | { 38 | "type": "Feature", 39 | "id": "MP", 40 | "properties": { 41 | "title": "Millennium Park", 42 | "description": "A downtown park known for its art installations and unique architecture", 43 | "area": "24.5" 44 | }, 45 | "geometry": { 46 | "coordinates": [ 47 | -87.622554, 48 | 41.882534, 49 | 0.0 50 | ], 51 | "type": "Point" 52 | } 53 | }, 54 | { 55 | "type": "Feature", 56 | "id": "GP", 57 | "properties": { 58 | "title": "Grant Park", 59 | "description": "A downtown park that is the site of many of Chicago's favorite festivals and events", 60 | "area": "319" 61 | }, 62 | "geometry": { 63 | "coordinates": [ 64 | -87.619185, 65 | 41.876367, 66 | 0.0 67 | ], 68 | "type": "Point" 69 | } 70 | }, 71 | { 72 | "type": "Feature", 73 | "id": "HP", 74 | "properties": { 75 | "title": "Humboldt Park", 76 | "description": "A large park on Chicago's northwest side", 77 | "area": "207" 78 | }, 79 | "geometry": { 80 | "coordinates": [ 81 | -87.70199, 82 | 41.905423, 83 | 0.0 84 | ], 85 | "type": "Point" 86 | } 87 | }, 88 | { 89 | "type": "Feature", 90 | "id": "DP", 91 | "properties": { 92 | "title": "Douglas Park", 93 | "description": "A large park near in Chicago's North Lawndale neighborhood", 94 | "area": "173" 95 | }, 96 | "geometry": { 97 | "coordinates": [ 98 | -87.699329, 99 | 41.860092, 100 | 100.0 101 | ], 102 | "type": "Point" 103 | } 104 | }, 105 | { 106 | "type": "Feature", 107 | "id": "CP", 108 | "properties": { 109 | "title": "Calumet Park", 110 | "description": "A park on the Illinois-Indiana border featuring a historic fieldhouse", 111 | "area": "200" 112 | }, 113 | "geometry": { 114 | "coordinates": [ 115 | -87.530221, 116 | 41.715515, 117 | 400.34 118 | ], 119 | "type": "Point" 120 | } 121 | }, 122 | { 123 | "type": "Feature", 124 | "id": "JP", 125 | "properties": { 126 | "title": "Jackson Park", 127 | "description": "A lakeside park that was the site of the 1893 World's Fair", 128 | "area": "500" 129 | }, 130 | "geometry": { 131 | "coordinates": [ 132 | -87.580389, 133 | 41.783185, 134 | 0.0 135 | ], 136 | "type": "Point" 137 | } 138 | }, 139 | { 140 | "type": "Feature", 141 | "id": "COP", 142 | "properties": { 143 | "title": "Columbus Park", 144 | "description": "A large park in Chicago's Austin neighborhood", 145 | "area": "144" 146 | }, 147 | "geometry": { 148 | "coordinates": [ 149 | -87.769775, 150 | 41.873683, 151 | 10.0 152 | ], 153 | "type": "Point" 154 | } 155 | } 156 | ], 157 | "type": "FeatureCollection" 158 | } -------------------------------------------------------------------------------- /docs/notebooks/data/test.csv: -------------------------------------------------------------------------------- 1 | policyID,statecode,county,eq_site_limit,hu_site_limit,fl_site_limit,fr_site_limit,tiv_2011,tiv_2012,eq_site_deductible,hu_site_deductible,fl_site_deductible,fr_site_deductible,latitude,longitude,line,construction,point_granularity 119736,FL,CLAY COUNTY,498960,498960,498960,498960,498960,792148.9,0,9979.2,0,0,30.102261,-81.711777,Residential,Masonry,1 448094,FL,CLAY COUNTY,1322376.3,1322376.3,1322376.3,1322376.3,1322376.3,1438163.57,0,0,0,0,30.063936,-81.707664,Residential,Masonry,3 206893,FL,CLAY COUNTY,190724.4,190724.4,190724.4,190724.4,190724.4,192476.78,0,0,0,0,30.089579,-81.700455,Residential,Wood,1 333743,FL,CLAY COUNTY,0,79520.76,0,0,79520.76,86854.48,0,0,0,0,30.063236,-81.707703,Residential,Wood,3 -------------------------------------------------------------------------------- /docs/notebooks/data/test.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "id" : "test_geojson_1", 6 | "type": "Feature", 7 | "properties": { 8 | 9 | }, 10 | "geometry": { 11 | "type": "Point", 12 | "coordinates": [ 13 | -4.187271595001221, 14 | 40.95480471183075 15 | ] 16 | } 17 | }, 18 | { 19 | "id" : "test_geojson_2", 20 | "type": "Feature", 21 | "properties": { 22 | 23 | }, 24 | "geometry": { 25 | "type": "Point", 26 | "coordinates": [ 27 | -4.186810255050659, 28 | 40.954148389459085 29 | ] 30 | } 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /docs/notebooks/data/xyz_configuration.conf: -------------------------------------------------------------------------------- 1 | credentials: { 2 | XYZ_TOKEN: "MY-XYZ-TOKEN" 3 | } 4 | http_headers: { 5 | Authorization: "Bearer MY-XYZ-TOKEN" 6 | Content-Type: "application/geo+json" 7 | } 8 | url: "https://xyz.api.here.com" 9 | -------------------------------------------------------------------------------- /docs/notebooks/xyzspaces_configurations.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "tight-upper", 6 | "metadata": {}, 7 | "source": [ 8 | "# Configurations\n", 9 | "\n", 10 | "This notebook demonstrates various ways to configure `xyzspaces`" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "id": "coordinate-framework", 16 | "metadata": {}, 17 | "source": [ 18 | "## Default configuration\n", 19 | "In default configurations `XYZ_TOKEN` will be read from environment variable and other configurations options such as Data Hub URL and http headers will be taken as default values." 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "id": "seasonal-practitioner", 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "from xyzspaces import XYZ" 30 | ] 31 | }, 32 | { 33 | "cell_type": "markdown", 34 | "id": "liberal-fashion", 35 | "metadata": {}, 36 | "source": [ 37 | "Default configuration expects `XYZ_TOKEN` is exported in environment vairable and you just need to instantiate an object of XYZ class as shown below." 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "id": "russian-sheffield", 44 | "metadata": {}, 45 | "outputs": [], 46 | "source": [ 47 | "xyz = XYZ()" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "id": "portable-fundamental", 53 | "metadata": {}, 54 | "source": [ 55 | "## User defined configuration using config object\n", 56 | "You can create object of `XYZConfig` and define the configuration as shown below. The user defined configuration is required when you are hosting Data Hub APIs in on your own in separate instance. " 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": null, 62 | "id": "offshore-buffalo", 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "from xyzspaces import XYZ\n", 67 | "from xyzspaces.config.default import XYZConfig" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": null, 73 | "id": "associate-playback", 74 | "metadata": {}, 75 | "outputs": [], 76 | "source": [ 77 | "config_dict = {\n", 78 | " \"credentials\": {\"XYZ_TOKEN\": \"MY-XYZ-TOKEN\"},\n", 79 | " \"http_headers\": {\n", 80 | " \"Authorization\": \"Bearer MY-XYZ-TOKEN\",\n", 81 | " \"Content-Type\": \"application/geo+json\",\n", 82 | " },\n", 83 | " \"url\": \"https://xyz.api.here.com\",\n", 84 | "}\n", 85 | "\n", 86 | "config = XYZConfig(**config_dict)\n", 87 | "xyz = XYZ(config=config)" 88 | ] 89 | }, 90 | { 91 | "cell_type": "markdown", 92 | "id": "confidential-newspaper", 93 | "metadata": {}, 94 | "source": [ 95 | "## User defined configuration using config file\n", 96 | "You can read configuration from json file." 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "id": "adequate-writing", 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "from xyzspaces import XYZ\n", 107 | "from xyzspaces.config.default import XYZConfig\n", 108 | "\n", 109 | "xyz_config = XYZConfig.from_file(\"data/xyz_configuration.conf\")" 110 | ] 111 | }, 112 | { 113 | "cell_type": "code", 114 | "execution_count": null, 115 | "id": "cosmetic-buying", 116 | "metadata": {}, 117 | "outputs": [], 118 | "source": [ 119 | "xyz = XYZ(config=xyz_config)" 120 | ] 121 | } 122 | ], 123 | "metadata": { 124 | "kernelspec": { 125 | "display_name": "Python 3", 126 | "language": "python", 127 | "name": "python3" 128 | }, 129 | "language_info": { 130 | "codemirror_mode": { 131 | "name": "ipython", 132 | "version": 3 133 | }, 134 | "file_extension": ".py", 135 | "mimetype": "text/x-python", 136 | "name": "python", 137 | "nbconvert_exporter": "python", 138 | "pygments_lexer": "ipython3", 139 | "version": "3.8.5" 140 | } 141 | }, 142 | "nbformat": 4, 143 | "nbformat_minor": 5 144 | } 145 | -------------------------------------------------------------------------------- /docs/source/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/source/blogs_talks.rst: -------------------------------------------------------------------------------- 1 | Blogs and Talks 2 | =============== 3 | Various blog posts and conference presentations about ``xyzspaces``: 4 | 5 | Blogs 6 | ----- 7 | 8 | - `A simple way to build your own 3D map with Coloured LIDAR point clouds using xyzspaces`_. 9 | 10 | 11 | .. _A simple way to build your own 3D map with Coloured LIDAR point clouds using xyzspaces: https://medium.com/analytics-vidhya/a-simple-way-to-build-your-own-3d-map-with-coloured-lidar-point-clouds-using-xyzspaces-5b3ca6e43d48 12 | 13 | Talks 14 | ----- 15 | 16 | - `Online Python Machine Learning Conference & GeoPython 2020`_ 17 | 18 | .. _Online Python Machine Learning Conference & GeoPython 2020: http://2020.geopython.net/ -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | xyzspaces 0.7.2 (2021-08-18) 5 | ---------------------------- 6 | * Made geopandas, turfpy, geobuf as optional dependencies of the xyzspaces 7 | 8 | xyzspaces 0.7.1 (2021-08-10) 9 | ---------------------------- 10 | * Fixed minor issues 11 | 12 | xyzspaces 0.7.0 (2021-08-10) 13 | ---------------------------- 14 | 15 | * **Features** 16 | 17 | * Added support for Interactive Map Layers. (`#117 `_) 18 | 19 | 20 | xyzspaces 0.6.0 (2021-06-17) 21 | ---------------------------- 22 | 23 | * **Features** 24 | 25 | * Added support custom base URL of Data Hub APIs for self-hosted Data Hub. (`#113 `_) 26 | 27 | * Added support for launch datahub space invader. (`commit `_) 28 | 29 | * Fixed feature_id gettting skipped from GeoPandas data.(`commit `_) 30 | 31 | xyzspaces 0.5.0 (2021-02-01) 32 | ---------------------------- 33 | 34 | * **Features** 35 | 36 | * Added functionality to ``clone`` Space. (`#93 `_) 37 | * Handle HTTP 429 responses with ``backoff`` package. (`#95 `_) 38 | * support the new ``force2D`` parameter for all the APIs used to read features. (`#96 `_) 39 | * Add support for new mode and vizSampling params in HubApi.get_space_tile. (`#101 `_) 40 | 41 | * **Documentation** 42 | 43 | * Start collecting/documenting architecture decision records (ADRs). (`#90 `_) 44 | * Add support for executable code in Sphinx documentation. (`#99 `_) 45 | 46 | * **Misc** 47 | 48 | * Migrated CI from Travis to GitHub Actions. (`#111 `_) 49 | 50 | xyzspaces 0.4.0 (2020-09-18) 51 | ---------------------------- 52 | 53 | * **Features** 54 | 55 | * Added feature to upload data from ``kml`` file to the space.(`#49 `__) 56 | * Added ``limit`` param to method ``iter_feature`` to control numer of features to iterate in single call.(`#52 `__) 57 | * Fixed encoding and projections issue for ``shapefile`` upload.(`#54 `__) 58 | * Enabled property search while searching features in bounding box.(`#56 `__) 59 | * Added feature to upload data from ``geobuff`` file to the space.(`#57 `__) 60 | * Remove duplicate features for ``spatial_search_geometry`` with division(`#58 `__) 61 | * Enabled property search while searching features in tile.(`#61 `__) 62 | * Remove duplicate features while add to space using ``add_features`` and also added a ``mutation`` parameter to mutate input features or not.(`#64 `__) 63 | * ``description`` is optional when creating the space.(`#68 `__) 64 | * Added feature to upload data from ``Geopandas Dataframe`` file to the space.(`#71 `__) 65 | * Enabled reading space data as Geopandas Dataframe.(`#72 `__) 66 | * Improved performance of CSV upload.(`#77 `__) 67 | * Improvement in the performance of ``add_features_geojson``.(`#79 `__) 68 | * Changes to convert shape file with projection of different type to EPSG:4326.(`#83 `__) 69 | 70 | * **Documentation** 71 | 72 | * New notebook illustrating spatial search on MS US building footprints dataset.(`#62 `__) 73 | 74 | xyzspaces 0.3.2 (2020-08-19) 75 | ---------------------------- 76 | 77 | * **Features** 78 | 79 | * Added feature to upload data from ``shapefile`` to the space.(`#40 `__) 80 | * Added feature to upload data from ``WKT`` file to the space.(`#41 `__) 81 | * Added feature to upload data from ``gpx`` file to the space.(`#42 `__) 82 | * Optimized the ``spatial search`` to search features from large geometries.(`#44 `__) 83 | 84 | * **Misc** 85 | 86 | * Added Binder support to the repository.(`#28 `__) 87 | * Added **clientId** in query params of the Hub API requests.(`#36 `__) 88 | * Updated ``__version__`` attribute now it can be used as ``from xyzspaces import __version__``.(`#38 `__) 89 | 90 | xyzspaces 0.3.1 (2020-07-24) 91 | ---------------------------- 92 | 93 | * **Misc** 94 | 95 | * Minor changes to README.(`0.3.1 `__) 96 | 97 | xyzspaces 0.3.0 (2020-07-24) 98 | ---------------------------- 99 | 100 | * **Misc** 101 | 102 | * First public release.(`0.3.0 `__) 103 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | 16 | 17 | """Sphinx configuration file.""" 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = "xyzspaces" 22 | copyright = "2019-2021, HERE Europe B.V." 23 | author = "HERE Europe B.V." 24 | 25 | 26 | # -- General configuration --------------------------------------------------- 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | "sphinx.ext.autodoc", 33 | "sphinx.ext.viewcode", 34 | "sphinx.ext.todo", 35 | "sphinx.ext.autodoc.typehints", 36 | "sphinx_rtd_theme", 37 | "sphinx_thebe", 38 | "sphinx_copybutton", 39 | ] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ["_templates"] 43 | 44 | # The language for content autogenerated by Sphinx. Refer to documentation 45 | # for a list of supported languages. 46 | # 47 | # This is also used if you do content translation via gettext catalogs. 48 | # Usually you set "language" from the command line for these cases. 49 | language = "en" 50 | 51 | # List of patterns, relative to source directory, that match files and 52 | # directories to ignore when looking for source files. 53 | # This pattern also affects html_static_path and html_extra_path. 54 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 55 | 56 | # -- Options for Sphinx-Thebe ------------------------------------------------ 57 | 58 | thebe_config = { 59 | "repository_url": "https://github.com/heremaps/xyz-spaces-python", 60 | "repository_branch": "master", 61 | "selector": ".thebe", 62 | # "selector_input": "", 63 | # "selector_output": "", 64 | } 65 | 66 | # -- Options for HTML output ------------------------------------------------- 67 | 68 | # The theme to use for HTML and HTML Help pages. See the documentation for 69 | # a list of builtin themes. 70 | # 71 | html_theme = "sphinx_rtd_theme" 72 | 73 | # Add any paths that contain custom static files (such as style sheets) here, 74 | # relative to this directory. They are copied after the builtin static files, 75 | # so a file named "default.css" will overwrite the builtin "default.css". 76 | html_static_path = ["_static"] 77 | master_doc = "index" 78 | pygments_style = "sphinx" 79 | 80 | 81 | # -- Extension configuration ------------------------------------------------- 82 | 83 | # -- Options for todo extension ---------------------------------------------- 84 | 85 | # If true, `todo` and `todoList` produce output, else they produce nothing. 86 | todo_include_todos = True 87 | autodoc_typehints = "description" 88 | 89 | # -- autodoc options configuration ------------------------------------------------- 90 | 91 | autodoc_default_options = { 92 | "members": True, 93 | "undoc-members": True, 94 | "special-members": True, 95 | "show-inheritance": True, 96 | "member-order": "bysource", 97 | "exclude-members": "__dict__,__weakref__,__module__", 98 | } 99 | -------------------------------------------------------------------------------- /docs/source/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | The Jupyter `notebooks `__ show various functionalities of xyzspaces. 4 | You can directly play with examples by clicking on the binder button: 5 | 6 | .. image:: https://mybinder.org/badge_logo.svg 7 | :target: https://mybinder.org/v2/gh/heremaps/xyz-spaces-python/master?urlpath=lab/tree/docs/notebooks 8 | 9 | To run the example notebooks locally See `docs/notebooks/README.md `__. 10 | 11 | The GIF below shows an interaction with an example `notebook `__, demonstrating how to use a spatial search on a big public dataset, loaded from the HERE `Data Hub `__. 12 | 13 | .. image:: https://github.com/heremaps/xyz-spaces-python/raw/master/images/building_numbers.gif 14 | 15 | 16 | Interactive examples 17 | -------------------- 18 | 19 | These are some preliminary code snippets that can be executed online. Click on the "Start" button first before you execute the following cells! 20 | 21 | .. thebe-button:: Start 22 | 23 | .. comment 24 | 25 | The following is a cell. Click the "run" button or press Shift-Enter inside the cell to execute it. Launching the computation backend may take a few seconds, and you may need to re-start it. 26 | 27 | .. code-block:: python 28 | :class: thebe 29 | 30 | import xyzspaces 31 | xyzspaces.__version__ 32 | 33 | This is another cell. Replace the "MY_XYZ_TOKEN" with your real XYZ token and click "run" again to search for the GeoJSON feature of the White House in Washington, DC, USA! 34 | 35 | .. code-block:: python 36 | :class: thebe 37 | 38 | import os 39 | import geojson 40 | from xyzspaces.datasets import get_microsoft_buildings_space 41 | 42 | os.environ["XYZ_TOKEN"] = "MY_XYZ_TOKEN" 43 | space = get_microsoft_buildings_space() 44 | feat = next(space.search(tags=["postalcode@20500"])) 45 | print(geojson.dumps(feat, indent=4)) 46 | 47 | More to come... please stay tuned! 48 | 49 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. xyzspaces documentation master file, created by 2 | sphinx-quickstart on Wed Aug 5 16:59:49 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | =========================== 7 | Documentation for xyzspaces 8 | =========================== 9 | 10 | Manage your `XYZ Hub`_ or `HERE Data Hub`_ spaces and `Interactive Map Layer`_ from Python. 11 | 12 | Data Hub 13 | ======== 14 | 15 | XYZ is an Open Source, real-time, cloud database system providing access to large geospatial data at scale. 16 | An XYZ "Hub" manages "spaces" that contain "features" (geodata "records") with tags and properties, with spaces and features having unique IDs. 17 | A RESTful API exists to provide low-level access to interact with a XYZ Hub. 18 | 19 | This Python package allows to interact with your XYZ spaces and features on a given Hub using a higher level programmatic interface that wraps the RESTful API. 20 | Using this package you can: 21 | 22 | * Create, read, list, update, share, delete spaces (also: get space info and stats). 23 | * Add, read, update, iterate, search, cluster (hex/quad bins), delete features. 24 | * Search features by ID, tag, property, bbox, tile, radius, geometry. 25 | 26 | Based on the XYZ Hub the HERE Data Hub is a commercial service (with a free plan), that offers some additional features (in a pro plan), like clustering, virtual spaces, activity logs, schema validation, rule based tagging and likely more to come. 27 | 28 | Interactive Map Layers 29 | ====================== 30 | 31 | The ``xyzspaces`` package supports Interactive Map Layers which is Data Hub on `HERE Platform `_. 32 | Using ``xyzspaces`` you can interact with your Interactive Map Layers using higher level pythonic interface that wraps the RESTful API. 33 | With Interactive Map Layers, data is stored in GeoJSON and can be retrieved dynamically at any zoom level. 34 | Interactive map layer is optimized for the visualization, analysis, and modification of data on a map (i.e., GIS functions). 35 | 36 | Key features of Interactive Map Layers include: 37 | 38 | * Creating and modifying maps manually or programmatically; edits are published real-time and require no additional interaction. 39 | 40 | * Modifying data a granular feature and feature property level. 41 | 42 | * Adding and removing points, lines, and polygons directly on a map. 43 | 44 | * Ability to retrieve data in different tiling schemes. 45 | 46 | * Exploring and retrieving data by feature ID, bounding box, spatial search, property search, and features contained within a tile. 47 | 48 | * Searching for data by values of feature properties (e.g., speed limits, type of place, address, name, etc.). 49 | 50 | * Data sampling, making it possible to efficiently render an excerpt of a very large data set for visual reference and analysis. 51 | 52 | * Clustering using hexbins or quadbins to produce rich, visual data representations. 53 | 54 | 55 | .. _XYZ Hub: https://github.com/heremaps/xyz-hub 56 | .. _HERE Data Hub: https://developer.here.com/products/data-hub 57 | .. _Interactive Map Layer: https://developer.here.com/documentation/data-user-guide/user_guide/portal/layers/layers.html 58 | 59 | .. toctree:: 60 | :maxdepth: 1 61 | :caption: Getting Started 62 | 63 | Prerequisites 64 | Installation 65 | 66 | .. toctree:: 67 | :maxdepth: 1 68 | :caption: User Guide 69 | 70 | Examples 71 | API Reference 72 | Logging 73 | 74 | .. toctree:: 75 | :maxdepth: 1 76 | :caption: Reference Guide 77 | 78 | Tests 79 | Changelog 80 | Blogs and Talks 81 | 82 | .. toctree:: 83 | :maxdepth: 1 84 | :caption: Developer 85 | 86 | Contributing to xyzspaces 87 | 88 | 89 | Indices and tables 90 | ================== 91 | 92 | * :ref:`genindex` 93 | * :ref:`modindex` 94 | * :ref:`search` 95 | -------------------------------------------------------------------------------- /docs/source/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | xyzspaces has optional dependencies for its spatial functionality on a large geospatial, open 5 | source stack of libraries (`Geopandas`_, `turfpy`_, `geobuf`_). See the 6 | :ref:`dependencies` section below for more details. The C depedencies of Geopandas such as (`GEOS`_, `GDAL`_, `PROJ`_) 7 | can sometimes be a challenge to install. Therefore, we advise you 8 | to closely follow the recommendations below to avoid installation problems. 9 | 10 | .. _install-conda: 11 | 12 | Installing with Anaconda / conda 13 | -------------------------------- 14 | 15 | To install xyzspaces and all its dependencies, we recommend to use the `conda`_ 16 | package manager. This can be obtained by installing the 17 | `Anaconda Distribution`_ (a free Python distribution for data science), or 18 | through `miniconda`_ (minimal distribution only containing Python and the 19 | `conda`_ package manager). See also the `installation docs 20 | `__ for more information 21 | on how to install Anaconda or miniconda locally. 22 | 23 | The advantage of using the `conda`_ package manager is that it provides 24 | pre-built binaries for all the required dependencies of xyzspaces 25 | for all platforms (Windows, Mac, Linux). 26 | 27 | To install the latest version of xyzspaces from `conda-forge`_, you can then do:: 28 | 29 | conda install -c conda-forge xyzspaces 30 | 31 | Creating a new environment 32 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 33 | 34 | Creating a new environment is not strictly necessary, but given that installing 35 | other geospatial packages from different channels may cause dependency conflicts 36 | (as mentioned in the note above), it can be good practice to install the geospatial 37 | stack in a clean environment starting fresh. 38 | 39 | The following commands create a new environment with the name ``xyz_env``, 40 | configures it to install packages always from conda-forge, and installs 41 | xyzspaces in it:: 42 | 43 | conda create -n xyz_env 44 | conda activate xyz_env 45 | conda config --env --add channels conda-forge 46 | conda config --env --set channel_priority strict 47 | conda install python=3 xyzspaces 48 | 49 | 50 | .. _install-pip: 51 | 52 | Installing with pip 53 | ------------------- 54 | 55 | Install ``xyzspaces`` without optional dependencies:: 56 | 57 | pip install xyzspaces 58 | 59 | Install ``xyzspaces`` with optional dependencies:: 60 | 61 | pip install "xyzspaces[geo]" 62 | 63 | 64 | .. _install-deps: 65 | 66 | .. warning:: 67 | 68 | When using pip to install xyzspaces, you need to make sure that all dependencies of Geopandas are 69 | installed correctly. 70 | 71 | - `fiona`_ provides binary wheels with the dependencies included for Mac and Linux, 72 | but not for Windows. 73 | - `pyproj`_ and `shapely`_ provide binary wheels with dependencies included 74 | for Mac, Linux, and Windows. 75 | - `rtree`_ does not provide wheels. 76 | - Windows wheels for `shapely`, `fiona`, `pyproj` and `rtree` 77 | can be found at `Christopher Gohlke's website 78 | `_. 79 | 80 | So depending on your platform, you might need to compile and install their 81 | C dependencies manually. We refer to the individual packages for more 82 | details on installing those. 83 | Using conda (see above) avoids the need to compile the dependencies yourself. 84 | 85 | Installing from source 86 | ---------------------- 87 | 88 | You may install the latest development version by cloning the 89 | `GitHub` repository and using pip to install from the local directory:: 90 | 91 | git clone https://github.com/heremaps/xyz-spaces-python.git 92 | cd xyz-spaces-python 93 | pip install . 94 | 95 | It is also possible to install the latest development version 96 | directly from the GitHub repository with:: 97 | 98 | pip install -e git+https://github.com/heremaps/xyz-spaces-python#egg=xyzspaces 99 | 100 | For installing xyzspaces from source, the same :ref:`note ` on 101 | the need to have all dependencies correctly installed applies. 102 | 103 | See the :ref:`section on conda ` above for more details on 104 | getting running with Anaconda. 105 | 106 | .. _dependencies: 107 | 108 | Dependencies 109 | ------------ 110 | 111 | Required dependencies: 112 | 113 | - `requirements`_ 114 | 115 | Optional depedencies: 116 | 117 | - `Geopandas`_ 118 | - `turfpy`_ 119 | - `geobuf`_ 120 | 121 | Dev dependencies: 122 | 123 | - `dev requirements`_ 124 | 125 | 126 | 127 | .. _PyPI: https://pypi.python.org/pypi/xyzspaces 128 | 129 | .. _GitHub: https://github.com/heremaps/xyz-spaces-python 130 | 131 | .. _requirements: https://github.com/heremaps/xyz-spaces-python/blob/master/requirements.txt 132 | 133 | .. _dev requirements: https://github.com/heremaps/xyz-spaces-python/blob/master/requirements_dev.txt 134 | 135 | .. _Geopandas: https://geopandas.org/ 136 | 137 | .. _turfpy: https://pypi.org/project/turfpy/ 138 | 139 | .. _shapely: https://shapely.readthedocs.io 140 | 141 | .. _fiona: https://fiona.readthedocs.io 142 | 143 | .. _pyproj: https://github.com/pyproj4/pyproj 144 | 145 | .. _rtree: https://github.com/Toblerity/rtree 146 | 147 | .. _conda: https://conda.io/en/latest/ 148 | 149 | .. _Anaconda distribution: https://www.anaconda.com/distribution/ 150 | 151 | .. _miniconda: https://docs.conda.io/en/latest/miniconda.html 152 | 153 | .. _conda-forge: https://conda-forge.org/ 154 | 155 | .. _GDAL: https://www.gdal.org/ 156 | 157 | .. _GEOS: https://geos.osgeo.org 158 | 159 | .. _PROJ: https://proj.org/ 160 | 161 | .. _geobuf: https://pypi.org/project/geobuf/ 162 | 163 | -------------------------------------------------------------------------------- /docs/source/logging.rst: -------------------------------------------------------------------------------- 1 | Logging Configuration 2 | ===================== 3 | By default logging is disabled. To enable logging, use below code snippets in your python code to setup logging at DEBUG level: 4 | 5 | .. code-block:: python 6 | 7 | import logging 8 | from xyzspaces import setup_logging 9 | 10 | setup_logging(default_level=logging.DEBUG) 11 | 12 | Default logging configuration is defined in a `file `__. 13 | This ensures that log messages will be written to the file ``xyz.log`` in your current working directory. 14 | Here is an example log file (xyz.log):: 15 | 16 | 2020-02-21 17:55:46,132 - apis.py:130 - ERROR - Curl command: curl --request GET https://xyz.api.here.com/hub/spaces/dummy-111 --header "Authorization: Bearer " 17 | 2020-02-21 17:55:46,133 - apis.py:131 - ERROR - Response status code: 404 18 | 2020-02-21 17:55:46,133 - apis.py:132 - ERROR - Response headers: {'Content-Type': 'application/json', 'Content-Length': '150', 'Connection': 'keep-alive', 'Date': 'Fri, 21 Feb 2020 12:25:46 GMT', 'x-amzn-RequestId': '397c8039-79f1-4956-bbe4-46ca78c7ec2d', 'content-encoding': 'gzip', 'Stream-Id': '397c8039-79f1-4956-bbe4-46ca78c7ec2d', 'x-amzn-Remapped-Content-Length': '150', 'x-amzn-Remapped-Connection': 'keep-alive', 'x-amz-apigw-id': 'IPzblGVFjoEF5pg=', 'x-amzn-Remapped-Date': 'Fri, 21 Feb 2020 12:25:46 GMT', 'X-Cache': 'Error from cloudfront', 'Via': '1.1 e25383e25378de918d3b187b3239eb5b.cloudfront.net (CloudFront)', 'X-Amz-Cf-Pop': 'BOM51-C2', 'X-Amz-Cf-Id': 'nZAJUB_FBiHdojziSoG3SBcMdf8rNyHuOMSlJljyxDNlx1I0O3t9YQ=='} 19 | 2020-02-21 17:55:46,134 - apis.py:133 - ERROR - Response text: {"type":"ErrorResponse","error":"Exception","errorMessage":"The requested resource does not exist.","streamId":"397c8039-79f1-4956-bbe4-46ca78c7ec2d"} 20 | 21 | 22 | To customize the logging configuration, set the variable XYZ_LOG_CONFIG to hold the full path of the logging configuration options file `logging_config.json `__:: 23 | 24 | export XYZ_LOG_CONFIG=~/logging_config.json 25 | 26 | -------------------------------------------------------------------------------- /docs/source/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/prerequisites.rst: -------------------------------------------------------------------------------- 1 | Prerequisites 2 | ============= 3 | 4 | Before you install the ``xyzspaces`` package make sure you meet the following prerequisites: 5 | 6 | * A Python installation, 3.7+ recommended, with the ``pip`` or ``conda`` command available to install dependencies. 7 | 8 | Data Hub 9 | -------- 10 | * A HERE developer account, free and available under `HERE Developer Portal`_. 11 | * An XYZ API access token from your XYZ Hub server or the `XYZ portal`_ (see also its `Getting Started`_ section) in an environment variable named XYZ_TOKEN which you can set like this (with a valid value, of course):: 12 | 13 | export XYZ_TOKEN=MY-XYZ-TOKEN 14 | 15 | 16 | .. _HERE Developer Portal: https://developer.here.com/ 17 | .. _XYZ portal: https://www.here.xyz/ 18 | .. _Getting Started: https://www.here.xyz/getting-started/ 19 | 20 | Interactive Map Layer 21 | --------------------- 22 | 23 | * To interact with Interactive Map Layers you will need an account on the HERE Platform. To get more details on the HERE Platform account please check our documentation `Get a HERE account `_. 24 | 25 | Once you have the account follow the below steps to get credentials: 26 | 27 | * Go to `HERE Platform Applications and Keys `_ and register a new app. 28 | 29 | * Create a key for the app and download the generated ``credentials.properties`` file. 30 | 31 | The HERE platform generated app credentials should look similar to the example below:: 32 | 33 | here.user.id = 34 | here.client.id = 35 | here.access.key.id = 36 | here.access.key.secret = 37 | here.token.endpoint.url = 38 | 39 | You can provide your credentials using any of the following methods: 40 | 41 | * Default credentials 42 | 43 | * Environment variables 44 | 45 | * Credentials file 46 | 47 | Default credentials 48 | ~~~~~~~~~~~~~~~~~~~~~~ 49 | * Place the credentials file into: 50 | 51 | For Linux/MacOS:: 52 | 53 | $HOME/.here/credentials.properties 54 | 55 | For Windows:: 56 | 57 | %USERPROFILE%\.here\credentials.properties 58 | 59 | 60 | Environment Variables 61 | ~~~~~~~~~~~~~~~~~~~~~~ 62 | You can override default credentials by assigning values to the following environment variables:: 63 | 64 | HERE_USER_ID 65 | HERE_CLIENT_ID 66 | HERE_ACCESS_KEY_ID 67 | HERE_ACCESS_KEY_SECRET 68 | HERE_TOKEN_ENDPOINT_URL 69 | 70 | 71 | Credentials File 72 | ~~~~~~~~~~~~~~~~ 73 | You can specify any credentials file as an alternative to that found in ``~/.here/credentials.properties``. An error is generated if there is no file present at the path, or if the file is not properly formatted. 74 | 75 | -------------------------------------------------------------------------------- /docs/source/tests.rst: -------------------------------------------------------------------------------- 1 | Tests 2 | ===== 3 | 4 | xyzspaces uses `pytest `__ for testing. 5 | 6 | You can run the test suite locally:: 7 | 8 | pip install -r requirements_dev.txt 9 | pytest -v --cov=xyzspaces tests 10 | 11 | The test suite provides test coverage of around 98% (but less if the tests cannot find your credentials). 12 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.__version__.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.\_\_version\_\_ module 2 | ================================ 3 | 4 | .. automodule:: xyzspaces.__version__ 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces._compact.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.\_compact module 2 | ========================== 3 | 4 | .. automodule:: xyzspaces._compact 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.apis.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.apis module 2 | ===================== 3 | 4 | .. automodule:: xyzspaces.apis 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.auth.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.auth module 2 | ===================== 3 | 4 | .. automodule:: xyzspaces.auth 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.config.default.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.config.default module 2 | =============================== 3 | 4 | .. automodule:: xyzspaces.config.default 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.config.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.config package 2 | ======================== 3 | 4 | .. automodule:: xyzspaces.config 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | 10 | Submodules 11 | ---------- 12 | 13 | .. toctree:: 14 | :maxdepth: 4 15 | 16 | xyzspaces.config.default 17 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.curl.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.curl module 2 | ===================== 3 | 4 | .. automodule:: xyzspaces.curl 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.datasets.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.datasets package 2 | ========================== 3 | 4 | .. automodule:: xyzspaces.datasets 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.exceptions.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.exceptions module 2 | =========================== 3 | 4 | .. automodule:: xyzspaces.exceptions 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.iml.apis.aaa_oauth2_api.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.iml.apis.aaa\_oauth2\_api module 2 | ========================================== 3 | 4 | .. automodule:: xyzspaces.iml.apis.aaa_oauth2_api 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.iml.apis.api.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.iml.apis.api module 2 | ============================= 3 | 4 | .. automodule:: xyzspaces.iml.apis.api 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.iml.apis.data_config_api.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.iml.apis.data\_config\_api module 2 | =========================================== 3 | 4 | .. automodule:: xyzspaces.iml.apis.data_config_api 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.iml.apis.data_interactive_api.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.iml.apis.data\_interactive\_api module 2 | ================================================ 3 | 4 | .. automodule:: xyzspaces.iml.apis.data_interactive_api 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.iml.apis.lookup_api.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.iml.apis.lookup\_api module 2 | ===================================== 3 | 4 | .. automodule:: xyzspaces.iml.apis.lookup_api 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.iml.apis.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.iml.apis package 2 | ========================== 3 | 4 | .. automodule:: xyzspaces.iml.apis 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | 10 | Submodules 11 | ---------- 12 | 13 | .. toctree:: 14 | :maxdepth: 4 15 | 16 | xyzspaces.iml.apis.aaa_oauth2_api 17 | xyzspaces.iml.apis.api 18 | xyzspaces.iml.apis.data_config_api 19 | xyzspaces.iml.apis.data_interactive_api 20 | xyzspaces.iml.apis.lookup_api 21 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.iml.auth.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.iml.auth module 2 | ========================= 3 | 4 | .. automodule:: xyzspaces.iml.auth 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.iml.catalog.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.iml.catalog module 2 | ============================ 3 | 4 | .. automodule:: xyzspaces.iml.catalog 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.iml.credentials.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.iml.credentials module 2 | ================================ 3 | 4 | .. automodule:: xyzspaces.iml.credentials 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.iml.exceptions.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.iml.exceptions module 2 | =============================== 3 | 4 | .. automodule:: xyzspaces.iml.exceptions 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.iml.layer.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.iml.layer module 2 | ========================== 3 | 4 | .. automodule:: xyzspaces.iml.layer 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.iml.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.iml package 2 | ===================== 3 | 4 | .. automodule:: xyzspaces.iml 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | 10 | Submodules 11 | ---------- 12 | 13 | .. toctree:: 14 | :maxdepth: 4 15 | 16 | xyzspaces.iml.auth 17 | xyzspaces.iml.catalog 18 | xyzspaces.iml.credentials 19 | xyzspaces.iml.exceptions 20 | xyzspaces.iml.layer 21 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.logconf.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.logconf module 2 | ======================== 3 | 4 | .. automodule:: xyzspaces.logconf 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.rst: -------------------------------------------------------------------------------- 1 | xyzspaces package 2 | ================= 3 | 4 | .. automodule:: xyzspaces 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | 10 | Subpackages 11 | ----------- 12 | 13 | .. toctree:: 14 | :maxdepth: 4 15 | 16 | xyzspaces.config 17 | xyzspaces.datasets 18 | xyzspaces.iml 19 | xyzspaces.iml.apis 20 | 21 | Submodules 22 | ---------- 23 | 24 | .. toctree:: 25 | :maxdepth: 4 26 | 27 | xyzspaces.__version__ 28 | xyzspaces.apis 29 | xyzspaces.auth 30 | xyzspaces._compact 31 | xyzspaces.curl 32 | xyzspaces.exceptions 33 | xyzspaces.logconf 34 | xyzspaces.spaces 35 | xyzspaces.tools 36 | xyzspaces.utils -------------------------------------------------------------------------------- /docs/source/xyzspaces.spaces.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.spaces module 2 | ======================= 3 | 4 | .. automodule:: xyzspaces.spaces 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.tools.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.tools module 2 | ====================== 3 | 4 | .. automodule:: xyzspaces.tools 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /docs/source/xyzspaces.utils.rst: -------------------------------------------------------------------------------- 1 | xyzspaces.utils module 2 | ====================== 3 | 4 | .. automodule:: xyzspaces.utils 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :private-members: 9 | -------------------------------------------------------------------------------- /images/building_numbers.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/xyz-spaces-python/fe6bcb0ee5131798be892be2a25e023d2c47b2b4/images/building_numbers.gif -------------------------------------------------------------------------------- /images/example_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/xyz-spaces-python/fe6bcb0ee5131798be892be2a25e023d2c47b2b4/images/example_map.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Core dependencies 2 | backoff>=1.10.0 3 | geojson 4 | requests 5 | ijson>=3.1.1 6 | pyhocon 7 | requests-oauthlib 8 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | # Development dependencies, use pip install -e .[dev] 2 | black[jupyter]>=21.8b0 3 | codecov 4 | coverage 5 | mypy 6 | proclamation 7 | proxy.py 8 | pytest>=5.4.3 9 | pytest-cov 10 | pytest-mypy 11 | pytest-rerunfailures 12 | Sphinx>=2.4.0 13 | sphinx-rtd-theme 14 | sphinx-thebe>=0.0.8 15 | sphinx-copybutton 16 | twine 17 | pytest-flake8 18 | isort 19 | darglint 20 | # mypy new version requires below 21 | types-requests 22 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Scripts 2 | 3 | The scripts in this directory are usually run on CI/CD, but some can also be useful 4 | outside that context. 5 | 6 | - `build_docs.sh` to create the documentation. 7 | - `check_xyz_platform.py` runs a smoke test to check if the XYZ platform is alive and kicking. 8 | - `walk_spaces.py` can serve as a template for building a maintenance script walking over all spaces. 9 | -------------------------------------------------------------------------------- /scripts/build_docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright (C) 2019-2021 HERE Europe B.V. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | # License-Filename: LICENSE 19 | 20 | # If the search path in conf.py is not set-up this will inspect the installed 21 | # xyzspaces package, not the local code in the xyzspaces subfolder! 22 | 23 | export DEST=docs/source 24 | rm -rf $DEST/_build 25 | rm -rf $DEST/_static 26 | rm -rf $DEST/_templates 27 | 28 | # Just creating conf.py, Makefile and make.bat once, hence commenting below. 29 | 30 | sphinx-apidoc --private --separate --module-first --full -o $DEST xyzspaces 31 | sphinx-build -b html -D html_theme=sphinx_rtd_theme $DEST $DEST/_build/html 32 | -------------------------------------------------------------------------------- /scripts/check_xyz_platform.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # Copyright (C) 2019-2021 HERE Europe B.V. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | # License-Filename: LICENSE 19 | 20 | """Run a quick smoke test to check if the XYZ platform is alive and kicking.""" 21 | 22 | import requests 23 | 24 | url = "https://xyz.api.here.com/hub" 25 | try: 26 | assert 200 <= requests.get(url).status_code < 300 27 | print("XYZ platform is alive and kicking! :)") 28 | except AssertionError: 29 | print("XYZ platform is not feeling well right now. :(") 30 | raise 31 | -------------------------------------------------------------------------------- /scripts/walk_spaces.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # Copyright (C) 2019-2021 HERE Europe B.V. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | # License-Filename: LICENSE 19 | 20 | """ 21 | This tool "template" can help perform some maintenance work on XYZ spaces. 22 | 23 | In its current form this only walks over all spaces available for the token 24 | found in the environment variable XYZ_TOKEN and prints the space ID, number 25 | of features inside and space title. It can be easily modified to perform 26 | simple maintenance tasks like clean-up no longer needer XYZ spaces, etc. 27 | """ 28 | 29 | from xyzspaces.apis import HubApi 30 | 31 | 32 | def walk_spaces(): 33 | """Walk over all spaces and do something to them...""" 34 | # Uses credentials from XYZ_TOKEN env. variable. 35 | api = HubApi() 36 | 37 | for i, space in enumerate(api.get_spaces()): 38 | id, title = space["id"], space["title"] 39 | count = api.get_space_count(space_id=id)["count"] 40 | # if title.find("Testing") >= 0: 41 | # if count > 0: 42 | # api.delete_space(space_id=id) 43 | print(i, id, count, title) 44 | 45 | 46 | if __name__ == "__main__": 47 | walk_spaces() 48 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [metadata] 5 | name = xyzspaces 6 | author = HERE Europe B.V. 7 | author_email = XYZSPACES_PYTHON_SUPPORT@here.com 8 | home-page = https://github.com/heremaps/xyz-spaces-python 9 | description = Manage your XYZ Hub server or HERE Data Hub from Python 10 | long-description = file: README.md 11 | license = Apache-2.0 12 | license_files = 13 | LICENSE 14 | platform = any 15 | keywords = 16 | XYZ 17 | API 18 | REST 19 | Client 20 | Geospatial 21 | classifiers = 22 | Development Status :: 4 - Beta 23 | Intended Audience :: Developers 24 | Topic :: Software Development :: Libraries 25 | License :: OSI Approved :: Apache Software License 26 | Operating System :: OS Independent 27 | Programming Language :: Python :: 3 28 | Programming Language :: Python :: 3 :: Only 29 | Programming Language :: Python :: 3.7 30 | Programming Language :: Python :: 3.8 31 | Programming Language :: Python :: 3.9 32 | project_urls = 33 | Bug Tracker = https://github.com/heremaps/xyz-spaces-python/issues 34 | Source = https://github.com/heremaps/xyz-spaces-python 35 | 36 | [mypy] 37 | ignore_missing_imports = True 38 | 39 | [flake8] 40 | max-line-length = 90 41 | exclude = .git,__pycache__,doc/,docs/,build/,dist/,archive/ 42 | ignore = E203,W503,E231 43 | 44 | [isort] 45 | line_length = 90 46 | 47 | [options] 48 | python_requires >= 3.6 49 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """Project setup file.""" 19 | 20 | from codecs import open 21 | from os import path 22 | 23 | from setuptools import find_packages, setup 24 | 25 | here = path.abspath(path.dirname(__file__)) 26 | 27 | # Get the core dependencies and installs 28 | with open(path.join(here, "requirements.txt"), encoding="utf-8") as f: 29 | all_reqs = f.read().split("\n") 30 | 31 | install_requires = [x.strip() for x in all_reqs if "git+" not in x] 32 | dependency_links = [ 33 | x.strip().replace("git+", "") for x in all_reqs if x.startswith("git+") 34 | ] 35 | 36 | # Get dev dependencies 37 | with open(path.join(here, "requirements_dev.txt"), encoding="utf-8") as f: 38 | dev_reqs = f.read().strip().split("\n") 39 | 40 | # Extra dependencies 41 | 42 | geo = ["geopandas", "turfpy>=0.0.3", "geobuf"] 43 | 44 | extras_require = {"dev": dev_reqs, "geo": geo} 45 | 46 | packages = find_packages(exclude=["docs", "tests*"]) 47 | 48 | version = {} 49 | with open('{}/__version__.py'.format(packages[0])) as f: 50 | exec(f.read(), version) 51 | 52 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 53 | long_description = f.read() 54 | 55 | download_url = ( 56 | "https://github.com/heremaps/xyz-spaces-python" 57 | "/archive/" + version['__version__'] + ".zip" 58 | ) 59 | 60 | setup( 61 | # download_url=download_url, 62 | packages=packages, 63 | version=version['__version__'], 64 | include_package_data=True, 65 | install_requires=install_requires, 66 | dependency_links=dependency_links, 67 | # scripts=["bin/xyzspaces"], 68 | extras_require=extras_require, 69 | long_description=long_description, 70 | long_description_content_type='text/markdown', 71 | ) 72 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/xyz-spaces-python/fe6bcb0ee5131798be892be2a25e023d2c47b2b4/tests/__init__.py -------------------------------------------------------------------------------- /tests/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/xyz-spaces-python/fe6bcb0ee5131798be892be2a25e023d2c47b2b4/tests/config/__init__.py -------------------------------------------------------------------------------- /tests/config/test_config.py: -------------------------------------------------------------------------------- 1 | """This module tests custom configurations for xyzspaces.""" 2 | 3 | import os 4 | from pathlib import Path 5 | 6 | from xyzspaces.config.default import DEFAULT_CONFIG, XYZConfig 7 | 8 | 9 | def test_default_config(): 10 | """Test default config.""" 11 | xyzconfig = XYZConfig.from_default() 12 | assert xyzconfig.config["credentials"]["XYZ_TOKEN"] == os.environ.get("XYZ_TOKEN") 13 | assert xyzconfig.config["credentials"]["HERE_USER"] == os.environ.get("HERE_USER") 14 | assert xyzconfig.config["credentials"]["HERE_PASSWORD"] == os.environ.get( 15 | "HERE_PASSWORD" 16 | ) 17 | assert xyzconfig.config["url"] == "https://xyz.api.here.com" 18 | assert ( 19 | xyzconfig.config["http_headers"]["Authorization"] 20 | == f"Bearer {os.environ.get('XYZ_TOKEN')}" 21 | ) 22 | assert xyzconfig.config["http_headers"]["Content-Type"] == "application/geo+json" 23 | 24 | 25 | def test_config_object(): 26 | """Test configurations using config object.""" 27 | xyzconfig = XYZConfig(**DEFAULT_CONFIG) 28 | assert xyzconfig.config["credentials"]["XYZ_TOKEN"] == os.environ.get("XYZ_TOKEN") 29 | assert xyzconfig.config["credentials"]["HERE_USER"] == os.environ.get("HERE_USER") 30 | assert xyzconfig.config["credentials"]["HERE_PASSWORD"] == os.environ.get( 31 | "HERE_PASSWORD" 32 | ) 33 | assert xyzconfig.config["url"] == "https://xyz.api.here.com" 34 | assert ( 35 | xyzconfig.config["http_headers"]["Authorization"] 36 | == f"Bearer {os.environ.get('XYZ_TOKEN')}" 37 | ) 38 | assert xyzconfig.config["http_headers"]["Content-Type"] == "application/geo+json" 39 | 40 | 41 | def test_config_from_file(): 42 | """Test configurations using file.""" 43 | root = Path(__file__).parent.parent.parent 44 | file_path = ( 45 | root / Path("docs") / Path("notebooks") / Path("data") / "xyz_configuration.conf" 46 | ) 47 | xyzconfig = XYZConfig.from_file(file_path) 48 | assert xyzconfig.config["credentials"]["XYZ_TOKEN"] == "MY-XYZ-TOKEN" 49 | assert xyzconfig.config["url"] == "https://xyz.api.here.com" 50 | assert xyzconfig.config["http_headers"]["Authorization"] == "Bearer MY-XYZ-TOKEN" 51 | assert xyzconfig.config["http_headers"]["Content-Type"] == "application/geo+json" 52 | -------------------------------------------------------------------------------- /tests/curl/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/xyz-spaces-python/fe6bcb0ee5131798be892be2a25e023d2c47b2b4/tests/curl/__init__.py -------------------------------------------------------------------------------- /tests/curl/test_curl.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """ 19 | Module for testing xyzspaces.curl. 20 | 21 | Here we don't generate any temporary spaces. 22 | """ 23 | 24 | import datetime 25 | import subprocess 26 | 27 | import pytest 28 | import requests 29 | 30 | import xyzspaces.curl as curl 31 | 32 | try: 33 | subprocess.check_output(["which", "curl"]) 34 | HAVE_CURL = True 35 | except subprocess.CalledProcessError: 36 | HAVE_CURL = False 37 | 38 | 39 | @pytest.mark.skipif(False, reason="Not yet implemented.") 40 | @pytest.mark.skipif(not HAVE_CURL, reason="No curl command found.") 41 | def test_new_curl_command_list(): 42 | """Test compare results for new curl command vs. requests using same interface.""" 43 | url = "https://xkcd.com/552/info.0.json" 44 | curl_cmd = curl.get(url=url) 45 | curl_res = curl.execute(curl_cmd) 46 | reqs_res = requests.get(url=url) 47 | assert curl_res.ok == reqs_res.ok 48 | assert curl_res.content == reqs_res.content 49 | resp = curl.execute(["dummy"]) 50 | assert resp.status_code == 500 51 | assert not resp.content 52 | 53 | 54 | def test_curl_command_str(): 55 | """Test building curl command as string.""" 56 | cmd = curl.get(url="https://xkcd.com/552/info.0.json") 57 | assert " ".join(cmd) == "curl --request GET https://xkcd.com/552/info.0.json" 58 | 59 | 60 | def test_curl_command_list(): 61 | """Test building curl command as list.""" 62 | cmd = curl.get(url="https://xkcd.com/552/info.0.json") 63 | assert cmd == [ 64 | "curl", 65 | "--request", 66 | "GET", 67 | "https://xkcd.com/552/info.0.json", 68 | ] 69 | 70 | 71 | def test_curl_command_data_params(): 72 | """Test building curl command for parameters data and proxy.""" 73 | cmd = curl.post( 74 | url="https://xkcd.com/552/info.0.json", 75 | data={"dummy": 2, "type": "test"}, 76 | headers={"Date": datetime.datetime.today().strftime("%Y-%m-%d")}, 77 | proxies={"proxy1": "http://user:password@example.proxy.here.com" ":8888"}, 78 | ) 79 | assert cmd == [ 80 | "curl", 81 | "--request", 82 | "POST", 83 | "https://xkcd.com/552/info.0.json", 84 | "--header", 85 | f'Date: {datetime.datetime.today().strftime("%Y-%m-%d")}', 86 | "--header", 87 | "Content-Type: application/x-www-form-urlencoded", 88 | "--data", 89 | "dummy=2&type=test", 90 | "--proxy", 91 | "http://user:password@example.proxy.here.com:8888", 92 | ] 93 | -------------------------------------------------------------------------------- /tests/data/example.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | Example gpx 4 | 5 | 2372 6 | LAGORETICO 7 | 8 | Example gpx1 9 | 2376 10 | 2375 11 | 2372 12 | 2373 13 | 2374 14 | 2375 15 | 2376 16 | 17 | -------------------------------------------------------------------------------- /tests/data/stations-32633.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/xyz-spaces-python/fe6bcb0ee5131798be892be2a25e023d2c47b2b4/tests/data/stations-32633.zip -------------------------------------------------------------------------------- /tests/data/stations.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/xyz-spaces-python/fe6bcb0ee5131798be892be2a25e023d2c47b2b4/tests/data/stations.zip -------------------------------------------------------------------------------- /tests/data/test.csv: -------------------------------------------------------------------------------- 1 | policyID,statecode,county,eq_site_limit,hu_site_limit,fl_site_limit,fr_site_limit,tiv_2011,tiv_2012,eq_site_deductible,hu_site_deductible,fl_site_deductible,fr_site_deductible,latitude,longitude,line,construction,point_granularity 119736,FL,CLAY COUNTY,498960,498960,498960,498960,498960,792148.9,0,9979.2,0,0,30.102261,-81.711777,Residential,Masonry,1 448094,FL,CLAY COUNTY,1322376.3,1322376.3,1322376.3,1322376.3,1322376.3,1438163.57,0,0,0,0,30.063936,-81.707664,Residential,Masonry,3 206893,FL,CLAY COUNTY,190724.4,190724.4,190724.4,190724.4,190724.4,192476.78,0,0,0,0,30.089579,-81.700455,Residential,Wood,1 333743,FL,CLAY COUNTY,0,79520.76,0,0,79520.76,86854.48,0,0,0,0,30.063236,-81.707703,Residential,Wood,3 -------------------------------------------------------------------------------- /tests/data/test.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "id" : "test_geojson_1", 6 | "type": "Feature", 7 | "properties": { 8 | 9 | }, 10 | "geometry": { 11 | "type": "Point", 12 | "coordinates": [ 13 | -4.187271595001221, 14 | 40.95480471183075 15 | ] 16 | } 17 | }, 18 | { 19 | "id" : "test_geojson_2", 20 | "type": "Feature", 21 | "properties": { 22 | 23 | }, 24 | "geometry": { 25 | "type": "Point", 26 | "coordinates": [ 27 | -4.186810255050659, 28 | 40.954148389459085 29 | ] 30 | } 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /tests/data/test.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/xyz-spaces-python/fe6bcb0ee5131798be892be2a25e023d2c47b2b4/tests/data/test.pbf -------------------------------------------------------------------------------- /tests/data/test.wkt: -------------------------------------------------------------------------------- 1 | GEOMETRYCOLLECTION (LINESTRING (30 10, 10 30, 40 40), MULTILINESTRING ((10 10, 20 20, 10 40),(40 40, 30 30, 40 20, 30 10)), MULTIPOLYGON (((30 20, 10 40, 45 40, 30 20)),((15 5, 40 10, 10 20, 5 10, 15 5))), MULTIPOINT ((10 40), (40 30), (20 20), (30 10)), POLYGON ((30 10, 10 20, 20 40, 40 40, 30 10)), POLYGON ((35 10, 10 20, 15 40, 45 45, 35 10),(20 30, 35 35, 30 20, 20 30))) -------------------------------------------------------------------------------- /tests/data/test_altitude.csv: -------------------------------------------------------------------------------- 1 | policyID,statecode,county,eq_site_limit,hu_site_limit,fl_site_limit,fr_site_limit,tiv_2011,tiv_2012,eq_site_deductible,hu_site_deductible,fl_site_deductible,fr_site_deductible,latitude,longitude,line,construction,point_granularity,altitude 2 | 119736,FL,CLAY COUNTY,498960,498960,498960,498960,498960,792148.9,0,9979.2,0,0,30.102261,-81.711777,Residential,Masonry,1,1 3 | 448094,FL,CLAY COUNTY,1322376.3,1322376.3,1322376.3,1322376.3,1322376.3,1438163.57,0,0,0,0,30.063936,-81.707664,Residential,Masonry,3,1 4 | 206893,FL,CLAY COUNTY,190724.4,190724.4,190724.4,190724.4,190724.4,192476.78,0,0,0,0,30.089579,-81.700455,Residential,Wood,1,1 5 | 333743,FL,CLAY COUNTY,0,79520.76,0,0,79520.76,86854.48,0,0,0,0,30.063236,-81.707703,Residential,Wood,3,1 -------------------------------------------------------------------------------- /tests/data/test_altitude.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "id" : "test_geojson_1", 6 | "type": "Feature", 7 | "properties": { 8 | 9 | }, 10 | "geometry": { 11 | "type": "Point", 12 | "coordinates": [ 13 | -4.187271595001221, 14 | 40.95480471183075, 15 | 3 16 | ] 17 | } 18 | }, 19 | { 20 | "id" : "test_geojson_2", 21 | "type": "Feature", 22 | "properties": { 23 | 24 | }, 25 | "geometry": { 26 | "type": "Point", 27 | "coordinates": [ 28 | -4.186810255050659, 29 | 40.954148389459085, 30 | 9 31 | ] 32 | } 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /tests/hub/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/xyz-spaces-python/fe6bcb0ee5131798be892be2a25e023d2c47b2b4/tests/hub/__init__.py -------------------------------------------------------------------------------- /tests/hub/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """Module for providing test fixtures for the Hub API tests.""" 19 | 20 | from time import sleep 21 | 22 | import pytest 23 | 24 | from xyzspaces.apis import HubApi 25 | from xyzspaces.config.default import XYZConfig 26 | from xyzspaces.datasets import get_chicago_parks_data, get_countries_data 27 | 28 | 29 | @pytest.fixture() 30 | def api(): 31 | """Create shared XYZ Hub Api instance as a pytest fixture.""" 32 | api = HubApi(config=XYZConfig.from_default()) 33 | return api 34 | 35 | 36 | @pytest.fixture() 37 | def space_id(): 38 | """Create shared XYZ space with countries data as a pytest fixture.""" 39 | api = HubApi(config=XYZConfig.from_default()) 40 | 41 | # setup, create temporary space 42 | res = api.post_space( 43 | data={ 44 | "title": "Testing xyzspaces", 45 | "description": "Temporary space containing countries data.", 46 | } 47 | ) 48 | space_id = res["id"] 49 | 50 | # add features to space 51 | gj_countries = get_countries_data() 52 | sleep(0.5) 53 | api.put_space_features(space_id=space_id, data=gj_countries) 54 | 55 | yield space_id 56 | 57 | # now teardown (delete temporary space) 58 | api.delete_space(space_id=space_id) 59 | 60 | 61 | @pytest.fixture() 62 | def point_space_id(): 63 | """Create shared XYZ space with Chicago Parks data.""" 64 | api = HubApi(config=XYZConfig.from_default()) 65 | # setup, create temporary space 66 | res = api.post_space( 67 | data={ 68 | "title": "Testing xyzspaces", 69 | "description": "Temporary space containing Chicago Parks data", 70 | } 71 | ) 72 | space_id = res["id"] 73 | gj_chicago_parks = get_chicago_parks_data() 74 | sleep(0.5) 75 | api.put_space_features(space_id=space_id, data=gj_chicago_parks) 76 | yield space_id 77 | 78 | # now teardown (delete temporary space) 79 | api.delete_space(space_id=space_id) 80 | -------------------------------------------------------------------------------- /tests/hub/test_aliases.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """Module for testing various HTTP method aliases.""" 19 | 20 | import pytest 21 | 22 | from xyzspaces.utils import get_xyz_token 23 | 24 | XYZ_TOKEN = get_xyz_token() 25 | 26 | 27 | @pytest.mark.skipif(not XYZ_TOKEN, reason="No token found.") 28 | def test_get_alias(api, space_id): 29 | """Get space statistics.""" 30 | path = f"/hub/spaces/{space_id}/statistics" 31 | stats = api.get(path=path).json() 32 | assert stats["type"] == "StatisticsResponse" 33 | -------------------------------------------------------------------------------- /tests/hub/test_datasets.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """Module for testing example default datasets.""" 19 | 20 | from pathlib import Path 21 | 22 | import pytest 23 | 24 | import xyzspaces 25 | from xyzspaces.datasets import get_countries_data 26 | from xyzspaces.utils import get_xyz_token 27 | 28 | XYZ_TOKEN = get_xyz_token() 29 | 30 | 31 | @pytest.mark.skipif(not XYZ_TOKEN, reason="No token found.") 32 | def test_load_countries(): 33 | """Load countries dataset.""" 34 | gj_countries = get_countries_data() 35 | keys = gj_countries.keys() 36 | assert "type" in keys 37 | assert "features" in keys 38 | assert len(gj_countries["features"]) == 180 39 | 40 | datasets_home = Path(xyzspaces.datasets.__file__).parent 41 | url_countries = ( 42 | "https://raw.githubusercontent.com" 43 | "/johan/world.geo.json/master/countries.geo.json" 44 | ) 45 | fn_countries = datasets_home / Path(url_countries).name 46 | try: 47 | fn_countries.unlink() 48 | except OSError: 49 | pass 50 | assert not fn_countries.exists() 51 | 52 | gj_countries = get_countries_data() 53 | -------------------------------------------------------------------------------- /tests/hub/test_exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """Module for testing API exceptions.""" 19 | 20 | 21 | import pytest 22 | import requests 23 | 24 | from xyzspaces.exceptions import ApiError 25 | 26 | 27 | def test_exception_requests_inalid(api): 28 | """Raise exception via requests as response for invalid endpoint.""" 29 | with pytest.raises(ApiError) as execinfo: 30 | url = f"{api.xyzconfig.config['url']}/hub/invalid" 31 | resp = requests.get(url) 32 | raise ApiError(resp) 33 | resp = execinfo.value.args[0] 34 | assert resp.status_code == 404 35 | assert resp.reason == "Not Found" 36 | 37 | 38 | def test_exception_requests_inalid_str(api): 39 | """Test raised exception as string follow expected pattern.""" 40 | with pytest.raises(ApiError) as execinfo: 41 | url = f"{api.xyzconfig.config['url']}/hub/invalid" 42 | resp = requests.get(url) 43 | raise ApiError(resp) 44 | assert str(execinfo.value).startswith('404, Not Found, {"type":"error",') 45 | 46 | 47 | def test_exception_response_invalid(api): 48 | """Raise exception via API as response for invalid endpoint.""" 49 | with pytest.raises(ApiError) as execinfo: 50 | api.get(path="/hub/invalid") 51 | resp = execinfo.value.args[0] 52 | assert resp.status_code == 404 53 | assert resp.reason == "Not Found" 54 | assert resp.json()["message"] == "The requested resource does not exist." 55 | -------------------------------------------------------------------------------- /tests/hub/test_logconf.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """This module tests logging configuration.""" 19 | 20 | import logging 21 | import os 22 | from pathlib import Path 23 | 24 | import pytest 25 | 26 | from xyzspaces.exceptions import ApiError 27 | from xyzspaces.logconf import setup_logging 28 | 29 | 30 | def test_setup_logging_file_invalid(caplog): 31 | """Test logging setup when logging config file is not present.""" 32 | setup_logging(default_path="dummy", default_level=logging.INFO) 33 | with caplog.at_level(logging.INFO): 34 | logger = logging.getLogger(__name__) 35 | logger.info("Testing logging on standard output") 36 | assert "Testing logging on standard output" in caplog.text 37 | 38 | 39 | def test_setup_logging_json(api): 40 | """Test logging setup using json config file.""" 41 | os.environ["XYZ_LOG_CONFIG"] = str( 42 | Path(__file__).parents[2] / "xyzspaces" / "config" / "logconfig.json" 43 | ) 44 | setup_logging() 45 | space_id = "dummy-111" 46 | with pytest.raises(ApiError): 47 | api.get_space(space_id=space_id) 48 | log_path = Path(__file__).parents[2] / "xyz.log" 49 | assert log_path.exists() 50 | assert log_path.stat().st_size > 0 51 | with open(log_path) as fh: 52 | lines = fh.read() 53 | assert " - ERROR - status code: 404, response:" in lines 54 | -------------------------------------------------------------------------------- /tests/hub/test_proxy.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """Module for testing proxies for the XYZ API class.""" 19 | 20 | import multiprocessing 21 | import os 22 | import socket 23 | import subprocess 24 | import time 25 | from contextlib import closing 26 | 27 | import pytest 28 | 29 | from xyzspaces.utils import get_xyz_token 30 | 31 | XYZ_TOKEN = get_xyz_token() 32 | 33 | 34 | def find_free_port(): 35 | """Find and return a free port to use on this machine.""" 36 | with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: 37 | s.bind(("localhost", 0)) 38 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 39 | return s.getsockname()[1] 40 | 41 | 42 | def run_proxy(port: int): 43 | """Start a HTTP proxy.""" 44 | cmd = f"proxy.py --port {port}".split() 45 | subprocess.check_output(cmd) 46 | 47 | 48 | @pytest.fixture() 49 | def proxy_port(): 50 | """Run an HTTP proxy as a pytest fixture.""" 51 | port = find_free_port() 52 | p = multiprocessing.Process(target=run_proxy, args=[port]) 53 | p.daemon = True 54 | p.start() 55 | time.sleep(1) 56 | 57 | yield port 58 | 59 | # now teardown (terminating temporary proxy) 60 | p.terminate() 61 | time.sleep(1) 62 | 63 | 64 | @pytest.mark.skipif(not XYZ_TOKEN, reason="No token found.") 65 | def test_no_proxy(api): 66 | """Get the hub info with no proxy set.""" 67 | hub = api.get_hub() 68 | assert "reporter" in hub 69 | assert "status" in hub 70 | assert "schemaVersion" in hub 71 | 72 | 73 | @pytest.mark.skipif(not XYZ_TOKEN, reason="No token found.") 74 | @pytest.mark.skipif( 75 | True, 76 | reason="Needs to set/use/expose a fixed port on Docker for CI/CD, first...", 77 | ) 78 | def test_living_proxy(api, proxy_port): 79 | """Get the hub info with specified and running proxy.""" 80 | os.environ["HTTP_PROXY"] = f"http://localhost:{proxy_port}" 81 | os.environ["HTTPS_PROXY"] = f"https://localhost:{proxy_port}" 82 | try: 83 | hub = api.get_hub() 84 | finally: 85 | del os.environ["HTTP_PROXY"] 86 | del os.environ["HTTPS_PROXY"] 87 | assert "reporter" in hub 88 | assert "status" in hub 89 | assert "schemaVersion" in hub 90 | -------------------------------------------------------------------------------- /tests/hub/test_search.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """ 19 | Module for testing various endpoints quickly to increase test code coverage. 20 | 21 | These tests should be spread over other modules, soon... 22 | """ 23 | 24 | import pytest 25 | 26 | from xyzspaces.utils import get_xyz_token 27 | 28 | XYZ_TOKEN = get_xyz_token() 29 | 30 | 31 | @pytest.mark.skipif(not XYZ_TOKEN, reason="No token found.") 32 | def test_get_space_search(api, space_id): 33 | """Get all features from space by searching them.""" 34 | feats = api.get_space_search(space_id=space_id) 35 | assert feats["type"] == "FeatureCollection" 36 | assert len(feats["features"]) > 0 37 | 38 | feats = api.get_space_search(space_id=space_id, limit=10) 39 | assert feats["type"] == "FeatureCollection" 40 | assert len(feats["features"]) <= 10 41 | 42 | feats = api.get_space_search(space_id=space_id, tags=["non-existing"]) 43 | assert feats["type"] == "FeatureCollection" 44 | assert len(feats["features"]) == 0 45 | 46 | feats = api.get_space_search(space_id=space_id, params={"f.id=lte": "AGO"}) 47 | assert len(feats["features"]) == 2 48 | 49 | feats = api.get_space_search(space_id=space_id, params={"f.id": "IND"}) 50 | assert feats["features"][0]["id"] == "IND" 51 | 52 | feats = api.get_space_search( 53 | space_id=space_id, selection=["p.color"], params={"f.id": "IND"} 54 | ) 55 | assert feats["features"][0]["properties"] == {} 56 | -------------------------------------------------------------------------------- /tests/iml/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | -------------------------------------------------------------------------------- /tests/iml/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """Module for providing test fixtures for the IML tests.""" 19 | 20 | import os 21 | from collections import namedtuple 22 | 23 | import pytest 24 | 25 | from xyzspaces import IML 26 | 27 | HERE_USER_ID = os.environ.get("HERE_USER_ID") 28 | HERE_CLIENT_ID = os.environ.get("HERE_CLIENT_ID") 29 | HERE_ACCESS_KEY_ID = os.environ.get("HERE_ACCESS_KEY_ID") 30 | HERE_ACCESS_KEY_SECRET = os.environ.get("HERE_ACCESS_KEY_SECRET") 31 | 32 | 33 | @pytest.fixture() 34 | def read_layer(): 35 | """Fixture for all read operations on interactive map layer.""" 36 | iml = IML.from_catalog_hrn_and_layer_id( 37 | catalog_hrn="hrn:here:data::olp-here:catalog-to-test-in-ci-don-not-delete", 38 | layer_id="countries", 39 | ) 40 | return iml.layer 41 | 42 | 43 | def env_setup_done(): 44 | env_vars_present = all( 45 | v is not None 46 | for v in [ 47 | HERE_USER_ID, 48 | HERE_CLIENT_ID, 49 | HERE_ACCESS_KEY_ID, 50 | HERE_ACCESS_KEY_SECRET, 51 | ] 52 | ) 53 | return env_vars_present 54 | 55 | 56 | def get_mock_response(status_code: int, reason: str, text: str): 57 | """ 58 | Return mock response. 59 | 60 | :param status_code: An int representing status_code. 61 | :param reason: A string to represent reason. 62 | :param text: A string to represent text. 63 | :return: MockResponse object. 64 | """ 65 | MockResponse = namedtuple("MockResponse", ["status_code", "reason", "text"]) 66 | mock_response = MockResponse(status_code, reason, text) 67 | return mock_response 68 | -------------------------------------------------------------------------------- /tests/iml/data/dummy_credentials.properties: -------------------------------------------------------------------------------- 1 | here.user.id = dummy_user_id 2 | here.client.id = dummy_client_id 3 | here.access.key.id = dummy_access_key_id 4 | here.access.key.secret = dummy_access_key_secret 5 | here.token.endpoint.url = dummy_token_endpoint -------------------------------------------------------------------------------- /tests/iml/test_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | """Test api module.""" 18 | import pytest 19 | 20 | from tests.iml.conftest import get_mock_response 21 | from xyzspaces.iml.apis.api import Api 22 | from xyzspaces.iml.exceptions import ( 23 | AuthenticationException, 24 | PayloadTooLargeException, 25 | RequestEntityTooLargeException, 26 | TooManyRequestsException, 27 | ) 28 | 29 | 30 | def test_raise_response_exception(): 31 | reason = "This is mock reason" 32 | text = "This is mock text" 33 | mock_response = get_mock_response(513, reason, text) 34 | with pytest.raises(PayloadTooLargeException): 35 | Api.raise_response_exception(mock_response) 36 | 37 | mock_response = get_mock_response(401, reason, text) 38 | with pytest.raises(AuthenticationException): 39 | Api.raise_response_exception(mock_response) 40 | 41 | mock_response = get_mock_response(413, reason, text) 42 | with pytest.raises(RequestEntityTooLargeException): 43 | Api.raise_response_exception(mock_response) 44 | 45 | mock_response = get_mock_response(400, reason, text) 46 | with pytest.raises(Exception): 47 | Api.raise_response_exception(mock_response) 48 | 49 | mock_response = get_mock_response(429, reason, text) 50 | with pytest.raises(TooManyRequestsException): 51 | Api.raise_response_exception(mock_response) 52 | -------------------------------------------------------------------------------- /tests/iml/test_credentials.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | """Test credentials module.""" 18 | import tempfile 19 | from pathlib import Path 20 | 21 | import pytest 22 | 23 | from xyzspaces.iml.credentials import Credentials 24 | from xyzspaces.iml.exceptions import ConfigException 25 | 26 | 27 | def test_from_credentials_file(): 28 | """Test credentials from file.""" 29 | file_path = ( 30 | Path(__file__).parent / Path("data") / Path("dummy_credentials.properties") 31 | ) 32 | cred = Credentials.from_credentials_file(file_path) 33 | assert cred.cred_properties["user"] == "dummy_user_id" 34 | assert cred.cred_properties["client"] == "dummy_client_id" 35 | assert cred.cred_properties["key"] == "dummy_access_key_id" 36 | assert cred.cred_properties["secret"] == "dummy_access_key_secret" 37 | assert cred.cred_properties["endpoint"] == "dummy_token_endpoint" 38 | with pytest.raises(ConfigException): 39 | with tempfile.NamedTemporaryFile() as tmp: 40 | _ = Credentials.from_credentials_file(tmp.name) 41 | -------------------------------------------------------------------------------- /tests/iml/test_exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | """Test all exceptions.""" 18 | 19 | 20 | import pytest 21 | 22 | from tests.iml.conftest import get_mock_response 23 | from xyzspaces.iml.exceptions import ( 24 | AuthenticationException, 25 | PayloadTooLargeException, 26 | RequestEntityTooLargeException, 27 | TooManyRequestsException, 28 | ) 29 | 30 | 31 | def test_authentication_exception(): 32 | """Test :class:`AuthenticationException`""" 33 | status_code = 401 34 | reason = "This is mock reason" 35 | text = "This is mock text" 36 | mock_response = get_mock_response(status_code, reason, text) 37 | with pytest.raises(AuthenticationException) as execinfo: 38 | raise AuthenticationException(mock_response) 39 | resp = execinfo.value.args[0] 40 | assert resp.status_code == status_code 41 | assert resp.reason == reason 42 | assert resp.text == text 43 | 44 | 45 | def test_payload_too_large_exception(): 46 | """Test :class:`PayloadTooLargeException`""" 47 | status_code = 513 48 | reason = "This is mock reason" 49 | text = "This is mock text" 50 | mock_response = get_mock_response(status_code, reason, text) 51 | with pytest.raises(PayloadTooLargeException) as execinfo: 52 | raise PayloadTooLargeException(mock_response) 53 | resp = execinfo.value.args[0] 54 | assert resp.status_code == status_code 55 | assert resp.reason == reason 56 | assert resp.text == text 57 | 58 | 59 | def test_too_many_requests_exception(): 60 | """Test :class:`TooManyRequestsException`""" 61 | status_code = 429 62 | reason = "This is mock reason" 63 | text = "This is mock text" 64 | mock_response = get_mock_response(status_code, reason, text) 65 | with pytest.raises(TooManyRequestsException) as execinfo: 66 | raise TooManyRequestsException(mock_response) 67 | resp = execinfo.value.args[0] 68 | assert resp.status_code == status_code 69 | assert resp.reason == reason 70 | assert resp.text == text 71 | 72 | 73 | def test_request_entity_too_large_exception(): 74 | """Test :class:`RequestEntityTooLargeException`""" 75 | status_code = 413 76 | reason = "This is mock reason" 77 | text = "This is mock text" 78 | mock_response = get_mock_response(status_code, reason, text) 79 | with pytest.raises(RequestEntityTooLargeException) as execinfo: 80 | raise RequestEntityTooLargeException(mock_response) 81 | resp = execinfo.value.args[0] 82 | assert resp.status_code == status_code 83 | assert resp.reason == reason 84 | assert resp.text == text 85 | -------------------------------------------------------------------------------- /tests/iml/test_iml.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | """This module will test functionality in IML class.""" 18 | 19 | import json 20 | from pathlib import Path 21 | from time import sleep 22 | 23 | import pytest 24 | 25 | from tests.iml.conftest import env_setup_done 26 | from xyzspaces import IML 27 | from xyzspaces.iml.catalog import Catalog 28 | from xyzspaces.iml.credentials import Credentials 29 | from xyzspaces.iml.layer import InteractiveMapLayer 30 | 31 | 32 | @pytest.mark.skipif(not env_setup_done(), reason="Credentials are not setup in env.") 33 | def test_from_catalog_hrn_and_layer_id(): 34 | """Test IML classmethod.""" 35 | hrn = "hrn:here:data::olp-here:catalog-to-test-in-ci-don-not-delete" 36 | layer_id = "countries" 37 | iml = IML.from_catalog_hrn_and_layer_id(catalog_hrn=hrn, layer_id=layer_id) 38 | assert isinstance(iml.catalog, Catalog) 39 | assert isinstance(iml.layer, InteractiveMapLayer) 40 | 41 | 42 | @pytest.mark.skipif(not env_setup_done(), reason="Credentials are not setup in env.") 43 | def test_catalog_lifecycle(): 44 | """This funtion tests catalog lifecycle. 45 | 46 | - Create a new catalod and interactive map layer 47 | - Add features to layer 48 | - remvoe features from layer 49 | - update features from layer 50 | - Delete catalog. 51 | """ 52 | # cleanup before start. just delete catalg if it already exists. 53 | cred = Credentials.from_env() 54 | try: 55 | obj = IML() 56 | obj.delete_catalog( 57 | catalog_hrn="hrn:here:data::olp-here:test-catalog-iml-remove", 58 | credentials=cred, 59 | ) 60 | sleep(5) 61 | except: # noqa: E722 62 | pass 63 | 64 | layer_details = { 65 | "id": "countries-test", 66 | "name": "countries-test", 67 | "summary": "Borders of world countries.", 68 | "description": "Borders of world countries. Test layer for read operations in CI", 69 | "layerType": "interactivemap", 70 | "interactiveMapProperties": {}, 71 | } 72 | iml = IML.new( 73 | catalog_id="test-catalog-iml-remove", 74 | catalog_name="test-catalog-iml-remove", 75 | catalog_summary="This is test catalog used in CI for xyzspaces.", 76 | catalog_description="Test IML functionality in CI for xyzspaces.", 77 | layer_details=layer_details, 78 | credentials=cred, 79 | ) 80 | sleep(2) 81 | root = Path(__file__).parent.parent.parent 82 | file_path = root / Path("xyzspaces") / Path("datasets") / Path("countries.geo.json") 83 | iml.layer.write_features(from_file=file_path) 84 | assert iml.layer.statistics["count"]["value"] == 179 85 | feature = { 86 | "geometry": {"coordinates": [73, 19], "type": "Point"}, 87 | "properties": {}, 88 | "type": "Feature", 89 | "id": "test-delete", 90 | } 91 | iml.layer.write_feature(feature_id="test-delete", data=feature) 92 | sleep(1) 93 | resp = iml.layer.get_feature(feature_id="test-delete") 94 | ft = resp.to_geojson() 95 | assert ft["id"] == "test-delete" 96 | feature["properties"] = {"name": "delete"} 97 | iml.layer.update_feature(feature_id="test-delete", data=feature) 98 | sleep(1) 99 | iml.layer.delete_feature(feature_id="test-delete") 100 | # Add new layer to the catalog. 101 | layer2 = { 102 | "id": "countries-test2", 103 | "name": "countries-test2", 104 | "summary": "Borders of world countries second layer.", 105 | "description": "Borders of world countries.", 106 | "layerType": "interactivemap", 107 | "interactiveMapProperties": {}, 108 | } 109 | iml.add_interactive_map_layer( 110 | catalog_hrn="hrn:here:data::olp-here:test-catalog-iml-remove", 111 | layer_details=layer2, 112 | credentials=cred, 113 | ) 114 | assert iml.layer.id == "countries-test2" 115 | with open(file_path) as fh: 116 | countries_data = json.load(fh) 117 | iml.layer.write_features(features=countries_data) 118 | assert iml.layer.statistics["count"]["value"] == 179 119 | resp = iml.layer.get_features(feature_ids=["IND", "USA", "DEU"]) 120 | features = resp.to_geojson()["features"] 121 | iml.layer.delete_features(feature_ids=["IND", "USA", "DEU"]) 122 | assert iml.layer.statistics["count"]["value"] == 176 123 | iml.layer.write_features(features=features) 124 | sleep(0.5) 125 | assert iml.layer.statistics["count"]["value"] == 179 126 | -------------------------------------------------------------------------------- /tests/iml/test_layer.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | """Tests for layer module.""" 18 | import geopandas 19 | import pytest 20 | from geojson import Feature, FeatureCollection, Point 21 | 22 | from tests.iml.conftest import env_setup_done 23 | from xyzspaces.iml.layer import HexbinClustering, InteractiveMapApiResponse 24 | 25 | # Read operation on layer. 26 | 27 | 28 | @pytest.mark.skipif(not env_setup_done(), reason="Credentials are not setup in env.") 29 | def test_statistics(read_layer): 30 | """Test statistics of interactive map layer.""" 31 | stats = read_layer.statistics 32 | assert stats["count"]["value"] == 179 33 | assert str(read_layer) == "layer_id: countries" 34 | 35 | 36 | @pytest.mark.skipif(not env_setup_done(), reason="Credentials are not setup in env.") 37 | def test_get_feature(read_layer): 38 | """Test get single feature from interactive map layer.""" 39 | int_resp = read_layer.get_feature(feature_id="IND", selection=["name"]) 40 | feature = int_resp.to_geojson() 41 | assert isinstance(feature, Feature) 42 | assert feature["id"] == "IND" 43 | with pytest.raises(NotImplementedError): 44 | int_resp.to_geopandas() 45 | 46 | 47 | @pytest.mark.skipif(not env_setup_done(), reason="Credentials are not setup in env.") 48 | def test_get_features(read_layer): 49 | """Test get multiple features from interactive map layer.""" 50 | feature_ids = ["IND", "DEU", "USA"] 51 | int_resp = read_layer.get_features( 52 | feature_ids=feature_ids, selection=["name"], force_2d=True 53 | ) 54 | fc = int_resp.to_geojson() 55 | assert isinstance(fc, FeatureCollection) 56 | for f in fc["features"]: 57 | assert f["id"] in feature_ids 58 | gdf = int_resp.to_geopandas() 59 | assert isinstance(gdf, geopandas.GeoDataFrame) 60 | with pytest.raises(ValueError): 61 | read_layer.get_features(feature_ids=[]) 62 | 63 | 64 | @pytest.mark.skipif(not env_setup_done(), reason="Credentials are not setup in env.") 65 | def test_search_features(read_layer): 66 | """Test search features.""" 67 | int_resp = read_layer.search_features( 68 | params={"p.name": "India"}, 69 | selection=["name"], 70 | ) 71 | fc = int_resp.to_geojson() 72 | assert isinstance(fc, FeatureCollection) 73 | assert fc["features"][0]["id"] == "IND" 74 | 75 | 76 | @pytest.mark.skipif(not env_setup_done(), reason="Credentials are not setup in env.") 77 | def test_iter_feature(read_layer): 78 | """Test iter features""" 79 | itr = read_layer.iter_features(selection=["name"]) 80 | feature = next(itr) 81 | assert isinstance(feature, Feature) 82 | 83 | 84 | @pytest.mark.skipif(not env_setup_done(), reason="Credentials are not setup in env.") 85 | def test_get_features_in_bounding_box(read_layer): 86 | """Test features in bounding box.""" 87 | clustering = HexbinClustering(absolute_resolution=1) 88 | int_resp = read_layer.get_features_in_bounding_box( 89 | bounds=(68.1766451354, 7.96553477623, 97.4025614766, 35.4940095078), 90 | clustering=clustering, 91 | selection=["name"], 92 | ) 93 | fc = int_resp.to_geojson() 94 | assert isinstance(fc, FeatureCollection) 95 | 96 | 97 | @pytest.mark.skipif(not env_setup_done(), reason="Credentials are not setup in env.") 98 | def test_spatial_search(read_layer): 99 | """Test spatial search.""" 100 | int_resp = read_layer.spatial_search(lng=73, lat=19, radius=1000, selection=["name"]) 101 | fc = int_resp.to_geojson() 102 | assert fc["features"][0]["id"] == "IND" 103 | 104 | 105 | @pytest.mark.skipif(not env_setup_done(), reason="Credentials are not setup in env.") 106 | def test_spatial_search_geometry(read_layer): 107 | """Test spatial search using geometry.""" 108 | pt = Point((73, 19)) 109 | feature = Feature(geometry=pt) 110 | int_resp = read_layer.spatial_search_geometry( 111 | geometry=feature, radius=1000, selection=["name"] 112 | ) 113 | fc = int_resp.to_geojson() 114 | assert fc["features"][0]["id"] == "IND" 115 | 116 | 117 | def test_inavlid_response(): 118 | """Test invalid response from interactive api.""" 119 | resp = InteractiveMapApiResponse({"type": "dummy"}) 120 | with pytest.raises(NotImplementedError): 121 | resp.to_geojson() 122 | -------------------------------------------------------------------------------- /tests/iml/test_lookup_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | """This module will test functionality of lookup_api.""" 18 | 19 | from xyzspaces.iml.apis.aaa_oauth2_api import AAAOauth2Api 20 | from xyzspaces.iml.apis.lookup_api import LookupApi 21 | from xyzspaces.iml.auth import Auth 22 | from xyzspaces.iml.credentials import Credentials 23 | 24 | 25 | def test_get_resource_api(): 26 | """Test get resource api.""" 27 | hrn = "hrn:here:data::olp-here:catalog-to-test-in-ci-don-not-delete" 28 | cred = Credentials.from_default() 29 | aaa_oauth2_api = AAAOauth2Api(base_url=cred.cred_properties["endpoint"], proxies={}) 30 | auth = Auth(credentials=cred, aaa_oauth2_api=aaa_oauth2_api) 31 | lookup_api = LookupApi(auth=auth, proxies={}) 32 | resource_apis = lookup_api.get_resource_api(hrn, api="interactive", version="v1") 33 | assert resource_apis == { 34 | "api": "interactive", 35 | "version": "v1", 36 | "baseURL": "https://interactive.data.api.platform.here.com/interactive/v1" 37 | "/catalogs/hrn:here:data::olp-here:catalog-to-test-in-ci-don-not" 38 | "-delete", 39 | "parameters": {}, 40 | } 41 | -------------------------------------------------------------------------------- /tests/project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/xyz-spaces-python/fe6bcb0ee5131798be892be2a25e023d2c47b2b4/tests/project/__init__.py -------------------------------------------------------------------------------- /tests/project/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """Module for providing test fixtures for the Project API tests.""" 19 | 20 | import uuid 21 | 22 | import pytest 23 | 24 | from xyzspaces.apis import ProjectApi 25 | from xyzspaces.config.default import XYZConfig 26 | 27 | 28 | @pytest.fixture() 29 | def api(): 30 | """Create shared XYZ Project Api instance as a pytest fixture.""" 31 | api = ProjectApi(config=XYZConfig.from_default()) 32 | return api 33 | 34 | 35 | @pytest.fixture() 36 | def project_id(): 37 | """Create shared XYZ project as a pytest fixture.""" 38 | api = ProjectApi() 39 | 40 | # setup, create temporary project 41 | project = api.post_project( 42 | data={ 43 | "title": "Testing xyzspaces", 44 | "description": "Temporary project.", 45 | } 46 | ) 47 | project_id = project["id"] 48 | 49 | yield project_id 50 | 51 | # now teardown (delete temporary project) 52 | api.delete_project(project_id=project_id) 53 | 54 | 55 | @pytest.fixture() 56 | def create_projects(api): 57 | """Create a fixture to be used for creating temporary projects.""" 58 | project_ids = [] 59 | for i in range(5): 60 | data = dict( 61 | id=f"temp_project_{uuid.uuid4().urn[9:]}", 62 | title=f"My temp_project_{i}", 63 | description=f"temporary project for testing: {i}", 64 | status="UNPUBLISHED", 65 | ) 66 | project = api.post_project(data=data) 67 | project_id = project["id"] 68 | project_ids.append(project_id) 69 | yield project_ids 70 | # tear down deleting all temp projects 71 | for project_id in project_ids: 72 | api.delete_project(project_id=project_id) 73 | -------------------------------------------------------------------------------- /tests/project/test_projects.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """Module for testing HERE XYZ Project API endpoints.""" 19 | 20 | 21 | import pytest 22 | 23 | from xyzspaces.apis import ProjectApi 24 | from xyzspaces.utils import get_xyz_token 25 | 26 | XYZ_TOKEN = get_xyz_token() 27 | 28 | 29 | @pytest.mark.skipif(not XYZ_TOKEN, reason="No token found.") 30 | def test_get_projects(api): 31 | """Test get projects list.""" 32 | projects = api.get_projects() 33 | assert type(projects) == list 34 | 35 | 36 | @pytest.mark.skipif(not XYZ_TOKEN, reason="No token found.") 37 | def test_get_projects_env_token(): 38 | """Test get projects list with default token directly from environment.""" 39 | my_api = ProjectApi() 40 | projects = my_api.get_projects() 41 | assert type(projects) == list 42 | 43 | 44 | @pytest.mark.skipif(not XYZ_TOKEN, reason="No token found.") 45 | def test_get_project(api, project_id): 46 | """Test get single project.""" 47 | project = api.get_project(project_id=project_id) 48 | print(project) 49 | exp = set( 50 | [ 51 | "id", 52 | "status", 53 | # 'rot', 'base', 'meta', 'layers', 'bookmarks', 54 | # 'thumbnail', 'created_at', 'last_update', 'map_settings', 55 | # 'publish_settings' 56 | ] 57 | ) 58 | assert exp.issubset(set(project.keys())) 59 | 60 | 61 | @pytest.mark.skipif(not XYZ_TOKEN, reason="No token found.") 62 | def _test_get_my_project(api, project_id): 63 | """Test get single project.""" 64 | # https://xyz.here.com/studio/project/5c54716d-f900-4b89-80ac-b21518e94b30 65 | project = api.get_project(project_id="5c54716d-f900-4b89-80ac-b21518e94b30") 66 | print(project) 67 | 68 | 69 | # This is tested inside the roundtrip test below. 70 | @pytest.mark.skipif(not XYZ_TOKEN, reason="No token found.") 71 | def _test_post_project(api): 72 | """Test post new project.""" 73 | data = dict( 74 | id="temp-project-1", 75 | title="My temp-project-1", 76 | description="Temporary project for test purposes.", 77 | status="UNPUBLISHED", 78 | ) 79 | project = api.post_project(data=data) 80 | exp = set(["id", "description", "status", "created_at", "last_update"]) 81 | assert exp == set(project.keys()) 82 | 83 | 84 | # This is tested inside the roundtrip test below. 85 | @pytest.mark.skipif(not XYZ_TOKEN, reason="No token found.") 86 | def _test_delete_project(api): 87 | """Test delete new project.""" 88 | response = api.delete_project(project_id="temp-project-1") 89 | assert response == "" 90 | 91 | 92 | # @pytest.mark.skipif(not XYZ_TOKEN, reason="No token found.") 93 | @pytest.mark.skipif(not XYZ_TOKEN, reason="No token found.") 94 | def test_roundtrip_project(api): 95 | """Test create/update/delete project.""" 96 | data = dict( 97 | # id=project_id, 98 | description="Temporary project for test purposes.", 99 | status="UNPUBLISHED", 100 | ) 101 | project = api.post_project(data=data) 102 | project_id = project["id"] 103 | exp = set(["id", "description", "status"]) 104 | assert exp.issubset(set(project.keys())) 105 | 106 | project = api.put_project(project_id=project_id, data={}) 107 | assert "description" not in set(project.keys()) 108 | 109 | data = dict(description="Temporary project (after put and patch).") 110 | project = api.patch_project(project_id=project_id, data=data) 111 | assert "description" in set(project.keys()) 112 | 113 | response = api.delete_project(project_id=project_id) 114 | assert response == "" 115 | 116 | 117 | @pytest.mark.skipif(not XYZ_TOKEN, reason="No token found.") 118 | def test_get_projects_by_pagination(api, create_projects): 119 | """ 120 | Test get multiple projects based on ``paginate``, ``limit`` and ``handle`` params. 121 | 122 | The ``limit`` parameter will limit number of projects returned by 123 | ``ProjectApi.get_projects`` method. 124 | 125 | :param api: A pytest fixture which will return :class:`ProjectApi` object. 126 | :param create_projects: A pytest fixture that will return list of project_ids. 127 | """ 128 | # Added assert below to just check fixture has return list of project ids. 129 | assert type(create_projects) == list 130 | my_api = ProjectApi() 131 | limit_projects = my_api.get_projects(paginate=True, limit=2) 132 | assert type(limit_projects) == dict 133 | assert len(limit_projects["projects"]) == 2 134 | assert type(limit_projects["handle"]) == int 135 | 136 | handle = limit_projects["handle"] 137 | projs = my_api.get_projects(paginate=True, limit=3, handle=handle) 138 | assert type(projs) == dict 139 | assert len(projs["projects"]) == 3 140 | assert type(projs["handle"]) == int 141 | -------------------------------------------------------------------------------- /tests/space/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/xyz-spaces-python/fe6bcb0ee5131798be892be2a25e023d2c47b2b4/tests/space/__init__.py -------------------------------------------------------------------------------- /tests/space/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """Module for providing test fixtures for the Hub API tests.""" 19 | 20 | import json 21 | from pathlib import Path 22 | from time import sleep 23 | 24 | import pytest 25 | 26 | from xyzspaces.apis import HubApi 27 | from xyzspaces.config.default import XYZConfig 28 | from xyzspaces.datasets import get_chicago_parks_data, get_countries_data 29 | from xyzspaces.spaces import Space 30 | 31 | 32 | @pytest.fixture() 33 | def api(): 34 | """Create shared XYZ Hub Api instance as a pytest fixture.""" 35 | api = HubApi(config=XYZConfig.from_default()) 36 | return api 37 | 38 | 39 | @pytest.fixture() 40 | def space_id(): 41 | """Create shared XYZ space with countries data as a pytest fixture.""" 42 | api = HubApi(config=XYZConfig.from_default()) 43 | 44 | # setup, create temporary space 45 | res = api.post_space( 46 | data={ 47 | "title": "Testing xyzspaces", 48 | "description": "Temporary space containing countries data.", 49 | } 50 | ) 51 | space_id = res["id"] 52 | 53 | # add features to space 54 | gj_countries = get_countries_data() 55 | sleep(0.5) 56 | api.put_space_features(space_id=space_id, data=gj_countries) 57 | 58 | yield space_id 59 | 60 | # now teardown (delete temporary space) 61 | api.delete_space(space_id=space_id) 62 | 63 | 64 | @pytest.fixture() 65 | def empty_space(api): 66 | """Create shared empty XYZ space as a pytest fixture.""" 67 | # setup, create temporary space 68 | space = Space(api=api).new( 69 | title="Testing xyzspaces", 70 | description="Temporary empty space containing no features.", 71 | ) 72 | 73 | sleep(0.5) 74 | yield space 75 | 76 | # now teardown (delete temporary space) 77 | space.delete() 78 | 79 | 80 | @pytest.fixture() 81 | def space_object(api, space_id): 82 | """Create from an existing space ID.""" 83 | space = Space.from_id(space_id) 84 | return space 85 | 86 | 87 | @pytest.fixture() 88 | def upstream_spaces(api, space_id): 89 | """Create a list of space_ids, to test virtual-spaces.""" 90 | res = api.post_space( 91 | data={ 92 | "title": "Testing xyzspaces with Chicago Parks data", 93 | "description": "Temporary space containing Chicago parks data", 94 | } 95 | ) 96 | space_id2 = res["id"] 97 | 98 | # add features to space 99 | gj_countries = get_chicago_parks_data() 100 | sleep(0.5) 101 | api.put_space_features(space_id=space_id2, data=gj_countries) 102 | 103 | yield [space_id, space_id2] 104 | 105 | # now teardown (delete temporary spaces) 106 | api.delete_space(space_id=space_id2) 107 | 108 | 109 | @pytest.fixture() 110 | def shared_space(): 111 | """Create a new shared space.""" 112 | space = Space.new( 113 | title="test shared space", description="test shared space", shared=True 114 | ) 115 | 116 | sleep(0.5) 117 | yield space 118 | 119 | # now teardown (delete temporary space) 120 | space.delete() 121 | 122 | 123 | @pytest.fixture() 124 | def activity_log_space(): 125 | """Create a new space supporting activity log.""" 126 | listeners = { 127 | "id": "activity-log", 128 | "params": { 129 | "states": 5, 130 | "storageMode": "DIFF_ONLY", 131 | "writeInvalidatedAt": "true", 132 | }, 133 | "eventTypes": ["ModifySpaceEvent.request"], 134 | } 135 | space = Space.new( 136 | title="Activity-Log Test", 137 | description="A test space for Activity-Log", 138 | enable_uuid=True, 139 | listeners=listeners, 140 | ) 141 | sleep(0.5) 142 | yield space 143 | 144 | # now teardown (delete temporary space) 145 | space.delete() 146 | 147 | 148 | @pytest.fixture() 149 | def large_data_space(): 150 | """Create a new large data space.""" 151 | space = Space.new( 152 | title="test large data space", 153 | description="test large data space", 154 | shared=True, 155 | ) 156 | path = Path(__file__).parent.parent / "data" / "large_data.geojson" 157 | 158 | with open(path) as json_file: 159 | data = json.load(json_file) 160 | 161 | space.add_features(data, features_size=5000, chunk_size=2) 162 | 163 | sleep(0.5) 164 | yield space 165 | 166 | # now teardown (delete temporary space) 167 | space.delete() 168 | 169 | 170 | @pytest.fixture() 171 | def schema_validation_space(): 172 | """Create a space with schema validation.""" 173 | schema = ( 174 | '{"definitions":{},"$schema":"http://json-schema.org/draft-07/schema#",' 175 | '"$id":"http://example.com/root.json","type":"object",' 176 | '"title":"TheRootSchema","required":["geometry","type","properties"]}' 177 | ) 178 | space = Space.new( 179 | title="test schema validation space", 180 | description="test schema validation space", 181 | schema=schema, 182 | ) 183 | yield space 184 | 185 | # now teardown (delete temporary space) 186 | space.delete() 187 | -------------------------------------------------------------------------------- /tests/token/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/xyz-spaces-python/fe6bcb0ee5131798be892be2a25e023d2c47b2b4/tests/token/__init__.py -------------------------------------------------------------------------------- /tests/token/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """Module for providing test fixtures for the Token API tests.""" 19 | 20 | import warnings 21 | 22 | import pytest 23 | 24 | from xyzspaces.apis import TokenApi 25 | from xyzspaces.config.default import XYZConfig 26 | from xyzspaces.exceptions import AuthenticationError 27 | 28 | 29 | @pytest.fixture() 30 | def api(): 31 | """Create shared XYZ Token API instance as a pytest fixture.""" 32 | try: 33 | api = TokenApi(config=XYZConfig.from_default()) 34 | except AuthenticationError: 35 | api = TokenApi() 36 | warnings.warn( 37 | "Ignoring invalid credentials, creating TokenApi " 38 | "instance without. Access limitations may apply." 39 | ) 40 | return api 41 | -------------------------------------------------------------------------------- /tests/token/test_auth.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """Module for testing obtaining cookies to authenticate certain API endpoints.""" 19 | 20 | import os 21 | 22 | import pytest 23 | 24 | from xyzspaces.auth import get_auth_cookies 25 | from xyzspaces.exceptions import AuthenticationError 26 | 27 | HERE_USER = os.environ.get("HERE_USER") 28 | HERE_PASSWORD = os.environ.get("HERE_PASSWORD") 29 | 30 | 31 | def test_bad_credentials(): 32 | """Raise error for invalid account credentials.""" 33 | with pytest.raises(AuthenticationError): 34 | get_auth_cookies(username="foo", password="bar") 35 | 36 | 37 | @pytest.mark.skipif( 38 | not (HERE_USER and HERE_PASSWORD), 39 | reason="No HERE account credentials found.", 40 | ) 41 | def test_get_access_cookie(): 42 | """Get response for a valid cookie.""" 43 | cookies = get_auth_cookies(HERE_USER, HERE_PASSWORD) 44 | exp = [ 45 | "here_access_oidc", 46 | "here_auth_oidc", 47 | "here_access", 48 | "here_auth", 49 | "here_ca_access", 50 | ] 51 | assert set(cookies.keys()) == set(exp) 52 | 53 | 54 | @pytest.mark.skipif( 55 | not (HERE_USER and HERE_PASSWORD), 56 | reason="No HERE account credentials found.", 57 | ) 58 | def test_get_tokens_cookies(api): 59 | """Get the tokens.""" 60 | # url = "http://xyz.api.here.com/token-api/tokens" 61 | cookies = get_auth_cookies(HERE_USER, HERE_PASSWORD) 62 | # tokens = requests.get(url, cookies=cookies).json() 63 | api.headers = {} 64 | tokens = api.get(path="/token-api/tokens", cookies=cookies).json() 65 | assert type(tokens) == list 66 | 67 | api.headers = {} 68 | tokens = api.get(path="/token-api/tokens").json() 69 | assert type(tokens) == list 70 | 71 | api.headers = {} 72 | tokens = api.get_tokens() 73 | assert type(tokens) == list 74 | -------------------------------------------------------------------------------- /tests/token/test_basic.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """Module for testing HERE XYZ Token API endpoints.""" 19 | 20 | import datetime 21 | import os 22 | 23 | import pytest 24 | 25 | from xyzspaces.apis import TokenApi 26 | from xyzspaces.exceptions import ApiError 27 | 28 | XYZ_TOKEN = os.environ.get("XYZ_TOKEN") 29 | 30 | 31 | def test_get_invalid_token_raw(api): 32 | """Get response for an invalid token.""" 33 | with pytest.raises(ApiError) as execinfo: 34 | api.get(path="/token-api/tokens/INVALID.json").text 35 | resp = execinfo.value.args[0] 36 | assert resp.status_code == 412 37 | assert resp.reason == "Precondition Failed" 38 | assert resp.text == "tokenId is not valid" 39 | 40 | 41 | @pytest.mark.skipif(not XYZ_TOKEN, reason="No token found.") 42 | def test_get_token(api): 43 | """Get the token info.""" 44 | info = api.get_token(XYZ_TOKEN) 45 | assert "description" in info 46 | assert "limits" in info 47 | assert "tid" in info 48 | 49 | 50 | @pytest.mark.skipif(not XYZ_TOKEN, reason="No token found.") 51 | def test_get_token_env_token(api): 52 | """Get the token info with default token directly from environment.""" 53 | my_api = TokenApi() 54 | info = my_api.get_token(XYZ_TOKEN) 55 | assert "description" in info 56 | assert "limits" in info 57 | assert "tid" in info 58 | 59 | 60 | # The following tests are considered "protected" and need access 61 | # cookies which will be created when a TokenApi instance is created. 62 | 63 | HERE_APP_ID = os.environ.get("HERE_APP_ID") 64 | HERE_USER = os.environ.get("HERE_USER") 65 | HERE_PASSWORD = os.environ.get("HERE_PASSWORD") 66 | 67 | 68 | @pytest.mark.skipif( 69 | not (HERE_USER and HERE_PASSWORD), 70 | reason="No HERE account credentials found.", 71 | ) 72 | def test_get_tokens(api): 73 | """Get a list of tokens.""" 74 | api.headers = {} 75 | tokens = api.get_tokens() 76 | assert type(tokens) == list 77 | exp = [ 78 | "aid", 79 | "awsPlanId", 80 | "urm", 81 | "iat", 82 | "limits", 83 | "description", 84 | "cid", 85 | "tid", 86 | ] 87 | assert set(exp).issubset(set(tokens[0].keys())) 88 | 89 | 90 | @pytest.mark.skipif( 91 | not (HERE_USER and HERE_PASSWORD and HERE_APP_ID), 92 | reason="No HERE account credentials found.", 93 | ) 94 | def test_create_token(api): 95 | """Create a new token.""" 96 | utc_now = datetime.datetime.utcnow() 97 | # expiry data must be at least 60 minutes in the future 98 | expiry_date = utc_now + datetime.timedelta(minutes=90) 99 | data = dict( 100 | cid=HERE_APP_ID, 101 | exp=int(expiry_date.timestamp()), 102 | description=( 103 | "This is a disposable token for testing only " 104 | f"(expiring on {expiry_date} UTC)." 105 | ), 106 | urm={"xyz-hub": {"readFeatures": [{}]}}, 107 | metadata=dict(foo=42), 108 | ) 109 | info = api.post_token(json=data) 110 | assert info["description"] == data["description"] 111 | assert info["metadata"] == data["metadata"] 112 | 113 | 114 | # This would need to remove the auth cookie for this to make sense... 115 | def _test_delete_not_authorized_token(api): 116 | """Delete a token without authorization.""" 117 | with pytest.raises(ApiError) as execinfo: 118 | api.delete_token("INVALID") 119 | resp = execinfo.value.args[0] 120 | assert resp.status_code == 401 121 | assert resp.reason == "Unauthorized" 122 | assert resp.json()["message"] == "Unauthorized" 123 | 124 | 125 | @pytest.mark.skipif( 126 | not (HERE_USER and HERE_PASSWORD), 127 | reason="No HERE account credentials found.", 128 | ) 129 | def test_delete_non_existing_token(api): 130 | """Delete a non-existing token.""" 131 | api.headers = {} 132 | with pytest.raises(ApiError) as execinfo: 133 | api.delete_token("INVALID") 134 | resp = execinfo.value.args[0] 135 | assert resp.status_code == 404 136 | assert resp.reason == "Not Found" 137 | assert resp.text == "Token not found" 138 | 139 | 140 | @pytest.mark.skipif( 141 | not (HERE_USER and HERE_PASSWORD and HERE_APP_ID), 142 | reason="No HERE account credentials found.", 143 | ) 144 | def test_delete_existing_token(api): 145 | """Delete an existing token.""" 146 | # first create a token 147 | utc_now = datetime.datetime.utcnow() 148 | # expiry data must be at least 60 minutes in the future 149 | expiry_date = utc_now + datetime.timedelta(minutes=90) 150 | data = dict( 151 | cid=HERE_APP_ID, 152 | exp=int(expiry_date.timestamp()), 153 | description=( 154 | "This is a disposable token for testing only " 155 | f"(expiring on {expiry_date} UTC)." 156 | ), 157 | urm={"xyz-hub": {"readFeatures": [{}]}}, 158 | ) 159 | info = api.post_token(json=data) 160 | assert "token" in info 161 | token = info["token"] 162 | 163 | # delete it again 164 | info = api.delete_token(token) 165 | assert info == "" 166 | -------------------------------------------------------------------------------- /tests/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/xyz-spaces-python/fe6bcb0ee5131798be892be2a25e023d2c47b2b4/tests/tools/__init__.py -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/xyz-spaces-python/fe6bcb0ee5131798be892be2a25e023d2c47b2b4/tests/utils/__init__.py -------------------------------------------------------------------------------- /tests/utils/test_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """ 19 | Module for testing xyzspaces.utils. 20 | 21 | Here we don't generate any temporary spaces. 22 | """ 23 | 24 | import os 25 | 26 | import pytest 27 | 28 | from xyzspaces.utils import get_xyz_token, join_string_lists 29 | 30 | XYZ_TOKEN = get_xyz_token() 31 | 32 | 33 | def test_join_string_lists(): 34 | """Test join_string_lists function.""" 35 | res = join_string_lists(foo=["a", "b", "c"], bar=["a", "b"], foobar=None) 36 | assert res == {"foo": "a,b,c", "bar": "a,b"} 37 | 38 | 39 | @pytest.mark.skipif(not XYZ_TOKEN, reason="No token found.") 40 | def test_get_xyz_token_empty(): 41 | """Test for empty xyz_token.""" 42 | # storing existing token into variable. 43 | token = os.environ["XYZ_TOKEN"] 44 | del os.environ["XYZ_TOKEN"] 45 | result = get_xyz_token() 46 | assert result == "" 47 | # resetting the token again. 48 | os.environ["XYZ_TOKEN"] = token 49 | -------------------------------------------------------------------------------- /xyzspaces/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """The XYZ Spaces for Python - manage your XYZ Hub server or HERE Data Hub. 19 | 20 | XYZ Spaces for Python allows you to manage XYZ spaces, projects and tokens with the Hub 21 | API, Project API, and Token API, respectively. The Hub API provides the most 22 | features to let you read and write GeoJSON data (features) from and to an 23 | XYZ space, and perform some higher-level operations like return features 24 | inside or clipped by some bounding box or map tile. The Project API and 25 | Token API let you manage your XYZ projects and tokens. 26 | 27 | See also: 28 | - XYZ Hub server: https://github.com/heremaps/xyz-hub 29 | - HERE Data Hub: https://developer.here.com/products/data-hub 30 | """ 31 | 32 | from typing import Optional 33 | 34 | from xyzspaces.__version__ import __version__ # noqa: F401 35 | from xyzspaces.iml import IML # noqa: F401 36 | from xyzspaces.logconf import setup_logging # noqa: F401 37 | from xyzspaces.spaces import Space 38 | 39 | from .apis import HubApi 40 | from .config.default import XYZConfig 41 | 42 | 43 | class XYZ: 44 | """A single interface to interact with your XYZ Hub server or HERE Data Hub.""" 45 | 46 | def __init__(self, config: Optional[XYZConfig] = None): 47 | """Instantiate an XYZ object, optionally with custom configuration for 48 | your credentials and base URL. 49 | 50 | :param config: An object of `class:XYZConfig`, If not provied 51 | ``XYZ_TOKEN`` will be used from environment variable and other 52 | configurations will be used as defined in module :py:mod:`default_config` 53 | """ 54 | if config: 55 | self.hub_api = HubApi(config) 56 | else: 57 | config = XYZConfig.from_default() 58 | self.hub_api = HubApi(config) 59 | 60 | self.spaces = Space(api=self.hub_api) 61 | -------------------------------------------------------------------------------- /xyzspaces/__version__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """Project version information.""" 19 | 20 | __version__ = "0.7.2" 21 | -------------------------------------------------------------------------------- /xyzspaces/_compact.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """This module is used to check optional depedencies and based on that flags will 19 | be set.""" 20 | 21 | HAS_TURFPY = None 22 | 23 | HAS_GEOPANDAS = None 24 | 25 | 26 | try: 27 | import turfpy # noqa 28 | 29 | HAS_TURFPY = True 30 | except ImportError: 31 | HAS_TURFPY = False 32 | 33 | 34 | try: 35 | import geopandas # noqa 36 | 37 | HAS_GEOPANDAS = True 38 | except ImportError: 39 | HAS_GEOPANDAS = False 40 | -------------------------------------------------------------------------------- /xyzspaces/auth.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """ 19 | This module provides HERE authentication via cookies for the Token API. 20 | 21 | This :func:`get_auth_cookies()` function simulates the login process on 22 | http://developer.here.com to obtain an access cookie for authenticating 23 | with certain RESTful APIs like the XYZ Token API. 24 | 25 | This implementation is inspired by the Open Source HERE XYZ CLI: 26 | https://github.com/heremaps/here-cli/blob/master/src/sso.ts. 27 | """ 28 | 29 | import requests 30 | 31 | from .exceptions import AuthenticationError 32 | 33 | URL = ( 34 | "https://account.here.com/sign-in?" 35 | "client-id=es1HEn2LGqFocvfD1eEt&version=3&sdk=true&type=frame&" 36 | "uri=https%3A%2F%2Fxyz.here.com&sign-in-screen-config=password," 37 | "heread&track-id=trackUPMUI&lang=en-us" 38 | ) 39 | SIGN_IN_URL = "https://account.here.com/api/account/sign-in-with-password" 40 | 41 | 42 | def filter_cookies(cookies: requests.cookies.RequestsCookieJar, prefix: str) -> dict: 43 | """ 44 | Filter :mod:`requests` cookies with some given name prefix into a new dict. 45 | 46 | :param cookies: A :mod:`requests` cookies object. 47 | :param prefix: A prefix string to search in cookies. 48 | :return: A dict. 49 | 50 | Example: 51 | 52 | Input:: 53 | 54 | , 56 | , 57 | ] 58 | > 59 | 60 | Output:: 61 | 62 | {'here_account': 'foobar', 'here_account.sig': 'barfoo'} 63 | """ 64 | return {k: v.split(";")[0] for (k, v) in cookies.items() if k.startswith(prefix)} 65 | 66 | 67 | def get_auth_cookies(username: str, password: str) -> dict: 68 | """Get authentication cookies from name and password of a HERE account. 69 | 70 | :param username: Username for HERE account. 71 | :param password: Password for HERE account. 72 | :return: A dict. 73 | :raises AuthenticationError: If status_code for HTTP response returned by 74 | :mod:`requests` is not equal to 200. 75 | """ 76 | resp1 = requests.get(URL) 77 | body = resp1.text 78 | csrf_token = body[body.find("csrf") :] 79 | csrf_token = csrf_token[csrf_token.find(":") + 3 : csrf_token.find(",") - 1] 80 | headers = {"x-csrf-token": csrf_token} 81 | request_body = { 82 | "realm": "here", 83 | "email": username, 84 | "password": password, 85 | "rememberMe": True, 86 | } 87 | here_cookies = filter_cookies(resp1.cookies, prefix="here") 88 | resp2 = requests.post( 89 | SIGN_IN_URL, headers=headers, json=request_body, cookies=here_cookies 90 | ) 91 | if resp2.status_code != 200: 92 | raise AuthenticationError( 93 | "Error while authenticating. " "Please check credentials and try again." 94 | ) 95 | here_cookies = filter_cookies(resp2.cookies, prefix="here") 96 | return here_cookies 97 | -------------------------------------------------------------------------------- /xyzspaces/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/xyz-spaces-python/fe6bcb0ee5131798be892be2a25e023d2c47b2b4/xyzspaces/config/__init__.py -------------------------------------------------------------------------------- /xyzspaces/config/default.py: -------------------------------------------------------------------------------- 1 | """This module defines classes for default configuration for the project.""" 2 | 3 | import os 4 | from pathlib import Path 5 | from typing import Union 6 | 7 | from pyhocon import ConfigFactory 8 | 9 | # : This config dictionary is used by default to create object of XYZConfig class 10 | # : if user does not provide config object for project level configurations. 11 | DEFAULT_CONFIG = { 12 | "credentials": { 13 | "XYZ_TOKEN": os.environ.get("XYZ_TOKEN"), 14 | "HERE_USER": os.environ.get("HERE_USER"), 15 | "HERE_PASSWORD": os.environ.get("HERE_PASSWORD"), 16 | }, 17 | "http_headers": { 18 | "Authorization": f"Bearer {os.environ.get('XYZ_TOKEN')}", 19 | "Content-Type": "application/geo+json", 20 | }, 21 | "url": "https://xyz.api.here.com", 22 | } 23 | 24 | 25 | class XYZConfig: 26 | """This class defines methods to manage configurations for project.""" 27 | 28 | def __init__(self, **kwargs): 29 | self.config = kwargs 30 | 31 | @classmethod 32 | def from_default(cls) -> "XYZConfig": 33 | """Return the default config for the project.""" 34 | return cls(**DEFAULT_CONFIG) 35 | 36 | @classmethod 37 | def from_file(cls, path: Union[str, Path]) -> "XYZConfig": 38 | """Return the config from file path provided.""" 39 | config_data = ConfigFactory.parse_file(path) 40 | return cls(**config_data) 41 | -------------------------------------------------------------------------------- /xyzspaces/config/logconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "disable_existing_loggers": false, 4 | "formatters": { 5 | "simple": { 6 | "format": "%(asctime)s - %(filename)s:%(lineno)d - %(levelname)s - %(message)s" 7 | } 8 | }, 9 | 10 | "handlers": { 11 | "console": { 12 | "class": "logging.StreamHandler", 13 | "level": "DEBUG", 14 | "formatter": "simple", 15 | "stream": "ext://sys.stdout" 16 | }, 17 | 18 | "file_handler": { 19 | "class": "logging.handlers.RotatingFileHandler", 20 | "level": "DEBUG", 21 | "formatter": "simple", 22 | "filename": "xyz.log", 23 | "maxBytes": 10485760, 24 | "backupCount": 20, 25 | "encoding": "utf8" 26 | } 27 | 28 | }, 29 | 30 | "loggers": { }, 31 | "root": { 32 | "level": "INFO", 33 | "handlers": ["console", "file_handler"] 34 | } 35 | } -------------------------------------------------------------------------------- /xyzspaces/datasets/LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. 4 | 5 | In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and 6 | successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | For more information, please refer to -------------------------------------------------------------------------------- /xyzspaces/datasets/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """This package provides access to some public datasets used.""" 19 | 20 | import json 21 | import warnings 22 | from pathlib import Path 23 | 24 | import requests 25 | 26 | from xyzspaces import Space 27 | 28 | MICROSOFT_BUILDINGS_SPACE_ID = "R4QDHvd1" 29 | 30 | 31 | def get_countries_data(): 32 | """Pull countries example GeoJSON from the net or a locally cached file. 33 | 34 | If this is not locally cached, yet, it will be after the first call, 35 | unless the file cannot be saved, in which case it will be re-downloaded 36 | again with every call. 37 | 38 | Source (under http://unlicense.org): 39 | https://raw.githubusercontent.com/johan/world.geo.json/master/countries.geo.json 40 | 41 | The data contains 180 countries, and does not cover all existing countries, 42 | ca. 200. For example the Vatican is missing. 43 | 44 | :return: A JSON object. 45 | """ 46 | datasets_home = Path(__file__).parent 47 | url_countries = ( 48 | "https://raw.githubusercontent.com" 49 | "/johan/world.geo.json/master/countries.geo.json" 50 | ) 51 | fn_countries = datasets_home / Path(url_countries).name 52 | if fn_countries.exists(): 53 | gj_countries = json.load(fn_countries.open()) 54 | else: 55 | gj_countries = requests.get(url_countries).json() 56 | try: 57 | json.dump(gj_countries, fn_countries.open("w")) 58 | except IOError: 59 | warnings.warn( 60 | f"Could not cache {url_countries} to {datasets_home}. " 61 | "Check if you have write access. Will re-download next time." 62 | ) 63 | 64 | # Clean data for this specific file (simply remove features with ID "-99".) 65 | # gj_countries = [f for f in gj_countries["features"] if f["id"] != "-99"] 66 | 67 | # Clean data to replace non-unique IDs (-99 appears twice) with new ones: 68 | for f in gj_countries["features"]: 69 | if f["id"] == "-99": 70 | name = f["properties"]["name"] 71 | if name == "Northern Cyprus": 72 | f["id"] = "NCP" 73 | elif name == "Somaliland": 74 | f["id"] = "SML" 75 | 76 | return gj_countries 77 | 78 | 79 | def get_chicago_parks_data(): 80 | """Create GeoJSON from file ``chicago_parks.geo.json`` stored locally.""" 81 | datasets_home = Path(__file__).parent 82 | chicago_parks = datasets_home / "chicago_parks.geo.json" 83 | 84 | with open(chicago_parks, encoding="utf-8-sig") as json_file: 85 | chicago_parks_data = json.load(json_file) 86 | return chicago_parks_data 87 | 88 | 89 | def get_microsoft_buildings_space(): 90 | """Create a space object for the MS "US Buildings Footprints" dataset. 91 | 92 | The original source for this dataset can be found on 93 | https://github.com/Microsoft/USBuildingFootprints. 94 | 95 | :return: A space object. 96 | """ 97 | microsoft_buildings_space = Space.from_id(MICROSOFT_BUILDINGS_SPACE_ID) 98 | 99 | return microsoft_buildings_space 100 | -------------------------------------------------------------------------------- /xyzspaces/datasets/chicago_parks.geo.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": [ 3 | { 4 | "type": "Feature", 5 | "id": "LP", 6 | "properties": { 7 | "title": "Lincoln Park", 8 | "description": "A northside park that is home to the Lincoln Park Zoo", 9 | "area": "1200" 10 | }, 11 | "geometry": { 12 | "coordinates": [ 13 | -87.637596, 14 | 41.940403, 15 | 4.0 16 | ], 17 | "type": "Point" 18 | } 19 | }, 20 | { 21 | "type": "Feature", 22 | "id": "BP", 23 | "properties": { 24 | "title": "Burnham Park", 25 | "description": "A lakefront park on Chicago's south side", 26 | "area": "598" 27 | }, 28 | "geometry": { 29 | "coordinates": [ 30 | -87.603735, 31 | 41.829985, 32 | 0.0 33 | ], 34 | "type": "Point" 35 | } 36 | }, 37 | { 38 | "type": "Feature", 39 | "id": "MP", 40 | "properties": { 41 | "title": "Millennium Park", 42 | "description": "A downtown park known for its art installations and unique architecture", 43 | "area": "24.5" 44 | }, 45 | "geometry": { 46 | "coordinates": [ 47 | -87.622554, 48 | 41.882534, 49 | 0.0 50 | ], 51 | "type": "Point" 52 | } 53 | }, 54 | { 55 | "type": "Feature", 56 | "id": "GP", 57 | "properties": { 58 | "title": "Grant Park", 59 | "description": "A downtown park that is the site of many of Chicago's favorite festivals and events", 60 | "area": "319" 61 | }, 62 | "geometry": { 63 | "coordinates": [ 64 | -87.619185, 65 | 41.876367, 66 | 0.0 67 | ], 68 | "type": "Point" 69 | } 70 | }, 71 | { 72 | "type": "Feature", 73 | "id": "HP", 74 | "properties": { 75 | "title": "Humboldt Park", 76 | "description": "A large park on Chicago's northwest side", 77 | "area": "207" 78 | }, 79 | "geometry": { 80 | "coordinates": [ 81 | -87.70199, 82 | 41.905423, 83 | 0.0 84 | ], 85 | "type": "Point" 86 | } 87 | }, 88 | { 89 | "type": "Feature", 90 | "id": "DP", 91 | "properties": { 92 | "title": "Douglas Park", 93 | "description": "A large park near in Chicago's North Lawndale neighborhood", 94 | "area": "173" 95 | }, 96 | "geometry": { 97 | "coordinates": [ 98 | -87.699329, 99 | 41.860092, 100 | 100.0 101 | ], 102 | "type": "Point" 103 | } 104 | }, 105 | { 106 | "type": "Feature", 107 | "id": "CP", 108 | "properties": { 109 | "title": "Calumet Park", 110 | "description": "A park on the Illinois-Indiana border featuring a historic fieldhouse", 111 | "area": "200" 112 | }, 113 | "geometry": { 114 | "coordinates": [ 115 | -87.530221, 116 | 41.715515, 117 | 400.34 118 | ], 119 | "type": "Point" 120 | } 121 | }, 122 | { 123 | "type": "Feature", 124 | "id": "JP", 125 | "properties": { 126 | "title": "Jackson Park", 127 | "description": "A lakeside park that was the site of the 1893 World's Fair", 128 | "area": "500" 129 | }, 130 | "geometry": { 131 | "coordinates": [ 132 | -87.580389, 133 | 41.783185, 134 | 0.0 135 | ], 136 | "type": "Point" 137 | } 138 | }, 139 | { 140 | "type": "Feature", 141 | "id": "COP", 142 | "properties": { 143 | "title": "Columbus Park", 144 | "description": "A large park in Chicago's Austin neighborhood", 145 | "area": "144" 146 | }, 147 | "geometry": { 148 | "coordinates": [ 149 | -87.769775, 150 | 41.873683, 151 | 10.0 152 | ], 153 | "type": "Point" 154 | } 155 | } 156 | ], 157 | "type": "FeatureCollection" 158 | } -------------------------------------------------------------------------------- /xyzspaces/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """This module defines API exceptions.""" 19 | 20 | 21 | class AuthenticationError(Exception): 22 | """Exception raised when authentication fails.""" 23 | 24 | pass 25 | 26 | 27 | class ApiError(Exception): 28 | """Exception raised for API HTTP response status codes not in [200...300). 29 | 30 | The exception value will be the response object returned by :mod:`requests` 31 | which provides access to all its attributes, eg. :attr:`status_code`, 32 | :attr:`reason` and :attr:`text`, etc. 33 | 34 | Example: 35 | 36 | >>> try: 37 | >>> import os 38 | >>> os.environ["XYZ_TOKEN"] = "MY-XYZ-TOKEN" 39 | >>> api = HubApi() 40 | >>> api.get("/hub/nope").json() 41 | >>> except ApiError as e: 42 | >>> resp = e.value.args[0] 43 | >>> if resp.status_code == 404 and resp.reason == "Not Found": 44 | >>> ... 45 | """ 46 | 47 | def __str__(self): 48 | """Return a string from the HTTP response causing the exception. 49 | 50 | The string simply lists the repsonse's status code, reason and text 51 | content, separated with commas. 52 | """ 53 | resp = self.args[0] 54 | return f"{resp.status_code}, {resp.reason}, {resp.text}" 55 | 56 | 57 | class TooManyRequestsException(Exception): 58 | """Exception raised for API HTTP response status code 429. 59 | 60 | This is a dedicated exception to be used with the `backoff` package, because 61 | it requires a specific exception class. 62 | 63 | The exception value will be the response object returned by :mod:`requests` 64 | which provides access to all its attributes, eg. :attr:`status_code`, 65 | :attr:`reason` and :attr:`text`, etc. 66 | """ 67 | 68 | def __str__(self): 69 | """Return a string from the HTTP response causing the exception. 70 | 71 | The string simply lists the repsonse's status code, reason and text 72 | content, separated with commas. 73 | """ 74 | resp = self.args[0] 75 | return f"{resp.status_code}, {resp.reason}, {resp.text}" 76 | -------------------------------------------------------------------------------- /xyzspaces/iml/apis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heremaps/xyz-spaces-python/fe6bcb0ee5131798be892be2a25e023d2c47b2b4/xyzspaces/iml/apis/__init__.py -------------------------------------------------------------------------------- /xyzspaces/iml/apis/aaa_oauth2_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """ 19 | This module contains an :class:`AAAOauth2ApiClient` class to perform oauth API operations. 20 | 21 | The HERE API reference documentation used in this module can be found here: 22 | |iam_api_reference| 23 | 24 | .. |iam_api_reference| raw:: html 25 | 26 | IAM API Reference # noqa 27 | """ 28 | 29 | from typing import Dict, Optional 30 | 31 | from requests_oauthlib import OAuth1 32 | 33 | from xyzspaces.iml.apis.api import Api 34 | from xyzspaces.iml.exceptions import AuthenticationException, TooManyRequestsException 35 | 36 | 37 | class AAAOauth2Api(Api): 38 | """ 39 | This class provides access to HERE platform AAA Oauth2 APIs. 40 | """ 41 | 42 | def __init__( 43 | self, 44 | base_url: str, 45 | proxies: Optional[dict] = None, 46 | ): 47 | self.base_url = base_url 48 | self.proxies = proxies 49 | super().__init__( 50 | access_token=None, 51 | proxies=self.proxies, 52 | ) 53 | 54 | def request_scoped_access_token(self, oauth: OAuth1, data: str) -> Dict: 55 | """ 56 | Request scoped access oauth2 token from platform. 57 | 58 | :param oauth: oauth1 configuration. 59 | :param data: a string which represents request body. 60 | :return: a json with scoped access token. 61 | :raises TooManyRequestsException: If the status code of the HTTP response is 429 62 | :raises AuthenticationException: If platform responds with HTTP 401 or 403. 63 | :raises RuntimeError: If platform does not respond with HTTP 200. 64 | """ 65 | resp = self.post( 66 | url=self.base_url, 67 | headers={"Content-Type": "application/x-www-form-urlencoded"}, 68 | data=data, 69 | auth=oauth, 70 | ) 71 | if resp.status_code == 429: 72 | raise TooManyRequestsException(resp) 73 | elif resp.status_code in [401, 403]: 74 | raise AuthenticationException(resp) 75 | elif resp.status_code != 200: 76 | raise RuntimeError( 77 | "Authentication returned unexpected status {}".format(resp.status_code) 78 | ) 79 | resp_dict: dict = resp.json() 80 | return resp_dict 81 | -------------------------------------------------------------------------------- /xyzspaces/iml/apis/data_config_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """ 19 | This module contains a :class:`DataConfigApi` class to perform API operations. 20 | 21 | The HERE API reference documentation used in this module can be found here: 22 | |config_api_reference| 23 | 24 | .. |config_api_reference| raw:: html 25 | 26 | Config API Reference # noqa E501 27 | """ 28 | from typing import Any, Dict, Optional 29 | 30 | from xyzspaces.iml.apis.api import Api 31 | from xyzspaces.iml.auth import Auth 32 | 33 | 34 | class DataConfigApi(Api): 35 | """This class defines data config APIs""" 36 | 37 | def __init__( 38 | self, 39 | auth: Auth, 40 | proxies: Optional[dict] = None, 41 | ): 42 | self.auth = auth 43 | super().__init__( 44 | access_token=auth.token, 45 | proxies=proxies, 46 | ) 47 | self.base_url = "https://config.data.api.platform.here.com/config/v1" 48 | 49 | def create_catalog( # type: ignore[return] 50 | self, data: Dict[str, Any], billing_tag: Optional[str] = None 51 | ) -> Dict: 52 | """ 53 | Create a catalog. 54 | 55 | :param data: a dict with a catalog metadata. 56 | :param billing_tag: A string which is used for grouping billing records. 57 | :return: response from the API. 58 | """ 59 | path = "/catalogs" 60 | url = "{}{}".format(self.base_url, path) 61 | params = {"billingTag": billing_tag} if billing_tag else {} 62 | resp = self.post(url, data, params) 63 | if resp.status_code == 202: 64 | return resp.json() 65 | else: 66 | self.raise_response_exception(resp) 67 | 68 | def get_catalog_status( # type: ignore[return] 69 | self, catalog_status_href: str, billing_tag: Optional[str] = None 70 | ) -> tuple: 71 | """ 72 | Get the status of the catalog operations for the given token. 73 | 74 | :param catalog_status_href: a catalog status href url. 75 | :param billing_tag: A string which is used for grouping billing records. 76 | :return: response from the API. 77 | """ 78 | params = {"billingTag": billing_tag} 79 | resp = self.get(url=catalog_status_href, params=params) 80 | if resp.status_code in [200, 202, 303]: 81 | return resp.json(), resp.status_code != 202 82 | else: 83 | self.raise_response_exception(resp) 84 | 85 | def get_catalog_details( # type: ignore[return] 86 | self, catalog_hrn: str, billing_tag: Optional[str] = None 87 | ) -> Dict: 88 | """ 89 | Get the full catalog configuration for the requested catalog. 90 | 91 | :param catalog_hrn: a HERE Resource Name 92 | :param billing_tag: A string which is used for grouping billing records. 93 | :return: response from the API. 94 | """ 95 | path = f"/catalogs/{catalog_hrn}" 96 | params = {"billingTag": billing_tag} 97 | url = "{}{}".format(self.base_url, path) 98 | resp = self.get(url, params=params) 99 | if resp.status_code == 200: 100 | return resp.json() 101 | else: 102 | self.raise_response_exception(resp) 103 | 104 | def update_catalog( # type: ignore[return] 105 | self, catalog_hrn: str, data: Dict[str, Any], billing_tag: Optional[str] = None 106 | ) -> dict: 107 | """ 108 | Update a catalog. 109 | 110 | :param catalog_hrn: a HERE Resource Name. 111 | :param data: body of the update catalog request. 112 | :param billing_tag: A string which is used for grouping billing records. 113 | :return: a dict with catalog update status. 114 | """ 115 | path = f"/catalogs/{catalog_hrn}" 116 | url = "{}{}".format(self.base_url, path) 117 | params = {"billingTag": billing_tag} 118 | resp = self.put(url=url, data=data, params=params) 119 | if resp.status_code == 202: 120 | return resp.json() 121 | else: 122 | self.raise_response_exception(resp) 123 | 124 | def delete_catalog(self, catalog_hrn: str, billing_tag: Optional[str] = None) -> dict: # type: ignore[return] # noqa: E501 125 | """ 126 | Delete a catalog. 127 | 128 | :param catalog_hrn: a HERE Resource Name. 129 | :param billing_tag: a string which is used for grouping billing records. 130 | :return: a dict with catalog deletion status. 131 | """ 132 | path = f"/catalogs/{catalog_hrn}" 133 | url = "{}{}".format(self.base_url, path) 134 | params = {"billingTag": billing_tag} 135 | resp = self.delete(url, params) 136 | if resp.status_code == 202: 137 | return resp.json() 138 | else: 139 | self.raise_response_exception(resp) 140 | -------------------------------------------------------------------------------- /xyzspaces/iml/apis/lookup_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """ 19 | This module contains an :class:`LookupApi` class to perform API operations. 20 | 21 | The HERE API reference documentation used in this module can be found here: 22 | |lookup_api_reference| 23 | 24 | .. |lookup_api_reference| raw:: html 25 | 26 | Lookup API Reference # noqa E501 27 | """ 28 | 29 | from typing import Optional 30 | 31 | from xyzspaces.iml.apis.api import Api 32 | from xyzspaces.iml.auth import Auth 33 | 34 | 35 | class LookupApi(Api): 36 | """ 37 | This class provides access to HERE platform Lookup APIs. 38 | 39 | Instances can call only to those API endpoints relevant for accessing 40 | catalog and layer metadata, as well as those needed to access the data 41 | contained in different types of layers. 42 | """ 43 | 44 | api_version_impl = { 45 | "lookup": "v1", 46 | "interactive": "v1", 47 | } 48 | 49 | platform_api_version_impl = {"lookup": "v1", "config": "v1", "artifact": "v1"} 50 | 51 | def __init__( 52 | self, 53 | auth: Auth, 54 | proxies: Optional[dict] = None, 55 | ): 56 | super().__init__( 57 | access_token=auth.token, 58 | proxies=proxies, 59 | ) 60 | server = "https://api-lookup.data.api.platform.here.com" 61 | base_path = "/lookup/" + self.api_version_impl["lookup"] 62 | self.base_url = f"{server}{base_path}" 63 | 64 | def get_resource_api_list(self, hrn: str, region: Optional[str] = None) -> dict: # type: ignore[return] # noqa E501 65 | """ 66 | Lookup all available APIs for given HRN. 67 | 68 | :param hrn: a HERE Resource Name identifying the resource 69 | :param region: an Optional param to look up a specific region for a given resource 70 | :return: The list of APIs that can be used with the resource 71 | """ 72 | path = f"/resources/{hrn}/apis" 73 | url = f"{self.base_url}{path}" 74 | params = dict(region=region) 75 | resp = self.get(url, params=params) 76 | if resp.status_code == 200: 77 | apis: dict = { 78 | el["api"]: {k: v for (k, v) in el.items() if k != "api"} 79 | for el in resp.json() 80 | if el["api"] in self.api_version_impl 81 | and el["version"] == self.api_version_impl[el["api"]] 82 | } 83 | return apis 84 | else: 85 | self.raise_response_exception(resp) 86 | 87 | def get_resource_api( # type: ignore[return] 88 | self, hrn: str, api: str, version: str, region: Optional[str] = None 89 | ) -> dict: 90 | """ 91 | Return details of a single API for a given resource identified by hrn, api and 92 | version. 93 | 94 | :param hrn: a HERE Resource Name identifying the resource 95 | :param api: The identifier of the API 96 | :param version: The version of the API 97 | :param region: an Optional param to look up a specific region for a given resource 98 | :return: Details of the requested API for the resource 99 | """ 100 | path = f"/resources/{hrn}/apis/{api}/{version}" 101 | url = f"{self.base_url}{path}" 102 | params = dict(region=region) 103 | resp = self.get(url, params=params) 104 | if resp.status_code == 200: 105 | return resp.json()[0] if resp.json() else dict() 106 | else: 107 | self.raise_response_exception(resp) 108 | -------------------------------------------------------------------------------- /xyzspaces/iml/auth.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020-2021 HERE Global B.V. and its affiliate(s). 2 | # All rights reserved. 3 | # 4 | # This software and other materials contain proprietary information 5 | # controlled by HERE and are protected by applicable copyright legislation. 6 | # Any use and utilization of this software and other materials and 7 | # disclosure to any third parties is conditional upon having a separate 8 | # agreement with HERE for the access, use, utilization or disclosure of this 9 | # software. In the absence of such agreement, the use of the software is not 10 | # allowed. 11 | 12 | """ 13 | This module provides an ``Auth`` class to authenticate an app on the platform. 14 | 15 | The authentication is based on some credentials object and will create an 16 | access token. It can be checked if the token is still valid, and it can be 17 | refreshed, too. 18 | """ 19 | 20 | 21 | from datetime import datetime, timedelta 22 | from typing import Optional 23 | 24 | from requests_oauthlib import OAuth1 25 | 26 | from xyzspaces.iml.apis.aaa_oauth2_api import AAAOauth2Api 27 | from xyzspaces.iml.credentials import Credentials 28 | 29 | 30 | class Auth: 31 | """ 32 | This class is responsible for authenticating with the HERE platform. 33 | 34 | It requires PlatformCredentials, AAAOauth2BaseApi object. 35 | """ 36 | 37 | def __init__(self, credentials: Credentials, aaa_oauth2_api: AAAOauth2Api): 38 | """ 39 | Instantiate authentication token. 40 | 41 | :param credentials: an instance of PlatformCredentials 42 | :param aaa_oauth2_api: an instance of AAAOauth2Api required 43 | in case of Credentials type. 44 | """ 45 | self.credentials = credentials 46 | self.aaa_oauth2_api = aaa_oauth2_api 47 | 48 | self._token: Optional[str] = None 49 | self._token_type: Optional[str] = None 50 | self._token_expires_in: Optional[int] = None 51 | self._token_requested_at: Optional[datetime] = None 52 | self._token_expires_at: Optional[datetime] = None 53 | self._scope: Optional[str] = None 54 | 55 | @property 56 | def token(self) -> Optional[str]: 57 | """ 58 | Return the current token or requests a new one if needed. 59 | 60 | :return: a valid token 61 | """ 62 | if not self.token_still_valid(): 63 | self.generate_token() 64 | return self._token 65 | 66 | def token_still_valid(self) -> bool: 67 | """ 68 | Check whether the auth token is still valid or expired. 69 | 70 | :return: a boolean indicating if a token is still valid. 71 | """ 72 | if not self._token or not self._token_expires_at: 73 | return False 74 | return datetime.now() < (self._token_expires_at - timedelta(seconds=60)) 75 | 76 | def generate_token(self): 77 | """ 78 | Authenticate with the HERE account service and retrieve a new token. 79 | """ 80 | oauth = OAuth1( 81 | self.credentials.cred_properties["key"], 82 | client_secret=self.credentials.cred_properties["secret"], 83 | signature_method="HMAC-SHA256", 84 | ) 85 | response_json = self.aaa_oauth2_api.request_scoped_access_token( 86 | oauth, data="grant_type=client_credentials" 87 | ) 88 | 89 | self._token = response_json.get("access_token") 90 | self._token_type = response_json.get("token_type") 91 | self._token_expires_in = int(response_json.get("expires_in")) 92 | self._token_requested_at = datetime.now() 93 | self._token_expires_at = self._token_requested_at + timedelta( 94 | seconds=self._token_expires_in 95 | ) 96 | -------------------------------------------------------------------------------- /xyzspaces/iml/catalog.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | """This module defines Catalog class.""" 18 | 19 | from typing import Dict, Optional 20 | 21 | from xyzspaces.iml.apis.aaa_oauth2_api import AAAOauth2Api 22 | from xyzspaces.iml.apis.data_config_api import DataConfigApi 23 | from xyzspaces.iml.apis.data_interactive_api import DataInteractiveApi 24 | from xyzspaces.iml.apis.lookup_api import LookupApi 25 | from xyzspaces.iml.auth import Auth 26 | from xyzspaces.iml.credentials import Credentials 27 | 28 | 29 | class Catalog: 30 | """A class to define catalog.""" 31 | 32 | def __init__( 33 | self, 34 | hrn: str, 35 | credentials: Optional[Credentials] = None, 36 | proxies: Optional[dict] = None, 37 | ): 38 | self.hrn = hrn 39 | self.credentials = credentials or Credentials.from_default() 40 | self.proxies = proxies 41 | self.aaa_oauth2_api = AAAOauth2Api( 42 | base_url=self.credentials.cred_properties["endpoint"], 43 | proxies=proxies, 44 | ) 45 | self.auth = Auth(self.credentials, aaa_oauth2_api=self.aaa_oauth2_api) 46 | self.lookup_api = LookupApi( 47 | auth=self.auth, 48 | proxies=self.proxies, 49 | ) 50 | resource_apis = self.lookup_api.get_resource_api_list(hrn) 51 | self._data_interactive_api: DataInteractiveApi = DataInteractiveApi( 52 | base_url=resource_apis["interactive"]["baseURL"], 53 | auth=self.auth, 54 | proxies=proxies, 55 | ) 56 | self._data_config_api = DataConfigApi(auth=self.auth, proxies=proxies) 57 | 58 | def get_details(self) -> Dict: 59 | """ 60 | Get catalog details. 61 | :return: Dict 62 | """ 63 | cat_config = self._data_config_api.get_catalog_details(catalog_hrn=self.hrn) 64 | return cat_config 65 | -------------------------------------------------------------------------------- /xyzspaces/iml/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | """This module defines all the exceptions for iml package.""" 18 | 19 | 20 | class AuthenticationException(Exception): 21 | """ 22 | This ``AuthenticationException`` is raised either authentication 23 | or authorization on the platform fails. 24 | """ 25 | 26 | def __init__(self, resp): 27 | """ 28 | Instantiate AuthenticationException . 29 | :param resp: response detail will be stored in this param 30 | """ 31 | 32 | self.resp = resp 33 | 34 | def __str__(self) -> str: 35 | """ 36 | Return the message to be raised for this exception. 37 | 38 | :return: error message 39 | """ 40 | return """An error occurred during authentication or authorization with HERE 41 | platform: Status {status} - 42 | Reason {reason}\n Response: {body}""".format( 43 | status=self.resp.status_code, 44 | reason=self.resp.reason, 45 | body=self.resp.text, 46 | ) 47 | 48 | 49 | class TooManyRequestsException(Exception): 50 | """Exception raised for API HTTP response status code 429. 51 | 52 | This is a dedicated exception to be used with the `backoff` package, because 53 | it requires a specific exception class. 54 | The exception value will be the response object returned by :mod:`requests` 55 | which provides access to all its attributes, eg. :attr:`status_code`, 56 | :attr:`reason` and :attr:`text`, etc. 57 | """ 58 | 59 | def __init__(self, resp): 60 | """ 61 | Instantiate AuthenticationException . 62 | :param resp: response detail will be stored in this param 63 | """ 64 | 65 | self.resp = resp 66 | 67 | def __str__(self): 68 | """Return a string from the HTTP response causing the exception. 69 | 70 | The string simply lists the response status code, reason and text 71 | content, separated with commas. 72 | """ 73 | 74 | return "TooManyRequestsException: Status \ 75 | {status} - Reason {reason}\n\n" "Response: {body}".format( 76 | status=self.resp.status_code, 77 | reason=self.resp.reason, 78 | body=self.resp.text, 79 | ) 80 | 81 | 82 | class ConfigException(Exception): 83 | """ 84 | This ``ConfigException`` is raised whenever there is any error related to 85 | platform configuration. 86 | """ 87 | 88 | 89 | class PayloadTooLargeException(Exception): 90 | """Exception raised for API HTTP response status code 513. 91 | 92 | This is a dedicated exception to be used for interactive map layer. 93 | This exception will be raised when response payload is larger than the 94 | specified limits of the interactive map layer. 95 | The exception value will be the response object returned by :mod:`requests` 96 | which provides access to all its attributes, eg. :attr:`status_code`, 97 | :attr:`reason` and :attr:`text`, etc. 98 | """ 99 | 100 | def __init__(self, resp): 101 | """ 102 | Instantiate AuthenticationException . 103 | :param resp: response detail will be stored in this param 104 | """ 105 | 106 | self.resp = resp 107 | 108 | def __str__(self): 109 | """Return a string from the HTTP response causing the exception. 110 | 111 | The string simply lists the response status code, reason and text 112 | content, separated with commas. 113 | """ 114 | 115 | return ( 116 | "PayloadTooLargeException: Status \ 117 | {status} - Reason {reason}\n\n" 118 | "Response: {body}".format( 119 | status=self.resp.status_code, reason=self.resp.reason, body=self.resp.text 120 | ) 121 | ) 122 | 123 | 124 | class RequestEntityTooLargeException(Exception): 125 | """Exception raised for API HTTP response status code 413. 126 | 127 | This is a dedicated exception to be used for interactive map layer. 128 | This exception will be raised when request body is larger than the 129 | specified limits of the interactive map layer. 130 | The exception value will be the response object returned by :mod:`requests` 131 | which provides access to all its attributes, eg. :attr:`status_code`, 132 | :attr:`reason` and :attr:`text`, etc. 133 | """ 134 | 135 | def __init__(self, resp): 136 | """ 137 | Instantiate AuthenticationException . 138 | :param resp: response detail will be stored in this param 139 | """ 140 | 141 | self.resp = resp 142 | 143 | def __str__(self): 144 | """Return a string from the HTTP response causing the exception. 145 | 146 | The string simply lists the response status code, reason and text 147 | content, separated with commas. 148 | """ 149 | 150 | return ( 151 | "RequestEntityTooLargeException: Status \ 152 | {status} - Reason {reason}\n\n" 153 | "Response: {body}".format( 154 | status=self.resp.status_code, reason=self.resp.reason, body=self.resp.text 155 | ) 156 | ) 157 | -------------------------------------------------------------------------------- /xyzspaces/logconf.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """This module configures logging.""" 19 | 20 | import json 21 | import logging 22 | import logging.config 23 | import os 24 | 25 | 26 | def setup_logging( 27 | default_path: str = "config/logconfig.json", 28 | default_level: int = logging.ERROR, 29 | env_key: str = "XYZ_LOG_CONFIG", 30 | ): 31 | """Set up logging configuration. 32 | 33 | :param default_path: A string representing the path of the config file in JSON format. 34 | :param default_level: An int representing logging level. 35 | :param env_key: A string representing environment variable to enable logging to file. 36 | """ 37 | path = default_path 38 | value = os.environ.get(env_key) 39 | if value: 40 | path = value 41 | if os.path.exists(path): 42 | with open(path, "rt") as f: 43 | config = json.load(f) 44 | logging.config.dictConfig(config) 45 | else: 46 | logging.basicConfig( 47 | level=default_level, 48 | format="%(asctime)s - %(filename)s:%(lineno)d - %(levelname)s" 49 | "- %(message)s", 50 | ) 51 | 52 | 53 | # Set up logging to ``/dev/null`` like a library is supposed to. 54 | # http://docs.python.org/3.3/howto/logging.html#configuring-logging-for-a-library 55 | 56 | 57 | class NullHandler(logging.Handler): 58 | """NullHandler class is a 'no-op' handler for use by library developers.""" 59 | 60 | def emit(self, record): 61 | """Skip the emit record. This is used to give preference to user.""" 62 | pass 63 | 64 | 65 | logger = logging.getLogger("xyzspaces") 66 | logger.addHandler(NullHandler()) 67 | -------------------------------------------------------------------------------- /xyzspaces/tools.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019-2021 HERE Europe B.V. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | # License-Filename: LICENSE 17 | 18 | """This is a preliminary collection of little tools that use XYZ.""" 19 | 20 | from typing import List, Optional 21 | 22 | from .apis import HubApi 23 | from .config.default import XYZConfig 24 | 25 | 26 | def subset_geojson( 27 | gj: dict, 28 | config: Optional[XYZConfig] = None, 29 | bbox: Optional[List[float]] = None, 30 | tile_type: Optional[str] = None, 31 | tile_id: Optional[str] = None, 32 | clip: Optional[bool] = False, 33 | lat: Optional[float] = None, 34 | lon: Optional[float] = None, 35 | radius: Optional[int] = None, 36 | ) -> dict: 37 | """Return a subset of the GeoJSON object inside some bbox or map tile or radius. 38 | 39 | This will create a temporary space, add the provided GeoJSON object, 40 | perform the bbox or tile subsetting and return the resulting GeoJSON 41 | object after deleting the temporary space again. 42 | 43 | :param config: An object of `class:XYZConfig`, If not provied 44 | ``XYZ_TOKEN`` will be used from environment variable and 45 | other configurations will be used as defined in :py:mod:`default_config`. 46 | :param gj: The GeoJSON data object. 47 | :param bbox: The bounding box described as a list of four numbers (its 48 | West, South, East, and North margins). 49 | :param tile_type: The tile type, for now one of: ... 50 | :param tile_id: The tile ID, a string composed of digits to identify 51 | the tile according to the specified tile type. 52 | :param clip: A Boolean to indicate if the features should be clipped 53 | at the tile or bbox. 54 | :param lat: A float to represent latitude. 55 | :param lon: A float to represent longitude. 56 | :param radius: An int in meter which defines the diameter of the search request. 57 | Should be provided with ``lat`` and ``lon`` for spatial search. 58 | :returns: A GeoJSON object covering the desired tile or bbox subset of 59 | the original GeoJSON object. 60 | :raises ValueError: If the wrong combination of ``bbox``, ``tile_type`` 61 | and ``tile_id``, ``lat`` and ``lon`` was provided. 62 | """ 63 | if bbox and (tile_type or tile_id): 64 | raise ValueError("The bbox cannot be provided together with tile ID/type.") 65 | elif bbox and (lat or lon): 66 | raise ValueError("The bbox cannot be provided together with lat and lon.") 67 | elif (tile_type or tile_id) and (lat or lon): 68 | raise ValueError( 69 | "The tile_id and tile_type cannot be provided together with " "lat and lon." 70 | ) 71 | if not bbox: 72 | assert (tile_type and tile_id) or ( 73 | lat and lon 74 | ), "Tile ID and type or lat and lon must be provided." 75 | if bbox: 76 | assert len(bbox) == 4 77 | 78 | api = HubApi(config=config if config else XYZConfig.from_default()) 79 | 80 | # Create space. 81 | res = api.post_space(data=dict(title="Tile GeoJSON", description="Temporary space.")) 82 | space_id = res["id"] 83 | 84 | # Add features. 85 | api.put_space_features(space_id=space_id, data=gj) 86 | 87 | if bbox: 88 | tiled = api.get_space_bbox(space_id=space_id, bbox=bbox, clip=clip) 89 | elif tile_type and tile_id: 90 | tiled = api.get_space_tile( 91 | space_id=space_id, tile_type=tile_type, tile_id=tile_id, clip=True 92 | ) 93 | elif lat and lon: 94 | tiled = api.get_space_spatial(space_id=space_id, lat=lat, lon=lon, radius=radius) 95 | 96 | # Delete space. 97 | api.delete_space(space_id=space_id) 98 | 99 | return tiled 100 | --------------------------------------------------------------------------------