├── .github └── workflows │ ├── build-docs.yml │ ├── ci.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── admin.py ├── docker-compose.yml ├── docs ├── about.md ├── assets │ ├── favicon.png │ └── logo.png ├── changelog.md ├── development.md ├── images │ ├── add-connection.png │ ├── add-repository.png │ ├── add_footprint_latest.png │ ├── add_footprint_planetary.png │ ├── add_stac_connectin.png │ ├── added_asset_planetary.png │ ├── assets_dialog.png │ ├── available_filters_planetary.png │ ├── filters.png │ ├── footprint.png │ ├── install-from-repository.png │ ├── install-from-zip.png │ ├── install_from_zip.png │ ├── ndvi.png │ ├── plugin_menu.gif │ ├── plugin_settings.png │ ├── raster.png │ ├── results.png │ ├── results_latest.png │ ├── search_result_stac_api_plugin.png │ ├── search_results_planetary.png │ ├── sentinel_bands.png │ ├── stac-plugin.png │ ├── toolbar.png │ ├── view_assets.png │ ├── view_assets_planetary.png │ └── web_menu.gif ├── index.md ├── installation.md ├── plugin │ └── changelog.txt ├── stylesheets │ └── extra.css ├── tutorial.md └── user-guide.md ├── i18n └── af.ts ├── mkdocs.yml ├── poetry.lock ├── pyproject.toml ├── resources ├── icon.png └── resources.qrc ├── run-tests.sh ├── scripts ├── compile-strings.sh ├── docker │ ├── qgis-stac-test-pre-scripts.sh │ └── qgis-testing-entrypoint.sh └── update-strings.sh ├── src └── qgis_stac │ ├── __init__.py │ ├── api │ ├── __init__.py │ ├── base.py │ ├── client.py │ ├── models.py │ └── network.py │ ├── conf.py │ ├── definitions │ ├── __init__.py │ ├── catalog.py │ └── constants.py │ ├── gui │ ├── __init__.py │ ├── asset_widget.py │ ├── assets_dialog.py │ ├── collection_dialog.py │ ├── connection_dialog.py │ ├── json_highlighter.py │ ├── qgis_stac_widget.py │ ├── queryable_property.py │ ├── result_item_model.py │ └── result_item_widget.py │ ├── jobs │ ├── __init__.py │ └── token_manager.py │ ├── lib │ ├── __init__.py │ ├── planetary_computer │ │ ├── LICENSE │ │ ├── __init__.py │ │ ├── sas.py │ │ ├── scripts │ │ │ ├── __init__.py │ │ │ └── cli.py │ │ ├── settings.py │ │ ├── utils.py │ │ └── version.py │ ├── pydantic │ │ ├── LICENSE │ │ ├── __init__.py │ │ ├── _hypothesis_plugin.py │ │ ├── annotated_types.py │ │ ├── class_validators.py │ │ ├── color.py │ │ ├── dataclasses.py │ │ ├── datetime_parse.py │ │ ├── decorator.py │ │ ├── env_settings.py │ │ ├── error_wrappers.py │ │ ├── errors.py │ │ ├── fields.py │ │ ├── generics.py │ │ ├── json.py │ │ ├── main.py │ │ ├── mypy.py │ │ ├── networks.py │ │ ├── parse.py │ │ ├── py.typed │ │ ├── schema.py │ │ ├── tools.py │ │ ├── types.py │ │ ├── typing.py │ │ ├── utils.py │ │ ├── validators.py │ │ └── version.py │ ├── pystac │ │ ├── LICENSE │ │ ├── __init__.py │ │ ├── asset.py │ │ ├── cache.py │ │ ├── catalog.py │ │ ├── collection.py │ │ ├── common_metadata.py │ │ ├── errors.py │ │ ├── extensions │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── datacube.py │ │ │ ├── eo.py │ │ │ ├── file.py │ │ │ ├── hooks.py │ │ │ ├── item_assets.py │ │ │ ├── label.py │ │ │ ├── pointcloud.py │ │ │ ├── projection.py │ │ │ ├── raster.py │ │ │ ├── sar.py │ │ │ ├── sat.py │ │ │ ├── scientific.py │ │ │ ├── table.py │ │ │ ├── timestamps.py │ │ │ ├── version.py │ │ │ └── view.py │ │ ├── item.py │ │ ├── item_collection.py │ │ ├── layout.py │ │ ├── link.py │ │ ├── media_type.py │ │ ├── provider.py │ │ ├── py.typed │ │ ├── rel_type.py │ │ ├── serialization │ │ │ ├── __init__.py │ │ │ ├── common_properties.py │ │ │ ├── identify.py │ │ │ └── migrate.py │ │ ├── stac_io.py │ │ ├── stac_object.py │ │ ├── summaries.py │ │ ├── utils.py │ │ ├── validation │ │ │ ├── __init__.py │ │ │ ├── schema_uri_map.py │ │ │ └── stac_validator.py │ │ └── version.py │ ├── pystac_client │ │ ├── LICENSE │ │ ├── __init__.py │ │ ├── cli.py │ │ ├── client.py │ │ ├── collection_client.py │ │ ├── conformance.py │ │ ├── exceptions.py │ │ ├── item_search.py │ │ ├── stac_api_io.py │ │ └── version.py │ ├── typing │ │ └── LICENSE │ └── typing_extensions.py │ ├── main.py │ ├── ui │ ├── asset_widget.ui │ ├── collection_dialog.ui │ ├── connection_dialog.ui │ ├── item_assets_widget.ui │ ├── qgis_stac_main.ui │ ├── queryable_property.ui │ └── result_item_widget.ui │ └── utils.py ├── test ├── __init__.py ├── mock │ ├── __init__.py │ ├── data │ │ ├── catalog.json │ │ ├── collection.json │ │ ├── collections.json │ │ ├── conformance.json │ │ ├── first_item.json │ │ ├── fourth_item.json │ │ ├── search.json │ │ ├── search_sorted.json │ │ ├── second_item.json │ │ └── third_item.json │ ├── mock_http_server.py │ ├── stac_api_auth_server_app.py │ └── stac_api_server_app.py ├── qgis_interface.py ├── tenbytenraster.asc ├── tenbytenraster.asc.aux.xml ├── tenbytenraster.keywords ├── tenbytenraster.lic ├── tenbytenraster.prj ├── tenbytenraster.qml ├── tenbytenraster.tif ├── tenbytenraster.tif.aux.xml ├── test_init.py ├── test_qgis_environment.py ├── test_settings_manager.py ├── test_stac_api_client_auth.py ├── test_stac_api_client_functions.py ├── test_translations.py └── utilities_for_testing.py └── test_suite.py /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | name: Build documentation 2 | on: workflow_dispatch 3 | 4 | jobs: 5 | build-docs: 6 | runs-on: ubuntu-22.04 7 | container: 8 | image: qgis/qgis:release-3_34 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | 13 | - name: Fix Python command 14 | run: apt-get install python-is-python3 15 | 16 | - name: Install poetry 17 | uses: Gr1N/setup-poetry@v8 18 | 19 | - name: Install plugin dependencies 20 | run: poetry install 21 | 22 | - name: Generate plugin repo XML 23 | run: poetry run python admin.py --verbose generate-plugin-repo-xml 24 | 25 | - name: Update the documentation 26 | run: poetry run mkdocs gh-deploy --force -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: 9 | - edited 10 | - opened 11 | - reopened 12 | - synchronize 13 | branches: 14 | - main 15 | 16 | env: 17 | # Global environment variable 18 | IMAGE: qgis/qgis 19 | WITH_PYTHON_PEP: "true" 20 | MUTE_LOGS: "true" 21 | 22 | jobs: 23 | test: 24 | runs-on: ubuntu-22.04 25 | name: Running tests on QGIS ${{ matrix.qgis_version_tag }} 26 | 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | qgis_version_tag: 31 | - release-3_16 32 | - release-3_22 33 | - release-3_34 34 | - release-3_36 35 | 36 | steps: 37 | 38 | - name: Checkout 39 | uses: actions/checkout@v2 40 | with: 41 | submodules: recursive 42 | 43 | - name: Preparing docker-compose environment 44 | env: 45 | QGIS_VERSION_TAG: ${{ matrix.qgis_version_tag }} 46 | run: | 47 | cat << EOF > .env 48 | QGIS_VERSION_TAG=${QGIS_VERSION_TAG} 49 | IMAGE=${IMAGE} 50 | ON_TRAVIS=true 51 | MUTE_LOGS=${MUTE_LOGS} 52 | WITH_PYTHON_PEP=${WITH_PYTHON_PEP} 53 | EOF 54 | - name: Install poetry 55 | uses: Gr1N/setup-poetry@v8 56 | with: 57 | poetry-version: 1.2.2 58 | 59 | - name: Install plugin dependencies 60 | run: poetry install 61 | 62 | - name: Preparing test environment 63 | run: | 64 | cat .env 65 | docker pull "${IMAGE}":${{ matrix.qgis_version_tag }} 66 | poetry run python admin.py build --tests 67 | docker-compose up -d 68 | sleep 10 69 | 70 | - name: Run test suite 71 | run: | 72 | docker-compose exec -T qgis-testing-environment sh -c "pip3 install flask" 73 | docker-compose exec -T qgis-testing-environment qgis_testrunner.sh test_suite.test_package -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create a release 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | 7 | jobs: 8 | create-release: 9 | runs-on: ubuntu-22.04 10 | container: 11 | image: qgis/qgis:release-3_34 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - name: Fix Python command 17 | run: apt-get install python-is-python3 18 | 19 | - name: Install poetry 20 | uses: Gr1N/setup-poetry@v8 21 | 22 | - name: Install plugin dependencies 23 | run: poetry install 24 | 25 | - name: Get experimental info 26 | id: get-experimental 27 | run: | 28 | echo "::set-output name=IS_EXPERIMENTAL::$(poetry run python -c "import toml; data=toml.load('pyproject.toml'); print(data['tool']['qgis-plugin']['metadata']['experimental'].lower())")" 29 | 30 | - name: Create release from tag 31 | id: create-release 32 | uses: actions/create-release@v1 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | with: 36 | tag_name: ${{ github.ref }} 37 | release_name: Release ${{ github.ref }} 38 | prerelease: ${{ steps.get-experimental.outputs.IS_EXPERIMENTAL }} 39 | draft: false 40 | 41 | - name: Generate zip 42 | run: poetry run python admin.py generate-zip 43 | 44 | - name: get zip details 45 | id: get-zip-details 46 | run: | 47 | echo "::set-output name=ZIP_PATH::dist/$(ls dist)\n" 48 | echo "::set-output name=ZIP_NAME::$(ls dist)" 49 | 50 | - name: Upload release asset 51 | id: upload-release-asset 52 | uses: actions/upload-release-asset@v1 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | with: 56 | upload_url: ${{ steps.create-release.outputs.upload_url}} 57 | asset_path: ${{ steps.get-zip-details.outputs.ZIP_PATH}} 58 | asset_name: ${{ steps.get-zip-details.outputs.ZIP_NAME}} 59 | asset_content_type: application/zip 60 | 61 | - name: Generate plugin repo XML 62 | run: poetry run python admin.py --verbose generate-plugin-repo-xml 63 | 64 | - name: Update release repository 65 | run: poetry run mkdocs gh-deploy --force 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | pip-wheel-metadata/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # pipenv 87 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 88 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 89 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 90 | # install all needed dependencies. 91 | #Pipfile.lock 92 | 93 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 94 | __pypackages__/ 95 | 96 | # Celery stuff 97 | celerybeat-schedule 98 | celerybeat.pid 99 | 100 | # SageMath parsed files 101 | *.sage.py 102 | 103 | # Environments 104 | .env 105 | .venv 106 | env/ 107 | venv/ 108 | ENV/ 109 | env.bak/ 110 | venv.bak/ 111 | 112 | # Spyder project settings 113 | .spyderproject 114 | .spyproject 115 | 116 | # Rope project settings 117 | .ropeproject 118 | 119 | # mkdocs documentation 120 | /site 121 | 122 | # mypy 123 | .mypy_cache/ 124 | .dmypy.json 125 | dmypy.json 126 | 127 | # Pyre type checker 128 | .pyre/ 129 | 130 | # Development 131 | .idea/ 132 | build/ 133 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Released 4 | 5 | ### 1.1.0 2022-07-18 6 | - Fix for footprints layer loading workflow 7 | - Data driven filtering using STAC Queryables 8 | - Multiple assets and footprints loading and downloading 9 | - Minimizeable plugin main window 10 | - Subscription key usage for SAS based connections 11 | - Support for COPC layers 12 | - Support for netCDF layers 13 | - New collection dialog 14 | - Auto assets loading after downloading assets 15 | - Fixed connection dialog window title when in edit mode 16 | - Fallback to overview when item thumbnail asset is not available 17 | - Display selected collections 18 | - Upgraded pystac-client library to 0.3.2 19 | - Support for CQL2-JSON filter language 20 | - Moved sort and order buttons to search tab 21 | 22 | ### 1.0.0 2022-01-13 23 | - Fix for plugin UI lagging bug. 24 | - Updates to loading and downloading assets workflow. 25 | - Support for adding vector based assets eg. GeoJSON, GeoPackage 26 | - Fix API page size now default is 10 items. 27 | - Include extension in the downloaded files. 28 | - Update UI with more descriptive tooltips. 29 | 30 | ## [Unreleased] 31 | 32 | ### 1.0.0-pre 2022-01-11 33 | - Changed loading and downloading assets workflow [#93](https://github.com/stac-utils/qgis-stac-plugin/pull/93). 34 | - Implemented testing connection functionality. 35 | - Reworked filter and sort features on the search item results. 36 | - Fetch for STAC API conformance classes [#82](https://github.com/stac-utils/qgis-stac-plugin/pull/82). 37 | - Added STAC API signing using SAS token [#79](https://github.com/stac-utils/qgis-stac-plugin/pull/79). 38 | - Support for downloading assets and loading item footprints in QGIS, [#70](https://github.com/stac-utils/qgis-stac-plugin/pull/70). 39 | - Enabled adding STAC item assets as map layers in QGIS [#58](https://github.com/stac-utils/qgis-stac-plugin/pull/58). 40 | - Added plugin documentation in GitHub pages. 41 | 42 | ## [beta] 43 | 44 | ### 1.0.0-beta 2021-12-11 45 | - Fixed slow item search. 46 | - Updated plugin search result to include pagination [#51](https://github.com/stac-utils/qgis-stac-plugin/pull/51). 47 | - Support for search result filtering and sorting [#47](https://github.com/stac-utils/qgis-stac-plugin/pull/47). 48 | - Implemented search [#40](https://github.com/stac-utils/qgis-stac-plugin/pull/40). 49 | - Added default configured STAC API catalogs [#26](https://github.com/stac-utils/qgis-stac-plugin/pull/26). 50 | - Basic STAC API support [#17](https://github.com/stac-utils/qgis-stac-plugin/pull/17). 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qgis-stac-plugin 2 | 3 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/stac-utils/qgis-stac-plugin/ci.yml?branch=main) 4 | ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/stac-utils/qgis-stac-plugin?include_prereleases) 5 | ![GitHub](https://img.shields.io/github/license/stac-utils/qgis-stac-plugin) 6 | 7 | QGIS plugin for reading STAC APIs 8 | 9 | Site https://stac-utils.github.io/qgis-stac-plugin 10 | 11 | **The QGIS STAC API Browser currently lacks funding for maintenance, 12 | bug fixes and new features; therefore development will be slow for now. 13 | However we’re dedicated to maintaining the project. 14 | For assistance or if you have funding to contribute 15 | please reach out to Kartoza ([info@kartoza.com](mailto:info@kartoza.com))** 16 | 17 | ### Installation 18 | 19 | During the development phase the plugin is available to install via 20 | a dedicated plugin repository 21 | https://stac-utils.github.io/qgis-stac-plugin/repository/plugins.xml 22 | 23 | Open the QGIS plugin manager, then select the **Settings** page, click **Add** 24 | button on the **Plugin Repositories** group box and use the above url to create 25 | the new plugin repository. 26 | ![Add plugin repository](docs/images/plugin_settings.png) 27 | 28 | After adding the new repository, the plugin should be available from the list 29 | of all plugins that can be installed. 30 | 31 | **NOTE:** While the development phase is on going the plugin will be flagged as experimental, make 32 | sure to enable the QGIS plugin manager in the **Settings** page to show the experimental plugins 33 | in order to be able to install it. 34 | 35 | Alternatively the plugin can be installed using **Install from ZIP** option on the 36 | QGIS plugin manager. Download zip file from the required plugin released version 37 | https://github.com/stac-utils/qgis-stac-plugin/releases/download/{tagname}/qgis_stac.{version}.zip. 38 | 39 | From the **Install from ZIP** page, select the zip file and click the **Install** button to install 40 | plugin 41 | ![Screenshot for install from zip option](docs/images/install_from_zip.png) 42 | 43 | When the development work is complete the plugin will be available on the QGIS 44 | official plugin repository. 45 | 46 | 47 | #### Development 48 | 49 | To use the plugin for development purposes, clone the repository locally, 50 | install poetry, a python dependencies management tool see https://python-poetry.org/docs/#installation 51 | then using the poetry tool, update the poetry lock file and install plugin dependencies by running 52 | ``` 53 | poetry update --lock 54 | poetry install --no-dev 55 | ``` 56 | 57 | To install the plugin into the QGIS application use the below command 58 | ``` 59 | poetry run python admin.py install 60 | ``` 61 | 62 | 63 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | services: 3 | qgis-testing-environment: 4 | image: ${IMAGE}:${QGIS_VERSION_TAG} 5 | volumes: 6 | - ./build/qgis_stac:/tests_directory:ro 7 | environment: 8 | QGIS_VERSION_TAG: "${QGIS_VERSION_TAG}" 9 | WITH_PYTHON_PEP: "${WITH_PYTHON_PEP}" 10 | ON_TRAVIS: "${ON_TRAVIS}" 11 | MUTE_LOGS: "${MUTE_LOGS}" 12 | DISPLAY: ":99" 13 | working_dir: /tests_directory 14 | entrypoint: /tests_directory/scripts/docker/qgis-testing-entrypoint.sh 15 | # Enable "command:" line below to immediately run unittests upon docker-compose up 16 | # command: qgis_testrunner.sh test_suite.test_package 17 | # Default behaviour of the container is to standby 18 | command: tail -f /dev/null 19 | # qgis_testrunner.sh needs tty for tee 20 | tty: true -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | --- 5 | 6 | ## STAC 7 | 8 | The SpatialTemporal Asset Catalog provides a standard way of describing and exposing geospatial data. 9 | A 'spatiotemporal asset' is any file that represents information about the earth captured in a certain space and time. 10 | See [https://stacspec.org/](https://stacspec.org/) for more information about the STAC specification. 11 | 12 | ## QGIS & STAC 13 | At the moment of developing the STAC API Browser plugin, there was no other plugin available on the official QGIS 14 | plugin repository that fully supported using STAC API services inside QGIS. 15 | 16 | However, there was a plugin called "STAC Browser" in the QGIS plugin repository, the plugin was not updated to use 17 | the latest stable release of the STAC API and was not being actively maintained. 18 | 19 | ## External Libraries 20 | The STAC API Browser uses a couple of external libraries to achieve its functionalities. The following are the libraries 21 | used in the plugin. 22 | 23 | - **PySTAC Client** a python package for working with STAC Catalogs and APIs that conform to the STAC and STAC API specs, 24 | [https://pystac-client.readthedocs.io/en/latest/](https://pystac-client.readthedocs.io/en/latest/). 25 | - **Planetary Computer** python library for interacting with Microsoft Planetary Computer STAC API services 26 | [https://pypi.org/project/planetary-computer/](https://pypi.org/project/planetary-computer/). 27 | -------------------------------------------------------------------------------- /docs/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/assets/favicon.png -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/assets/logo.png -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | --8<-- 2 | CHANGELOG.md 3 | --8<-- -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | --- 5 | 6 | ## Install instructions 7 | 8 | * Fork the repository [https://github.com/stac-utils/qgis-stac-plugin](https://github.com/stac-utils/qgis-stac-plugin) 9 | * Clone the repository locally: 10 | 11 | git clone https://github.com/stac-utils/qgis-stac-plugin.git 12 | 13 | Install poetry: 14 | 15 | Poetry is a python dependencies management tool see [https://python-poetry.org/docs/#installation](https://python-poetry.org/docs/#installation) then using the poetry tool, update the poetry lock file and install plugin dependencies by running 16 | 17 | cd qgis-stac-plugin 18 | 19 | poetry update --lock 20 | 21 | poetry install 22 | 23 | ## Install the plugin into QGIS 24 | 25 | Use the below command to install the plugin into QGIS 26 | 27 | poetry run python admin.py install 28 | 29 | ## Testing 30 | 31 | The plugin contains a bash script `run-tests.sh` in the root folder that can be used to run the 32 | all the plugin tests locally for QGIS 3.16 and 3.20 versions on a linux based OS. 33 | The script uses the QGIS official docker images, in order to use it, docker images for QGIS version 3.16 and 3.20 34 | need to be present. 35 | 36 | Run the following commands in linux shell to pull the images and execute the script for tests. 37 | 38 | ``` 39 | docker pull qgis/qgis:release-3_16 40 | docker pull qgis/qgis:release-3_22 41 | ``` 42 | 43 | ``` 44 | ./run-tests.sh 45 | ``` 46 | 47 | GitHub actions workflow is provided by the plugin to run tests in QGIS 3.16, 3.18, 3.20 and 3.22 versions in 48 | the plugin repository, the workflow is located in the following directory `.github/workflow/ci.yml` 49 | 50 | 51 | ## Building documentation 52 | 53 | Plugin uses a [mkdocs-material](https://squidfunk.github.io/mkdocs-material/) theme for the github pages documentation site, 54 | to run locally the site run the following 55 | commands after making updates to the documentation pages that are located inside the `docs` plugin folder. 56 | 57 | ``` 58 | poetry run mkdocs serve 59 | ``` 60 | 61 | This will create a local hosted site, that can be accessed via "localhost:8080". 62 | 63 | For more options available via the `mkdocs` run the following commands. 64 | 65 | ``` 66 | poetry run mkdocs --help 67 | ``` 68 | 69 | or 70 | 71 | ``` 72 | poetry run mkdocs command --help 73 | ``` 74 | where command can be `serve` or `build`. 75 | 76 | Whenever the poetry dependencies have changed, the poetry lock file should be updated and new packages should be installed. -------------------------------------------------------------------------------- /docs/images/add-connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/add-connection.png -------------------------------------------------------------------------------- /docs/images/add-repository.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/add-repository.png -------------------------------------------------------------------------------- /docs/images/add_footprint_latest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/add_footprint_latest.png -------------------------------------------------------------------------------- /docs/images/add_footprint_planetary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/add_footprint_planetary.png -------------------------------------------------------------------------------- /docs/images/add_stac_connectin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/add_stac_connectin.png -------------------------------------------------------------------------------- /docs/images/added_asset_planetary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/added_asset_planetary.png -------------------------------------------------------------------------------- /docs/images/assets_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/assets_dialog.png -------------------------------------------------------------------------------- /docs/images/available_filters_planetary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/available_filters_planetary.png -------------------------------------------------------------------------------- /docs/images/filters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/filters.png -------------------------------------------------------------------------------- /docs/images/footprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/footprint.png -------------------------------------------------------------------------------- /docs/images/install-from-repository.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/install-from-repository.png -------------------------------------------------------------------------------- /docs/images/install-from-zip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/install-from-zip.png -------------------------------------------------------------------------------- /docs/images/install_from_zip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/install_from_zip.png -------------------------------------------------------------------------------- /docs/images/ndvi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/ndvi.png -------------------------------------------------------------------------------- /docs/images/plugin_menu.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/plugin_menu.gif -------------------------------------------------------------------------------- /docs/images/plugin_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/plugin_settings.png -------------------------------------------------------------------------------- /docs/images/raster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/raster.png -------------------------------------------------------------------------------- /docs/images/results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/results.png -------------------------------------------------------------------------------- /docs/images/results_latest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/results_latest.png -------------------------------------------------------------------------------- /docs/images/search_result_stac_api_plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/search_result_stac_api_plugin.png -------------------------------------------------------------------------------- /docs/images/search_results_planetary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/search_results_planetary.png -------------------------------------------------------------------------------- /docs/images/sentinel_bands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/sentinel_bands.png -------------------------------------------------------------------------------- /docs/images/stac-plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/stac-plugin.png -------------------------------------------------------------------------------- /docs/images/toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/toolbar.png -------------------------------------------------------------------------------- /docs/images/view_assets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/view_assets.png -------------------------------------------------------------------------------- /docs/images/view_assets_planetary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/view_assets_planetary.png -------------------------------------------------------------------------------- /docs/images/web_menu.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/docs/images/web_menu.gif -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # QGIS STAC API Browser 2 | 3 | STAC API Browser is a QGIS plugin that allows the exploration of geospatial data from providers that support 4 | the STAC API specification 5 | [https://github.com/radiantearth/stac-api-spec](https://github.com/radiantearth/stac-api-spec). 6 | 7 | STAC API Browser provides a simple and user-friendly approach in searching and using STAC API resources in QGIS. 8 | It offers a comfortable way to browse the STAC API items and ability to add the 9 | STAC API assets as map layers into the QGIS application. 10 | 11 | The plugin was built through a joint effort between [Kartoza](https://kartoza.com/) and 12 | [Microsoft](https://planetarycomputer.microsoft.com/). 13 | 14 | ![image](images/search_result_stac_api_plugin.png) 15 | 16 | _Example search results, showing available items on a STAC API service_. 17 | 18 | 19 | ## Quick Installation 20 | The plugin is available to download and install in QGIS from the official QGIS plugin repository 21 | [https://plugins.qgis.org/](https://plugins.qgis.org/). 22 | 23 | To install the plugin, follow the below steps. 24 | 25 | - Launch QGIS application and open plugin manager. 26 | - Search for **STAC API Browser** in the **All** page of the manager. 27 | - Click on the **STAC API Browser** result item and page with plugin information will show up. 28 | - Click the **Install Plugin** button at the bottom of the dialog to install the plugin. 29 | 30 | For more information about other ways to install the plugin, visit the [installation page](./installation). 31 | 32 | ## Source code 33 | The source code is open source and available in a GitHub repository found here 34 | [https://github.com/stac-utils/qgis-stac-plugin](https://github.com/stac-utils/qgis-stac-plugin). 35 | 36 | ## Help 37 | 38 | - Any issue found when using the plugin or questions about the plugin 39 | can be reported in the plugin GitHub repository issues page 40 | [https://github.com/stac-utils/qgis-stac-plugin/issues](https://github.com/stac-utils/qgis-stac-plugin/issues). 41 | - Support is also available, contact us [info@kartoza.com](mailto:info@kartoza.com). 42 | 43 | 44 | ## License 45 | 46 | The plugin is published under the terms of the 47 | [GNU General Public License version 3](https://www.gnu.org/licenses/gpl-3.0.en.html). 48 | 49 | 50 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | 2 | # Installation 3 | The following sections explains how plugin users can install the plugin into the QGIS application, the 4 | installation guide for plugin development purposes is covered here [development page](./development). 5 | 6 | ## From QGIS plugin repository 7 | The plugin is available in the QGIS official plugin repository. 8 | 9 | To install the plugin, follow the below steps. 10 | 11 | - Launch QGIS application and open plugin manager. 12 | - Search for **STAC API Browser** in the **All** page of the manager. 13 | - Click on the **STAC API Browser** result item and page with plugin information will show up. 14 | - Click the **Install Plugin** button at the bottom of the dialog to install the plugin. 15 | 16 | ![image](images/install-from-repository.png) 17 | _Plugin entry in the QGIS plugin manager_ 18 | 19 | ## From a ZIP file 20 | 21 | Get the plugin ZIP file from [https://github.com/stac-utils/qgis-stac-plugin/releases](https://github.com/stac-utils/qgis-stac-plugin/releases), select the required 22 | release ZIP file and download it. 23 | 24 | From the **Install from ZIP** page in the QGIS plugin manager, select the downloaded ZIP file and click the 25 | **Install Plugin** button to install it. 26 | 27 | ![image](images/install-from-zip.png) 28 | _Install from ZIP file page_ 29 | 30 | 31 | ## Using custom plugin repository 32 | 33 | The plugin is also available via a custom plugin repository that can be used to install 34 | the **STAC API Browser** plugin versions that might not be on the official QGIS plugin repository. 35 | 36 | Users can add the custom plugin repository inside the QGIS plugin manager, and use it to download and 37 | install the latest plugin versions. 38 | 39 | The plugin versions available through the custom repository will be 40 | flagged experimental. This is because the custom repository might contain plugin versions that have not been approved yet 41 | for official use. 42 | 43 | When updating the plugin manager users should enable installation of experimental 44 | plugin in the **Settings** page of the plugin manager, in order to make sure the plugin manager 45 | fetches the experimental plugins from the custom repository. 46 | 47 | To add the custom repository and install the plugin from it. 48 | 49 | - Select the **Settings** page from the QGIS plugin manager. 50 | - Click Add button on the **Plugin Repositories** group box and 51 | use the plugin custom repository found here 52 | [https://stac-utils.github.io/qgis-stac-plugin/repository/plugins.xml](https://stac-utils.github.io/qgis-stac-plugin/repository/plugins.xml) 53 | to create a new plugin repository. 54 | 55 | ![image](images/add-repository.png) 56 | _Adding another QGIS plugin repository_ 57 | 58 | 59 | -------------------------------------------------------------------------------- /docs/plugin/changelog.txt: -------------------------------------------------------------------------------- 1 | Version 1.1.2 2024-04-18 2 | - Fixed issue in the item assets dialog title when the item doesn't have assets. 3 | - Added support for API header authentication. 4 | Version 1.1.1 2022-08-23 5 | - Fixed bug on progress bar percentage value setting. 6 | - Remove all plugin action entries once the plugin has been uninstalled. 7 | - Fix for testing connection error when adding a connection for the first time. 8 | 9 | Version 1.1.0 2022-07-18 10 | - Fix for footprints layer loading workflow. 11 | - Data driven filtering using STAC Queryables. 12 | - Multiple assets and footprints loading and downloading. 13 | - Minimizeable plugin main window. 14 | - Subscription key usage for SAS based connections. 15 | - Support for COPC layers. 16 | - Support for netCDF layers. 17 | - New collection dialog. 18 | - Auto assets loading after downloading assets. 19 | - Fixed connection dialog window title when in edit mode. 20 | - Fallback to overview when item thumbnail asset is not available. 21 | - Display selected collections. 22 | - Upgraded pystac-client library to 0.3.2. 23 | - Support for CQL2-JSON filter language. 24 | - Moved sort and order buttons to search tab. 25 | 26 | Version 1.0.0 2022-01-19 27 | - Fix for plugin UI lagging bug. 28 | - Updates to loading and downloading assets workflow. 29 | - Support for adding vector based assets eg. GeoJSON, GeoPackage 30 | - Fix connection page size now default is 10 items. 31 | - Include extension in the downloaded files. 32 | - Update UI with more descriptive tooltips. 33 | 34 | Version 1.0.0-pre 2022-01-11 35 | - Changed loading and downloading assets workflow. 36 | - Implemented testing connection functionality. 37 | - Reworked filter and sort features on the search item results. 38 | - Fetch for STAC API conformance classes. 39 | - Added STAC API signing using SAS token. 40 | - Support for downloading assets and loading item footprints in QGIS. 41 | - Enabled adding STAC item assets as map layers in QGIS. 42 | - Added plugin documentation in GitHub pages. 43 | 44 | Version 1.0.0-beta 2021-12-11 45 | - Fixed slow item search. 46 | - Updated plugin search result to include pagination. 47 | - Support for search result filtering and sorting. 48 | - Implemented search. 49 | - Added default configured STAC API catalogs. 50 | - Basic STAC API support. -------------------------------------------------------------------------------- /docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | [data-md-color-scheme="stac"] { 2 | --md-primary-fg-color: #158CBA; 3 | --md-primary-fg-color--light: #ECB7B7; 4 | --md-primary-fg-color--dark: #90030C; 5 | } 6 | 7 | body, input { 8 | font-family: Ubuntu; 9 | } -------------------------------------------------------------------------------- /docs/tutorial.md: -------------------------------------------------------------------------------- 1 | 2 | # Tutorials 3 | This page contains a tutorial that provides a step by step guide on how to use the plugin to get imagery that 4 | can be used in doing NDVI analysis in QGIS. 5 | 6 | ## Calculating NDVI using Sentinel 2 Imagery 7 | 8 | ### What is NDVI (Normalized Difference Vegetation Index) 9 | 10 | NDVI is an indicator used in assessing whether the target(e.g. space imagery) being observed contains live green vegetation. 11 | 12 | NDVI is built from the red(R) and near-infrared (NIR) bands. The normalized vegetation index highlights the 13 | difference between the red band and the near-infrared band. 14 | 15 | **NDVI = (NIR - R) / (NIR + R)** 16 | 17 | This index is susceptible to the vegetation intensity and quantity. 18 | 19 | NDVI values range from -1 to +1, the negative values correspond to surfaces other than plant covers, such as snow, water, 20 | or clouds for which the red reflectance is higher than the near-infrared reflectance. 21 | For bare soil, the reflectances are approximately the same in the red and near-infrared bands, the NDVI presents values close to 0. 22 | 23 | The vegetation formations have positive NDVI values, generally between 0.1 and 0.7. The highest values correspond to the densest cover. 24 | 25 | NDVI is used in agriculture to assess the strength and quantity of vegetation by analyzing remote sensing measurements. 26 | NDVI is often used in precision agriculture decision-making tools. 27 | 28 | ### NDVI calculator on data provided using the plugin 29 | The plugin through STAC API catalogs can provide imagery that can be downloaded and used to calculate NDVI in the QGIS 30 | desktop application. 31 | 32 | Follow the below steps to calculate NDVI on imagery acquiring the data using the plugin. 33 | 34 | - Load the **STAC API Browser** plugin, then select a STAC API provider that offers imagery that contain assets with 35 | infrared and red bands. 36 | - Search for the required items in the selected STAC API catalog. 37 | ![image](images/search_result_stac_api_plugin.png) 38 | 39 | _Screenshot showing available items one of Microsoft Planetary Computer catalog collections_. 40 | 41 | - From the search results, select **View assets** on the item that contain targeted imagery and 42 | click **Add asset as layer** to load the required assets into QGIS. 43 | - After the assets have been loaded successfully into QGIS as a COG layers. 44 | 45 | Open the raster calculator that is available 46 | from **Raster** > **Raster Calculator** menu or 47 | 48 | from the **Processing Toolbox**. 49 | 50 | - Inside the calculator dialog, add the NDVI formula **NDVI = (NIR - R) / (NIR + R)** into the expression text box, 51 | where **NIR** is the layer with infrared band and **R** is the layer with the red band. 52 | 53 | After adding the formular, click **Ok** to execute the formula. If the calculation is successful 54 | the resulting layer with NDVI computation will be loaded into QGIS. 55 | 56 | 57 | ![image](images/ndvi.png) 58 | 59 | _Example of a styled NDVI imagery_. 60 | 61 | See the [user guide](./user-guide) for more information about how to add imagery using the plugin. 62 | 63 | 64 | -------------------------------------------------------------------------------- /docs/user-guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | --- 5 | 6 | # User Guide 7 | The plugin supports searching for STAC resources, loading and downloading STAC items, 8 | retrieving information about the STAC API services. 9 | 10 | STAC API services that adhere and conform to the standard STAC specification and practices are recommended 11 | to be used in order to fully utilize the usage of the available plugin features. 12 | 13 | ## Features 14 | The plugin features can be categorized in two parts, search of STAC resources and access of STAC assets. 15 | 16 | ### Search of STAC resources 17 | The STAC API specification allows search for core catalog API capabilities and search for STAC item object. 18 | The plugin support item search and provides filters that can be used along with the search. 19 | 20 | The corresponding STAC API service used when searching needs to ensure that it has implemented the `/search` 21 | API endpoint accordingly to the specification, 22 | see [https://github.com/radiantearth/stac-api-spec/tree/master/item-search](https://github.com/radiantearth/stac-api-spec/tree/master/item-search). 23 | 24 | The plugin contains the following filters that can be used when searching for STAC items objects. 25 | 26 | - **Date filter** - users can search for single instant temporal resources or resources with a temporal range. 27 | - **Spatial extent filter** - users can provide a bounding box from which the results should be filtered against 28 | - **Advanced Filter** - this enables usage of STAC API filter languages to provide advanced queries for the search 29 | for more information 30 | see [https://github.com/radiantearth/stac-api-spec/tree/master/fragments/filter](https://github.com/radiantearth/stac-api-spec/tree/master/fragments/filter). 31 | 32 | ### Accessing STAC assets 33 | Each STAC Item object contains a number of assets and a footprint which a GeoJSON geometry that defines the full 34 | footprint of the assets represented by an item. 35 | 36 | The plugin search results items contain a dedicated dialog for viewing, loading and downloading item assets into 37 | QGIS, see [adding assets section](./user-guide#adding-assets) for more details. 38 | 39 | 40 | ## How to use 41 | After installing the plugin in QGIS, the following sections provides a guide on how to use the plugin. 42 | For the installation procedure see [installation page](./installation). 43 | 44 | ### Launching the STAC API Browser plugin 45 | 46 | Three plugin menus can be used to launch the plugin in QGIS. 47 | 48 | #### QGIS toolbar 49 | 50 | In QGIS toolbar, there will be a plugin entry with the STAC APIs Browser icon. 51 | Click on the icon to open the plugin main dialog. 52 | 53 | ![image](images/toolbar.png) 54 | 55 | _QGIS toolbar_ 56 | 57 | #### QGIS Plugins Menu 58 | In the QGIS main plugins menu, Go to **STAC API Browser Plugin** > **Open STAC API Browser** 59 | 60 | ![image](images/plugin_menu.gif) 61 | _Screenshot showing how to use plugins menu to open the plugin_ 62 | 63 | 64 | #### QGIS Web Menu 65 | In the QGIS web menu which is located in the toolbar, 66 | Go to **STAC API Browser Plugin** > **Open STAC API Browser** 67 | 68 | ![image](images/web_menu.gif) 69 | _Screenshot showing how to use QGIS web menu to open the plugin_ 70 | 71 | ### Adding a STAC API connection 72 | 73 | The STAC API Browser provides by default some predefined STAC API service connections when installed for the first time. 74 | 75 | To add a new STAC API service connection, click the **New** connection button, add the required details 76 | and click ok to save the connection. 77 | 78 | ![image](images/add_stac_connectin.png) 79 | 80 | _Connection dialog with a Microsoft Planetary Computer STAC API details_ 81 | 82 | The connection dialog contains a **API Capabilities** field which can be used to set the connection to use 83 | a `SAS Token` [signing mechanism](https://planetarycomputer.microsoft.com/docs/concepts/sas/). 84 | The signing mechanism includes a token that have expiry period, Users should research the API after the period passes in order to resign the items. 85 | 86 | The **Advanced** group contain a list of the conformances type that the STAC API adhere to, when creating 87 | new connections the list is empty, users can click the **Get conformance classes** button to fetch the conformance 88 | classes. The above image shows the [Planetary Computer STAC API](https://planetarycomputer.microsoft.com/api/stac/v1) 89 | with a list of conformances classes that have already been fetched. 90 | 91 | ### STAC API Items search 92 | 93 | #### Using the search filters 94 | All the search filters can be used only when their corresponding group boxes have been checked. 95 | 96 | For the **Advanced filter** group, the available filter languages are based on the supported STAC API filter 97 | languages, when **STAC_QUERY** is used then filter input will be treated as a [STAC QUERY](https://github.com/radiantearth/stac-api-spec/tree/master/fragments/query). 98 | If **CQL_JSON** is selected then filter will used as a [CQL_FILTER](https://github.com/radiantearth/stac-api-spec/tree/master/fragments/filter). 99 | 100 | 101 | ![image](images/available_filters_planetary.png) 102 | 103 | _Available filters_ 104 | 105 | 106 | 107 | ![image](images/search_results_planetary.png) 108 | 109 | _Example search result items_ 110 | 111 | ### Items footprint and assets 112 | 113 | The plugin enables to loading STAC Item assets and footprints in QGIS as map layers. 114 | After searching is complete and items footprint and assets can be viewed and added inside QGIS. 115 | 116 | 117 | #### Adding and downloading item assets 118 | 119 | The plugin currently support loading assets as [COGs](https://github.com/cogeotiff/cog-spec/blob/master/spec.md) layers in QGIS. 120 | To add the assets into QGIS canvas, click the **View assets** button from the required result item. 121 | 122 | 123 | ![image](images/view_assets_planetary.png) 124 | 125 | _Image showing the button used for viewing the STAC item assets_ 126 | 127 | Assets dialog will be opened, from the assets list click **Add assets as layers** button to add the item into QGIS 128 | as a COG layer, to download the asset click the **Download asset** button. 129 | 130 | ![image](images/assets_dialog.png) 131 | 132 | _Assets dialog_ 133 | 134 | 135 | 136 | ![image](images/added_asset_planetary.png) 137 | _One of the added asset as a QGIS map layer_ 138 | 139 | #### Adding item footprint 140 | 141 | Click the **Add footprint** button to add the footprint of an item into QGIS canvas. 142 | The footprint map layer will be loaded into QGIS, if the STAC item had properties then will be added in the 143 | resulting map layer attribute table as layer data. 144 | 145 | ![image](images/add_footprint_planetary.png) 146 | 147 | _Image showing the button used for adding the STAC item footprint_ 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /i18n/af.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @default 5 | 6 | 7 | Good morning 8 | Goeie more 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: QGIS STAC API Browser 2 | 3 | repo_url: https://github.com/stac-utils/qgis-stac-plugin 4 | repo_name: GitHub 5 | 6 | site_description: QGIS STAC API Browser 7 | 8 | site_author: Kartoza 9 | 10 | nav: 11 | - Home: 'index.md' 12 | - Installation: "installation.md" 13 | - User Guide: "user-guide.md" 14 | - Development: "development.md" 15 | - About: "about.md" 16 | - Changelog: "changelog.md" 17 | - Tutorials: "tutorial.md" 18 | 19 | plugins: 20 | - search 21 | - git-revision-date-localized 22 | - mkdocs-video 23 | 24 | markdown_extensions: 25 | - pymdownx.snippets: 26 | base_path: . 27 | - meta: 28 | 29 | extra_css: 30 | - stylesheets/extra.css 31 | 32 | theme: 33 | name: material 34 | custom_dir: docs 35 | palette: 36 | - scheme: stac 37 | toggle: 38 | icon: material/toggle-switch-off-outline 39 | name: Switch to dark mode 40 | - scheme: slate 41 | toggle: 42 | icon: material/toggle-switch 43 | name: Switch to light mode 44 | accent: cyan 45 | logo: assets/logo.png 46 | favicon: assets/favicon.png 47 | features: 48 | - navigation.instant 49 | - navigation.tabs 50 | - navigation.tabs.sticky 51 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "qgis-stac-plugin" 3 | version = "1.1.2" 4 | description = "QGIS plugin for reading STAC API catalogs" 5 | authors = ["Kartoza "] 6 | license = "GPL v3.0" 7 | 8 | [tool.poetry.dependencies] 9 | mkdocs = "^1.2.3" 10 | mkdocs-material = "^8.1.4" 11 | mkdocs-git-revision-date-localized-plugin = "^0.11.1" 12 | mkdocs-video = "^1.1.0" 13 | python = "^3.8" 14 | httpx = "^0.20.0" 15 | toml = "^0.10.2" 16 | typer = "^0.4.0" 17 | 18 | [tool.poetry.dev-dependencies] 19 | PyQt5 = "^5.15.6" 20 | flask = "^2.0.2" 21 | 22 | [tool.qgis-plugin.metadata] 23 | name = "STAC API Browser " 24 | qgisMinimumVersion = "3.0" 25 | qgisMaximumVersion = "3.99" 26 | icon = "icon.png" 27 | experimental = "False" 28 | deprecated = "False" 29 | homepage = "https://stac-utils.github.io/qgis-stac-plugin" 30 | tracker = "https://github.com/stac-utils/qgis-stac-plugin/issues" 31 | repository = "https://github.com/stac-utils/qgis-stac-plugin" 32 | tags = [ 33 | "stac", 34 | "web", 35 | "raster", 36 | "cog", 37 | ] 38 | category = "plugins, web" 39 | hasProcessingProvider = "no" 40 | about = """\ 41 | Adds functionality to search, load and manage STAC API resources inside QGIS. 42 | Sponsored by Microsoft. 43 | """ 44 | # changelog: dynamically pulled from the docs/plugin/changelog.txt file 45 | # description: dynamically pulled from the tool.poetry.description section 46 | # version: dynamically pulled from the tool.poetry.version section 47 | # author: dynamically pulled from the tool.poetry.authors section 48 | # email: dynamically pulled from the tool.poetry.authors section 49 | 50 | [build-system] 51 | requires = ["poetry-core>=1.0.0"] 52 | build-backend = "poetry.core.masonry.api" 53 | -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/resources/icon.png -------------------------------------------------------------------------------- /resources/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon.png 4 | 5 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | QGIS_IMAGE=qgis/qgis 4 | 5 | QGIS_IMAGE_V_3_16=release-3_16 6 | QGIS_IMAGE_V_3_20=release-3_20 7 | 8 | QGIS_VERSION_TAGS=($QGIS_IMAGE_V_3_16 $QGIS_IMAGE_V_3_20) 9 | 10 | export IMAGE=$QGIS_IMAGE 11 | 12 | for TAG in "${QGIS_VERSION_TAGS[@]}" 13 | do 14 | echo "Running tests for QGIS $TAG" 15 | export QGIS_VERSION_TAG=$TAG 16 | export WITH_PYTHON_PEP=false 17 | export ON_TRAVIS=false 18 | export MUTE_LOGS=true 19 | 20 | docker-compose up -d 21 | 22 | sleep 10 23 | docker-compose exec -T qgis-testing-environment sh -c "pip3 install flask" 24 | 25 | docker-compose exec -T qgis-testing-environment qgis_testrunner.sh test_suite.test_package 26 | docker-compose down 27 | 28 | done 29 | -------------------------------------------------------------------------------- /scripts/compile-strings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | LRELEASE=$1 3 | LOCALES=$2 4 | 5 | 6 | for LOCALE in ${LOCALES} 7 | do 8 | echo "Processing: ${LOCALE}.ts" 9 | # Note we don't use pylupdate with qt .pro file approach as it is flakey 10 | # about what is made available. 11 | $LRELEASE i18n/${LOCALE}.ts 12 | done 13 | -------------------------------------------------------------------------------- /scripts/docker/qgis-stac-test-pre-scripts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | qgis_setup.sh 4 | 5 | # FIX default installation because the sources must be in "stream_feature_extractor" parent folder 6 | rm -rf /root/.local/share/QGIS/QGIS3/profiles/default/python/plugins/qgis_stac 7 | ln -sf /tests_directory /root/.local/share/QGIS/QGIS3/profiles/default/python/plugins/qgis_stac 8 | ln -sf /tests_directory /usr/share/qgis/python/plugins/qgis_stac 9 | -------------------------------------------------------------------------------- /scripts/docker/qgis-testing-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Docker entrypoint file intended for docker-compose recipe for running unittests 4 | 5 | set -e 6 | 7 | source /tests_directory/scripts/docker/qgis-stac-test-pre-scripts.sh 8 | 9 | # Run supervisor 10 | # This is the default command of qgis/qgis but we will run it in background 11 | supervisord -c /etc/supervisor/supervisord.conf & 12 | 13 | # Wait for XVFB 14 | sleep 10 15 | 16 | exec "$@" -------------------------------------------------------------------------------- /scripts/update-strings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | LOCALES=$* 3 | 4 | # Get newest .py files so we don't update strings unnecessarily 5 | 6 | CHANGED_FILES=0 7 | PYTHON_FILES=`find . -regex ".*\(ui\|py\)$" -type f` 8 | for PYTHON_FILE in $PYTHON_FILES 9 | do 10 | CHANGED=$(stat -c %Y $PYTHON_FILE) 11 | if [ ${CHANGED} -gt ${CHANGED_FILES} ] 12 | then 13 | CHANGED_FILES=${CHANGED} 14 | fi 15 | done 16 | 17 | # Qt translation stuff 18 | # for .ts file 19 | UPDATE=false 20 | for LOCALE in ${LOCALES} 21 | do 22 | TRANSLATION_FILE="i18n/$LOCALE.ts" 23 | if [ ! -f ${TRANSLATION_FILE} ] 24 | then 25 | # Force translation string collection as we have a new language file 26 | touch ${TRANSLATION_FILE} 27 | UPDATE=true 28 | break 29 | fi 30 | 31 | MODIFICATION_TIME=$(stat -c %Y ${TRANSLATION_FILE}) 32 | if [ ${CHANGED_FILES} -gt ${MODIFICATION_TIME} ] 33 | then 34 | # Force translation string collection as a .py file has been updated 35 | UPDATE=true 36 | break 37 | fi 38 | done 39 | 40 | if [ ${UPDATE} == true ] 41 | # retrieve all python files 42 | then 43 | echo ${PYTHON_FILES} 44 | # update .ts 45 | echo "Please provide translations by editing the translation files below:" 46 | for LOCALE in ${LOCALES} 47 | do 48 | echo "i18n/"${LOCALE}".ts" 49 | # Note we don't use pylupdate with qt .pro file approach as it is flakey 50 | # about what is made available. 51 | pylupdate4 -noobsolete ${PYTHON_FILES} -ts i18n/${LOCALE}.ts 52 | done 53 | else 54 | echo "No need to edit any translation files (.ts) because no python files" 55 | echo "has been updated since the last update translation. " 56 | fi 57 | -------------------------------------------------------------------------------- /src/qgis_stac/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | QgisStac 5 | 6 | A QGIS plugin that provides support for accessing STAC APIs inside QGIS 7 | application. 8 | Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ 9 | ------------------- 10 | begin : 2021-11-15 11 | copyright : (C) 2021 by Kartoza 12 | email : info@kartoza.com 13 | git sha : $Format:%H$ 14 | ***************************************************************************/ 15 | /*************************************************************************** 16 | * * 17 | * This program is free software; you can redistribute it and/or modify * 18 | * it under the terms of the GNU General Public License as published by * 19 | * the Free Software Foundation; either version 2 of the License, or * 20 | * (at your option) any later version. * 21 | * * 22 | ***************************************************************************/ 23 | This script initializes the plugin, making it known to QGIS. 24 | """ 25 | import os 26 | import sys 27 | 28 | LIB_DIR = os.path.abspath( 29 | os.path.join(os.path.dirname(__file__), 'lib')) 30 | if LIB_DIR not in sys.path: 31 | sys.path.append(LIB_DIR) 32 | 33 | 34 | # noinspection PyPep8Naming 35 | def classFactory(iface): # pylint: disable=invalid-name 36 | """Load QgisStac class from file QgisStac. 37 | :param iface: A QGIS interface instance. 38 | :type iface: QgsInterface 39 | """ 40 | # 41 | from .main import QgisStac 42 | 43 | return QgisStac(iface) 44 | -------------------------------------------------------------------------------- /src/qgis_stac/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/src/qgis_stac/api/__init__.py -------------------------------------------------------------------------------- /src/qgis_stac/api/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ QGIS STAC API plugin API client 4 | 5 | Definition of plugin base client subclass that deals 6 | with post network response handling. 7 | """ 8 | 9 | from .base import BaseClient 10 | from .models import ( 11 | ResourcePagination, 12 | ) 13 | 14 | 15 | class Client(BaseClient): 16 | """ API client class that provides implementation of the 17 | STAC API querying operations. 18 | """ 19 | 20 | def handle_items( 21 | self, 22 | items_response, 23 | pagination 24 | ): 25 | """Emits the search results items, so plugin signal observers 26 | eg. gui can use the data. 27 | 28 | :param items_response: Search results items 29 | :type items_response: List[models.Items] 30 | 31 | :param pagination: Item results pagination details 32 | :type pagination: ResourcePagination 33 | """ 34 | self.items_received.emit(items_response, pagination) 35 | 36 | def handle_item_collections( 37 | self, 38 | items_response, 39 | pagination 40 | ): 41 | """Emits the search results item collections generator 42 | 43 | :param items_response: Search results items 44 | :type items_response: List[models.Items] 45 | 46 | :param pagination: Item collections results pagination details 47 | :type pagination: ResourcePagination 48 | """ 49 | self.item_collections_received.emit( 50 | items_response.get_item_collections(), 51 | pagination 52 | ) 53 | 54 | def handle_collection( 55 | self, 56 | collection_response, 57 | pagination 58 | ): 59 | """Emits the search response collection. 60 | 61 | :param collection_response: Search result collection 62 | :type collection_response: models.Collection 63 | 64 | :param pagination: Collection results pagination details 65 | :type pagination: ResourcePagination 66 | """ 67 | 68 | self.collection_received.emit(collection_response) 69 | 70 | def handle_collections( 71 | self, 72 | collections_response, 73 | pagination 74 | ): 75 | """Emits the search results collections. 76 | 77 | :param collections_response: Search results collections 78 | :type collections_response: List[models.Collection] 79 | 80 | :param pagination: Collection results pagination details 81 | :type pagination: ResourcePagination 82 | """ 83 | 84 | # TODO query filter pagination results from the 85 | # response 86 | pagination = ResourcePagination() 87 | 88 | self.collections_received.emit(collections_response, pagination) 89 | 90 | def handle_conformance( 91 | self, 92 | conformance, 93 | pagination 94 | ): 95 | """Emits the fetched conformance classes from the API. 96 | 97 | :param conformance: Conformance classes 98 | :type conformance: list 99 | 100 | :param pagination: Conformance classes pagination details. 101 | :type pagination: ResourcePagination 102 | """ 103 | self.conformance_received.emit(conformance, pagination) 104 | 105 | def handle_queryable( 106 | self, 107 | queryable 108 | ): 109 | """Emits the fetched queryable properties classes from the API. 110 | 111 | :param queryable: Queryable properties 112 | :type queryable: models.Queryable 113 | """ 114 | self.queryable_received.emit(queryable) 115 | 116 | def handle_error( 117 | self, 118 | message: str 119 | ): 120 | """Emits the found error message from the network response. 121 | 122 | :param message: Error message 123 | :type message: str 124 | """ 125 | self.error_received.emit(message) 126 | -------------------------------------------------------------------------------- /src/qgis_stac/definitions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/src/qgis_stac/definitions/__init__.py -------------------------------------------------------------------------------- /src/qgis_stac/definitions/catalog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Definitions for all pre-installed STAC API catalog connections 4 | """ 5 | 6 | from ..api.models import ApiCapability 7 | 8 | CATALOGS = [ 9 | { 10 | "id": "07e3e9dd-cbad-4cf6-8336-424b88abf8f3", 11 | "name": "Microsoft Planetary Computer STAC API", 12 | "url": "https://planetarycomputer.microsoft.com/api/stac/v1", 13 | "selected": True, 14 | "capability": ApiCapability.SUPPORT_SAS_TOKEN.value 15 | }, 16 | { 17 | "id": "d74817bf-da1f-44d7-a464-b87d4009c8a3", 18 | "name": "Earth Search", 19 | "url": "https://earth-search.aws.element84.com/v1", 20 | "selected": False, 21 | "capability": None, 22 | }, 23 | { 24 | "id": "aff201e0-58aa-483d-9e87-090c8baecd3c", 25 | "name": "Digital Earth Africa", 26 | "url": "https://explorer.digitalearth.africa/stac/", 27 | "selected": False, 28 | "capability": None, 29 | }, 30 | { 31 | "id": "98c95473-9f32-4947-83b2-acc8bbf71f36", 32 | "name": "Radiant MLHub", 33 | "url": "https://api.radiant.earth/mlhub/v1/", 34 | "selected": False, 35 | "capability": None, 36 | }, 37 | { 38 | "id": "17a79ce2-9a61-457d-926f-03d37c0606b6", 39 | "name": "NASA CMR STAC", 40 | "url": "https://cmr.earthdata.nasa.gov/stac", 41 | "selected": False, 42 | "capability": None, 43 | } 44 | ] 45 | 46 | SITE = "https://stac-utils.github.io/qgis-stac-plugin/" 47 | -------------------------------------------------------------------------------- /src/qgis_stac/definitions/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Plugin constants 4 | """ 5 | 6 | THUMBNAIL_HEIGHT_PARAM = 'height' 7 | THUMBNAIL_WIDTH_PARAM = 'width' 8 | 9 | THUMBNAIL_HEIGHT = 200 10 | THUMBNAIL_WIDTH = 200 11 | 12 | SAS_SUBSCRIPTION_VARIABLE = "PC_SDK_SUBSCRIPTION_KEY" 13 | 14 | GDAL_SUBDATASETS_KEY = "SUBDATASETS" 15 | GDAL_METADATA_NAME = "NAME" 16 | 17 | STAC_QUERYABLE_TIMESTAMP = "TIMESTAMP" 18 | -------------------------------------------------------------------------------- /src/qgis_stac/gui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/src/qgis_stac/gui/__init__.py -------------------------------------------------------------------------------- /src/qgis_stac/gui/asset_widget.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Asset item widget, used as a template for each item asset. 4 | """ 5 | 6 | import os 7 | 8 | from qgis.PyQt import ( 9 | QtCore, 10 | QtGui, 11 | QtNetwork, 12 | QtWidgets, 13 | ) 14 | from qgis.PyQt.uic import loadUiType 15 | 16 | from qgis.core import QgsNetworkAccessManager 17 | 18 | from ..api.models import AssetLayerType 19 | 20 | from ..utils import log, tr 21 | 22 | WidgetUi, _ = loadUiType( 23 | os.path.join(os.path.dirname(__file__), "../ui/asset_widget.ui") 24 | ) 25 | 26 | 27 | class AssetWidget(QtWidgets.QWidget, WidgetUi): 28 | """ Widget that provide UI for asset details, 29 | assets loading and downloading functionalities 30 | """ 31 | 32 | load_selected = QtCore.pyqtSignal() 33 | download_selected = QtCore.pyqtSignal() 34 | 35 | load_deselected = QtCore.pyqtSignal() 36 | download_deselected = QtCore.pyqtSignal() 37 | 38 | def __init__( 39 | self, 40 | asset, 41 | asset_dialog, 42 | parent=None, 43 | ): 44 | super().__init__(parent) 45 | self.setupUi(self) 46 | self.asset = asset 47 | self.asset_dialog = asset_dialog 48 | 49 | self.initialize_ui() 50 | 51 | def initialize_ui(self): 52 | """ Populate UI inputs when loading the widget""" 53 | 54 | self.title_la.setText(self.asset.title) 55 | self.type_la.setText(self.asset.type) 56 | asset_load = self.asset_loadable() 57 | 58 | self.load_box.setEnabled(asset_load) 59 | self.load_box.toggled.connect(self.asset_load_selected) 60 | self.load_box.stateChanged.connect(self.asset_load_selected) 61 | self.download_box.toggled.connect(self.asset_download_selected) 62 | self.download_box.stateChanged.connect(self.asset_download_selected) 63 | 64 | if asset_load: 65 | self.load_box.setToolTip( 66 | tr("Asset contains {} media type which " 67 | "cannot be loaded as a map layer in QGIS" 68 | ).format(self.asset.type) 69 | ) 70 | 71 | def asset_loadable(self): 72 | """ Returns if asset can be added into QGIS""" 73 | 74 | layer_types = [ 75 | AssetLayerType.COG.value, 76 | AssetLayerType.COPC.value, 77 | AssetLayerType.GEOTIFF.value, 78 | AssetLayerType.NETCDF.value, 79 | ] 80 | 81 | if self.asset.type is not None: 82 | return self.asset.type in ''.join(layer_types) 83 | else: 84 | try: 85 | request = QtNetwork.QNetworkRequest( 86 | QtCore.QUrl(self.asset.href) 87 | ) 88 | response = QgsNetworkAccessManager().\ 89 | instance().blockingGet(request) 90 | content_type = response.rawHeader( 91 | QtCore.QByteArray( 92 | 'content-type'.encode() 93 | ) 94 | ) 95 | content_type = str(content_type, 'utf-8') 96 | 97 | for layer_type in layer_types: 98 | layer_type_values = layer_type.split(' ') 99 | for value in layer_type_values: 100 | if value in content_type: 101 | return True 102 | 103 | except Exception as e: 104 | log(f"Problem fetching asset " 105 | f"type from the asset url {self.asset.href}," 106 | f" error {e}" 107 | ) 108 | 109 | return False 110 | 111 | def asset_load_selected(self, state=None): 112 | """ Emits the needed signal when an asset has been selected 113 | for loading. 114 | """ 115 | if self.load_box.isChecked(): 116 | self.load_selected.emit() 117 | else: 118 | self.load_deselected.emit() 119 | 120 | def asset_download_selected(self, state=None): 121 | """ Emits the needed signal when an asset has been selected 122 | for downloading. 123 | """ 124 | if self.download_box.isChecked(): 125 | self.download_selected.emit() 126 | else: 127 | self.download_deselected.emit() 128 | -------------------------------------------------------------------------------- /src/qgis_stac/gui/json_highlighter.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from qgis.PyQt.QtCore import QRegExp, Qt 4 | from qgis.PyQt.QtGui import QColor, QFont, QSyntaxHighlighter, QTextCharFormat 5 | 6 | 7 | class JsonHighlighter(QSyntaxHighlighter): 8 | """Json Highlighter.""" 9 | 10 | def __init__(self, parent=None): 11 | super().__init__(parent) 12 | 13 | self.highlight_rules = [] 14 | 15 | text_format = QTextCharFormat() 16 | pattern = QRegExp("([-0-9.]+)(?!([^\"]*\"[\\s]*\\:))") 17 | text_format.setForeground(Qt.darkRed) 18 | self.highlight_rules.append((pattern, text_format)) 19 | 20 | text_format = QTextCharFormat() 21 | pattern = QRegExp("(?:[ ]*\\,[ ]*)(\"[^\"]*\")") 22 | text_format.setForeground(Qt.darkGreen) 23 | self.highlight_rules.append((pattern, text_format)) 24 | 25 | text_format = QTextCharFormat() 26 | pattern = QRegExp("(\"[^\"]*\")(?:\\s*\\])") 27 | text_format.setForeground(Qt.darkGreen) 28 | self.highlight_rules.append((pattern, text_format)) 29 | 30 | text_format = QTextCharFormat() 31 | pattern = QRegExp("(\"[^\"]*\")\\s*\\:") 32 | text_format.setForeground(Qt.darkGreen) 33 | self.highlight_rules.append((pattern, text_format)) 34 | 35 | text_format = QTextCharFormat() 36 | pattern = QRegExp(":+(?:[: []*)(\"[^\"]*\")") 37 | text_format.setForeground(Qt.darkGreen) 38 | self.highlight_rules.append((pattern, text_format)) 39 | 40 | def highlightBlock(self, text): 41 | """Highlight of a comment block""" 42 | for pattern, text_format in self.highlight_rules: 43 | expression = QRegExp(pattern) 44 | index = expression.indexIn(text) 45 | 46 | while index >= 0: 47 | length = expression.matchedLength() 48 | self.setFormat(index, length, text_format) 49 | index = expression.indexIn(text, index + length) 50 | -------------------------------------------------------------------------------- /src/qgis_stac/gui/queryable_property.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Queryable property widget, used as a template for each property. 4 | """ 5 | 6 | import os 7 | 8 | from qgis.gui import QgsDateTimeEdit 9 | 10 | from qgis.PyQt import ( 11 | QtCore, 12 | QtGui, 13 | QtNetwork, 14 | QtWidgets, 15 | ) 16 | from qgis.PyQt.uic import loadUiType 17 | 18 | from ..api.models import FilterOperator, QueryablePropertyType 19 | from ..definitions.constants import STAC_QUERYABLE_TIMESTAMP 20 | 21 | 22 | from ..utils import tr 23 | 24 | WidgetUi, _ = loadUiType( 25 | os.path.join(os.path.dirname(__file__), "../ui/queryable_property.ui") 26 | ) 27 | 28 | 29 | class QueryablePropertyWidget(QtWidgets.QWidget, WidgetUi): 30 | """ Widget that provide UI for STAC queryable properties details. 31 | """ 32 | 33 | def __init__( 34 | self, 35 | queryable_property, 36 | parent=None, 37 | ): 38 | super().__init__(parent) 39 | self.setupUi(self) 40 | self.queryable_property = queryable_property 41 | 42 | self.input_widget = None 43 | self.initialize_ui() 44 | 45 | def initialize_ui(self): 46 | """ Populate UI inputs when loading the widget""" 47 | 48 | size_policy = QtWidgets.QSizePolicy( 49 | QtWidgets.QSizePolicy.Preferred, 50 | QtWidgets.QSizePolicy.Preferred, 51 | ) 52 | 53 | label = QtWidgets.QLabel(self.queryable_property.name) 54 | label.setSizePolicy(size_policy) 55 | 56 | label_layout = QtWidgets.QVBoxLayout(self.property_label) 57 | label_layout.setContentsMargins(9, 9, 9, 9) 58 | label_layout.addWidget(label) 59 | 60 | input_layout = QtWidgets.QVBoxLayout(self.property_input) 61 | input_layout.setContentsMargins(4, 4, 4, 4) 62 | 63 | if self.queryable_property.type == \ 64 | QueryablePropertyType.INTEGER.value: 65 | spin_box = QtWidgets.QSpinBox() 66 | spin_box.setRange( 67 | self.queryable_property.minimum or 1, 68 | self.queryable_property.maximum or 100 69 | ) 70 | spin_box.setSingleStep(1) 71 | spin_box.setSizePolicy(size_policy) 72 | 73 | input_layout.addWidget(spin_box) 74 | self.input_widget = spin_box 75 | elif self.queryable_property.type == \ 76 | QueryablePropertyType.ENUM.value: 77 | 78 | cmb_box = QtWidgets.QComboBox() 79 | cmb_box.setSizePolicy(size_policy) 80 | 81 | cmb_box.addItem("") 82 | for enum_value in self.queryable_property.values: 83 | cmb_box.addItem(enum_value, enum_value) 84 | cmb_box.setCurrentIndex(0) 85 | 86 | input_layout.addWidget(cmb_box) 87 | self.input_widget = cmb_box 88 | elif self.queryable_property.type == \ 89 | QueryablePropertyType.DATETIME.value: 90 | 91 | datetime_edit = QgsDateTimeEdit() 92 | datetime_edit.setSizePolicy(size_policy) 93 | 94 | input_layout.addWidget(datetime_edit) 95 | self.input_widget = datetime_edit 96 | else: 97 | line_edit = QtWidgets.QLineEdit() 98 | line_edit.setSizePolicy(size_policy) 99 | 100 | input_layout.addWidget(line_edit) 101 | self.input_widget = line_edit 102 | 103 | labels = { 104 | FilterOperator.LESS_THAN: tr("<"), 105 | FilterOperator.GREATER_THAN: tr(">"), 106 | FilterOperator.LESS_THAN_EQUAL: tr("<="), 107 | FilterOperator.GREATER_THAN_EQUAL: tr(">="), 108 | FilterOperator.EQUAL: tr("="), 109 | } 110 | self.operator_cmb.addItem("") 111 | for operator, label in labels.items(): 112 | self.operator_cmb.addItem(label, operator) 113 | self.operator_cmb.setCurrentIndex(0) 114 | 115 | def filter_text(self): 116 | """ Returns a cql-text representation of the property and the 117 | available value. 118 | 119 | :returns CQL-Text that can be used to filter STAC catalog 120 | :rtype: str 121 | """ 122 | 123 | try: 124 | current_operator = self.operator_cmb.itemData( 125 | self.operator_cmb.currentIndex() 126 | ) if self.operator_cmb.currentIndex() != 0 \ 127 | else FilterOperator.EQUAL 128 | 129 | if isinstance(self.input_widget, QtWidgets.QSpinBox) and \ 130 | self.input_widget.value() != "": 131 | 132 | text = f"{self.queryable_property.name} " \ 133 | f"{current_operator.value} " \ 134 | f"{self.input_widget.value()}" 135 | elif isinstance(self.input_widget, QtWidgets.QLineEdit) and \ 136 | self.input_widget.text() != "": 137 | text = f"{self.queryable_property.name} " \ 138 | f"{current_operator.value} " \ 139 | f"{self.input_widget.text()}" 140 | elif isinstance(self.input_widget, QtWidgets.QComboBox): 141 | input_value = self.input_widget.itemData( 142 | self.input_widget.currentIndex() 143 | ) 144 | text = f"{self.queryable_property.name} " \ 145 | f"{current_operator.value} " \ 146 | f"{input_value}" if input_value else None 147 | elif isinstance(self.input_widget, QgsDateTimeEdit) and \ 148 | not self.input_widget.isNull(): 149 | datetime_str = self.input_widget.dateTime().\ 150 | toString(QtCore.Qt.ISODate) 151 | text = f"{self.queryable_property.name} " \ 152 | f"{current_operator.value} " \ 153 | f"{STAC_QUERYABLE_TIMESTAMP}('{datetime_str}')" 154 | else: 155 | raise NotImplementedError 156 | except RuntimeError as e: 157 | text = None 158 | 159 | return text 160 | -------------------------------------------------------------------------------- /src/qgis_stac/gui/result_item_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Contains GUI models used to store search result items. 4 | """ 5 | from qgis.PyQt import ( 6 | QtCore, 7 | QtGui, 8 | QtWidgets 9 | ) 10 | 11 | 12 | class ItemsModel(QtCore.QAbstractItemModel): 13 | """ Stores the search result items""" 14 | 15 | def __init__(self, items, parent=None): 16 | super().__init__(parent) 17 | self.items = items 18 | 19 | def index( 20 | self, 21 | row: int, 22 | column: int, 23 | parent 24 | ): 25 | invalid_index = QtCore.QModelIndex() 26 | if self.hasIndex(row, column, parent): 27 | try: 28 | item = self.items[row] 29 | result = self.createIndex(row, column, item) 30 | except IndexError: 31 | result = invalid_index 32 | else: 33 | result = invalid_index 34 | return result 35 | 36 | def parent(self, index: QtCore.QModelIndex): 37 | return QtCore.QModelIndex() 38 | 39 | def rowCount(self, index: QtCore.QModelIndex = QtCore.QModelIndex()): 40 | return len(self.items) 41 | 42 | def columnCount(self, index: QtCore.QModelIndex = QtCore.QModelIndex()): 43 | return 1 44 | 45 | def data( 46 | self, 47 | index: QtCore.QModelIndex = QtCore.QModelIndex(), 48 | role: QtCore.Qt.ItemDataRole = QtCore.Qt.DisplayRole 49 | ): 50 | result = None 51 | if index.isValid(): 52 | item = index.internalPointer() 53 | result = item 54 | return result 55 | 56 | def flags( 57 | self, 58 | index: QtCore.QModelIndex = QtCore.QModelIndex() 59 | ): 60 | if index.isValid(): 61 | flags = super().flags(index) 62 | result = QtCore.Qt.ItemIsEditable | flags 63 | else: 64 | result = QtCore.Qt.NoItemFlags 65 | return result 66 | 67 | 68 | class ItemsSortFilterProxyModel(QtCore.QSortFilterProxyModel): 69 | """ Handles the custom functionality in sorting and filtering the 70 | search items results. 71 | """ 72 | 73 | def __init__(self, *args, **kwargs): 74 | super().__init__(*args, **kwargs) 75 | 76 | def filterAcceptsRow(self, source_row: int, source_parent: QtCore.QModelIndex): 77 | index = self.sourceModel().index(source_row, 0, source_parent) 78 | 79 | match = self.filterRegularExpression().match( 80 | self.sourceModel().data(index).id 81 | ) 82 | return match.hasMatch() 83 | -------------------------------------------------------------------------------- /src/qgis_stac/jobs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/src/qgis_stac/jobs/__init__.py -------------------------------------------------------------------------------- /src/qgis_stac/jobs/token_manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Handles the plugin connection Token management. 4 | """ 5 | 6 | import os 7 | import enum 8 | 9 | from qgis.PyQt import ( 10 | QtCore, 11 | ) 12 | 13 | from qgis.core import ( 14 | QgsApplication, 15 | QgsTask 16 | ) 17 | 18 | from ..conf import Settings, settings_manager 19 | from ..lib import planetary_computer as pc 20 | 21 | from ..api.models import ( 22 | ApiCapability, 23 | ResourceAsset, 24 | TimeUnits 25 | ) 26 | 27 | from ..utils import log 28 | 29 | from ..definitions.constants import SAS_SUBSCRIPTION_VARIABLE 30 | 31 | 32 | class RefreshState(enum.Enum): 33 | """ Represents time units.""" 34 | RUNNING = 'RUNNING' 35 | IDLE = 'IDLE' 36 | 37 | 38 | class SASManager(QtCore.QObject): 39 | """ Manager to help updates on the SAS token based connections. 40 | """ 41 | token_refresh_started = QtCore.pyqtSignal() 42 | token_refresh_finished = QtCore.pyqtSignal() 43 | token_refresh_error = QtCore.pyqtSignal() 44 | 45 | def __init__(self, *args, **kwargs): 46 | super().__init__(*args, **kwargs) 47 | 48 | def refresh_started(self): 49 | self.token_refresh_started.emit() 50 | 51 | def refresh_complete(self): 52 | 53 | settings_manager.set_value( 54 | Settings.REFRESH_STATE, 55 | RefreshState.IDLE 56 | ) 57 | self.token_refresh_finished.emit() 58 | 59 | def refresh_error(self): 60 | 61 | settings_manager.set_value( 62 | Settings.REFRESH_STATE, 63 | RefreshState.IDLE 64 | ) 65 | self.token_refresh_error.emit() 66 | 67 | def run_refresh_task(self): 68 | 69 | refresh_state = settings_manager.get_value( 70 | Settings.REFRESH_STATE, 71 | RefreshState.IDLE 72 | ) 73 | 74 | if refresh_state == RefreshState.RUNNING: 75 | return 76 | 77 | self.token_refresh_started.emit() 78 | 79 | task = RefreshTask() 80 | task.taskCompleted.connect(self.refresh_complete) 81 | task.taskTerminated.connect(self.refresh_error) 82 | task.run() 83 | 84 | 85 | class RefreshTask(QgsTask): 86 | """ Runs token manager refresh process on a background Task.""" 87 | def __init__( 88 | self 89 | ): 90 | 91 | super().__init__() 92 | 93 | def run(self): 94 | """ Operates the main logic of loading the token refreshin 95 | background. 96 | """ 97 | self.token_refresh() 98 | 99 | return True 100 | 101 | def token_refresh(self): 102 | """ Refreshes the current SAS token available in search results 103 | items store in the plugin settings. 104 | """ 105 | updated_items = [] 106 | 107 | last_update = settings_manager.get_value( 108 | Settings.REFRESH_LAST_UPDATE, 109 | None 110 | ) 111 | 112 | refresh_frequency = settings_manager.get_value( 113 | Settings.REFRESH_FREQUENCY, 114 | 1, 115 | setting_type=int 116 | ) 117 | 118 | unit = settings_manager.get_value( 119 | Settings.REFRESH_FREQUENCY_UNIT, 120 | TimeUnits.MINUTES 121 | ) 122 | 123 | refresh_time_count = { 124 | TimeUnits.MINUTES: 60000, 125 | TimeUnits.HOURS: 60 * 6000, 126 | TimeUnits.DAYS: 24 * 60 * 6000, 127 | } 128 | 129 | if last_update: 130 | last_update_date = QtCore.QDateTime.fromString( 131 | last_update, QtCore.Qt.ISODate 132 | ) 133 | else: 134 | last_update_date = QtCore.QDateTime.currentDateTime() 135 | settings_manager.set_value( 136 | Settings.REFRESH_LAST_UPDATE, 137 | last_update_date.toString(QtCore.Qt.ISODate) 138 | ) 139 | 140 | current_time = QtCore.QDateTime.currentDateTime() 141 | 142 | if last_update_date.msecsTo(current_time) < \ 143 | refresh_frequency * refresh_time_count[unit]: 144 | self.cancel() 145 | return 146 | 147 | settings_manager.set_value( 148 | Settings.REFRESH_LAST_UPDATE, 149 | current_time.toString(QtCore.Qt.ISODate) 150 | ) 151 | 152 | settings_manager.set_value( 153 | Settings.REFRESH_STATE, 154 | RefreshState.RUNNING 155 | ) 156 | 157 | connections = settings_manager.list_connections() 158 | 159 | key = os.getenv(SAS_SUBSCRIPTION_VARIABLE) 160 | 161 | # If the plugin defined connection sas subscription key 162 | # exists use it instead of the environment one. 163 | connection = settings_manager.get_current_connection() 164 | 165 | if connection and \ 166 | connection.capability == ApiCapability.SUPPORT_SAS_TOKEN and \ 167 | connection.sas_subscription_key: 168 | key = connection.sas_subscription_key 169 | 170 | if key: 171 | pc.set_subscription_key(key) 172 | 173 | for connection in connections: 174 | if connection.capability == \ 175 | ApiCapability.SUPPORT_SAS_TOKEN: 176 | settings_items = settings_manager.get_items( 177 | connection.id 178 | ) 179 | for page, items in settings_items.items(): 180 | for item in items: 181 | if item.stac_object: 182 | stac_object = pc.sign(item.stac_object) 183 | item.stac_object = stac_object 184 | item.assets = [ 185 | ResourceAsset( 186 | href=asset.href, 187 | title=asset.title or key, 188 | description=asset.description, 189 | type=asset.media_type, 190 | roles=asset.roles or [] 191 | ) 192 | for key, asset in stac_object.assets.items() 193 | ] 194 | updated_items.append(item) 195 | if updated_items: 196 | settings_manager.save_items( 197 | connection, 198 | updated_items, 199 | page 200 | ) 201 | updated_items = [] 202 | 203 | self.cancel() 204 | 205 | 206 | def finished(self, result: bool): 207 | """ Handle logic after task has completed. 208 | 209 | :param result: Whether the run() operation finished successfully 210 | :type result: bool 211 | """ 212 | if result: 213 | log("Successfully refreshed") 214 | settings_manager.set_value( 215 | Settings.REFRESH_STATE, 216 | RefreshState.IDLE 217 | ) 218 | else: 219 | log("Failed to refresh") 220 | 221 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/src/qgis_stac/lib/__init__.py -------------------------------------------------------------------------------- /src/qgis_stac/lib/planetary_computer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/planetary_computer/__init__.py: -------------------------------------------------------------------------------- 1 | """Planetary Computer Python SDK""" 2 | # flake8:noqa 3 | 4 | from planetary_computer.sas import ( 5 | sign, 6 | sign_url, 7 | sign_item, 8 | sign_assets, 9 | sign_asset, 10 | sign_item_collection, 11 | ) 12 | from planetary_computer.settings import set_subscription_key 13 | 14 | from planetary_computer.version import __version__ 15 | 16 | __all__ = [ 17 | "set_subscription_key", 18 | "sign_asset", 19 | "sign_assets", 20 | "sign_item_collection", 21 | "sign_item", 22 | "sign_url", 23 | "sign", 24 | "__version__", 25 | ] 26 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/planetary_computer/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/src/qgis_stac/lib/planetary_computer/scripts/__init__.py -------------------------------------------------------------------------------- /src/qgis_stac/lib/planetary_computer/scripts/cli.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import click 3 | 4 | 5 | @click.group(help="Microsoft Planetary Computer CLI") 6 | def app() -> None: 7 | """Click group for planetarycomputer subcommands""" 8 | pass 9 | 10 | 11 | @app.command() 12 | @click.option( 13 | "--subscription_key", 14 | prompt="Please enter your API subscription key", 15 | help="Your API subscription key", 16 | ) 17 | def configure(subscription_key: str) -> None: 18 | """Configure the planetarycomputer library""" 19 | settings_dir = Path("~/.planetarycomputer").expanduser() 20 | settings_dir.mkdir(exist_ok=True) 21 | with (settings_dir / "settings.env").open(mode="w") as settings_file: 22 | settings_file.write(f"PC_SDK_SUBSCRIPTION_KEY={subscription_key}\n") 23 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/planetary_computer/settings.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | from typing import Optional 3 | import pydantic 4 | 5 | SETTINGS_ENV_FILE = "~/.planetarycomputer/settings.env" 6 | SETTINGS_ENV_PREFIX = "PC_SDK_" 7 | 8 | DEFAULT_SAS_TOKEN_ENDPOINT = "https://planetarycomputer.microsoft.com/api/sas/v1/token" 9 | 10 | 11 | class Settings(pydantic.BaseSettings): 12 | """PC SDK configuration settings 13 | 14 | Settings defined here are attempted to be read in two ways, in this order: 15 | * environment variables 16 | * environment file: ~/.planetarycomputer/settings.env 17 | 18 | That is, any settings defined via environment variables will take precedence 19 | over settings defined in the environment file, so can be used to override. 20 | 21 | All settings are prefixed with `PC_SDK_` 22 | """ 23 | 24 | # PC_SDK_SUBSCRIPTION_KEY: subscription key to send along with token 25 | # requests. If present, allows less restricted rate limiting. 26 | subscription_key: Optional[str] = None 27 | 28 | # PC_SDK_SAS_URL: The planetary computer SAS endpoint URL. 29 | # This will default to the main planetary computer endpoint. 30 | sas_url: str = DEFAULT_SAS_TOKEN_ENDPOINT 31 | 32 | class Config: 33 | env_file = SETTINGS_ENV_FILE 34 | env_prefix = SETTINGS_ENV_PREFIX 35 | 36 | @staticmethod 37 | @lru_cache(maxsize=1) 38 | def get() -> "Settings": 39 | return Settings() 40 | 41 | 42 | def set_subscription_key(key: str) -> None: 43 | """Sets the Planetary Computer API subscription key to use 44 | within the process that loaded this module. Ths does not write 45 | to the settings file. 46 | 47 | Args: 48 | key: The Planetary Computer API subscription key to use 49 | for methods inside this library that can utilize the key, 50 | such as SAS token generation. 51 | """ 52 | Settings.get().subscription_key = key 53 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/planetary_computer/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from typing import Tuple, Optional 4 | from urllib.parse import ParseResult, urlunparse, urlparse 5 | 6 | import pystac 7 | 8 | 9 | def parse_blob_url(parsed_url: ParseResult) -> Tuple[str, str]: 10 | """Find the account and container in a blob URL 11 | 12 | Parameters 13 | ---------- 14 | url: str 15 | URL to extract information from 16 | 17 | Returns 18 | ------- 19 | Tuple of the account name and container name 20 | """ 21 | try: 22 | account_name = parsed_url.netloc.split(".")[0] 23 | path_blob = parsed_url.path.lstrip("/").split("/", 1) 24 | container_name = path_blob[-2] 25 | except Exception as failed_parse: 26 | raise ValueError( 27 | f"Invalid blob URL: {urlunparse(parsed_url)}" 28 | ) from failed_parse 29 | 30 | return account_name, container_name 31 | 32 | 33 | def parse_adlfs_url(url: str) -> Optional[str]: 34 | """ 35 | Extract the storage container from an adlfs URL. 36 | 37 | Parameters 38 | ---------- 39 | url : str 40 | The URL to extract the container from, if present 41 | 42 | Returns 43 | ------- 44 | str or None 45 | Returns the container name, if present. Otherwise None is returned. 46 | """ 47 | if url.startswith(("abfs://", "az://")): 48 | return urlparse(url).netloc 49 | return None 50 | 51 | 52 | def is_fsspec_asset(asset: pystac.Asset) -> bool: 53 | """ 54 | Determine if an Asset points to an fsspec URL. 55 | 56 | This checks if "account_name" is present in the asset's "table:storage_options" 57 | or "xarray:storage_options" fields. 58 | """ 59 | return "account_name" in asset.extra_fields.get( 60 | "table:storage_options", {} 61 | ) or "account_name" in asset.extra_fields.get("xarray:storage_options", {}) 62 | 63 | 64 | def is_vrt_string(s: str) -> bool: 65 | """ 66 | Check whether a string looks like a VRT 67 | """ 68 | return s.strip().startswith("") 69 | 70 | 71 | asset_xpr = re.compile( 72 | r"https://(?P[A-z0-9]+?)" 73 | r"\.blob\.core\.windows\.net/" 74 | r"(?P.+?)" 75 | r"/(?P[^<]+)" 76 | ) 77 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/planetary_computer/version.py: -------------------------------------------------------------------------------- 1 | """Library version""" 2 | 3 | __version__ = "0.4.4" 4 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pydantic/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017, 2018, 2019, 2020, 2021 Samuel Colvin and other contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pydantic/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from . import dataclasses 3 | from .annotated_types import create_model_from_namedtuple, create_model_from_typeddict 4 | from .class_validators import root_validator, validator 5 | from .decorator import validate_arguments 6 | from .env_settings import BaseSettings 7 | from .error_wrappers import ValidationError 8 | from .errors import * 9 | from .fields import Field, PrivateAttr, Required 10 | from .main import * 11 | from .networks import * 12 | from .parse import Protocol 13 | from .tools import * 14 | from .types import * 15 | from .version import VERSION 16 | 17 | # WARNING __all__ from .errors is not included here, it will be removed as an export here in v2 18 | # please use "from pydantic.errors import ..." instead 19 | __all__ = [ 20 | # annotated types utils 21 | 'create_model_from_namedtuple', 22 | 'create_model_from_typeddict', 23 | # dataclasses 24 | 'dataclasses', 25 | # class_validators 26 | 'root_validator', 27 | 'validator', 28 | # decorator 29 | 'validate_arguments', 30 | # env_settings 31 | 'BaseSettings', 32 | # error_wrappers 33 | 'ValidationError', 34 | # fields 35 | 'Field', 36 | 'Required', 37 | # main 38 | 'BaseConfig', 39 | 'BaseModel', 40 | 'Extra', 41 | 'compiled', 42 | 'create_model', 43 | 'validate_model', 44 | # network 45 | 'AnyUrl', 46 | 'AnyHttpUrl', 47 | 'HttpUrl', 48 | 'stricturl', 49 | 'EmailStr', 50 | 'NameEmail', 51 | 'IPvAnyAddress', 52 | 'IPvAnyInterface', 53 | 'IPvAnyNetwork', 54 | 'PostgresDsn', 55 | 'RedisDsn', 56 | 'validate_email', 57 | # parse 58 | 'Protocol', 59 | # tools 60 | 'parse_file_as', 61 | 'parse_obj_as', 62 | 'parse_raw_as', 63 | # types 64 | 'NoneStr', 65 | 'NoneBytes', 66 | 'StrBytes', 67 | 'NoneStrBytes', 68 | 'StrictStr', 69 | 'ConstrainedBytes', 70 | 'conbytes', 71 | 'ConstrainedList', 72 | 'conlist', 73 | 'ConstrainedSet', 74 | 'conset', 75 | 'ConstrainedStr', 76 | 'constr', 77 | 'PyObject', 78 | 'ConstrainedInt', 79 | 'conint', 80 | 'PositiveInt', 81 | 'NegativeInt', 82 | 'NonNegativeInt', 83 | 'NonPositiveInt', 84 | 'ConstrainedFloat', 85 | 'confloat', 86 | 'PositiveFloat', 87 | 'NegativeFloat', 88 | 'NonNegativeFloat', 89 | 'NonPositiveFloat', 90 | 'ConstrainedDecimal', 91 | 'condecimal', 92 | 'UUID1', 93 | 'UUID3', 94 | 'UUID4', 95 | 'UUID5', 96 | 'FilePath', 97 | 'DirectoryPath', 98 | 'Json', 99 | 'JsonWrapper', 100 | 'SecretStr', 101 | 'SecretBytes', 102 | 'StrictBool', 103 | 'StrictBytes', 104 | 'StrictInt', 105 | 'StrictFloat', 106 | 'PaymentCardNumber', 107 | 'PrivateAttr', 108 | 'ByteSize', 109 | # version 110 | 'VERSION', 111 | ] 112 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pydantic/annotated_types.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Any, Dict, FrozenSet, NamedTuple, Type 2 | 3 | from .fields import Required 4 | from .main import BaseModel, create_model 5 | 6 | if TYPE_CHECKING: 7 | 8 | class TypedDict(Dict[str, Any]): 9 | __annotations__: Dict[str, Type[Any]] 10 | __total__: bool 11 | __required_keys__: FrozenSet[str] 12 | __optional_keys__: FrozenSet[str] 13 | 14 | 15 | def create_model_from_typeddict(typeddict_cls: Type['TypedDict'], **kwargs: Any) -> Type['BaseModel']: 16 | """ 17 | Create a `BaseModel` based on the fields of a `TypedDict`. 18 | Since `typing.TypedDict` in Python 3.8 does not store runtime information about optional keys, 19 | we raise an error if this happens (see https://bugs.python.org/issue38834). 20 | """ 21 | field_definitions: Dict[str, Any] 22 | 23 | # Best case scenario: with python 3.9+ or when `TypedDict` is imported from `typing_extensions` 24 | if not hasattr(typeddict_cls, '__required_keys__'): 25 | raise TypeError( 26 | 'You should use `typing_extensions.TypedDict` instead of `typing.TypedDict`. ' 27 | 'Without it, there is no way to differentiate required and optional fields when subclassed.' 28 | ) 29 | 30 | field_definitions = { 31 | field_name: (field_type, Required if field_name in typeddict_cls.__required_keys__ else None) 32 | for field_name, field_type in typeddict_cls.__annotations__.items() 33 | } 34 | 35 | return create_model(typeddict_cls.__name__, **kwargs, **field_definitions) 36 | 37 | 38 | def create_model_from_namedtuple(namedtuple_cls: Type['NamedTuple'], **kwargs: Any) -> Type['BaseModel']: 39 | """ 40 | Create a `BaseModel` based on the fields of a named tuple. 41 | A named tuple can be created with `typing.NamedTuple` and declared annotations 42 | but also with `collections.namedtuple`, in this case we consider all fields 43 | to have type `Any`. 44 | """ 45 | namedtuple_annotations: Dict[str, Type[Any]] = getattr( 46 | namedtuple_cls, '__annotations__', {k: Any for k in namedtuple_cls._fields} 47 | ) 48 | field_definitions: Dict[str, Any] = { 49 | field_name: (field_type, Required) for field_name, field_type in namedtuple_annotations.items() 50 | } 51 | return create_model(namedtuple_cls.__name__, **kwargs, **field_definitions) 52 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pydantic/error_wrappers.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Sequence, Tuple, Type, Union 3 | 4 | from .json import pydantic_encoder 5 | from .utils import Representation 6 | 7 | if TYPE_CHECKING: 8 | from .main import BaseConfig # noqa: F401 9 | from .types import ModelOrDc # noqa: F401 10 | from .typing import ReprArgs 11 | 12 | Loc = Tuple[Union[int, str], ...] 13 | 14 | __all__ = 'ErrorWrapper', 'ValidationError' 15 | 16 | 17 | class ErrorWrapper(Representation): 18 | __slots__ = 'exc', '_loc' 19 | 20 | def __init__(self, exc: Exception, loc: Union[str, 'Loc']) -> None: 21 | self.exc = exc 22 | self._loc = loc 23 | 24 | def loc_tuple(self) -> 'Loc': 25 | if isinstance(self._loc, tuple): 26 | return self._loc 27 | else: 28 | return (self._loc,) 29 | 30 | def __repr_args__(self) -> 'ReprArgs': 31 | return [('exc', self.exc), ('loc', self.loc_tuple())] 32 | 33 | 34 | # ErrorList is something like Union[List[Union[List[ErrorWrapper], ErrorWrapper]], ErrorWrapper] 35 | # but recursive, therefore just use: 36 | ErrorList = Union[Sequence[Any], ErrorWrapper] 37 | 38 | 39 | class ValidationError(Representation, ValueError): 40 | __slots__ = 'raw_errors', 'model', '_error_cache' 41 | 42 | def __init__(self, errors: Sequence[ErrorList], model: 'ModelOrDc') -> None: 43 | self.raw_errors = errors 44 | self.model = model 45 | self._error_cache: Optional[List[Dict[str, Any]]] = None 46 | 47 | def errors(self) -> List[Dict[str, Any]]: 48 | if self._error_cache is None: 49 | try: 50 | config = self.model.__config__ # type: ignore 51 | except AttributeError: 52 | config = self.model.__pydantic_model__.__config__ # type: ignore 53 | self._error_cache = list(flatten_errors(self.raw_errors, config)) 54 | return self._error_cache 55 | 56 | def json(self, *, indent: Union[None, int, str] = 2) -> str: 57 | return json.dumps(self.errors(), indent=indent, default=pydantic_encoder) 58 | 59 | def __str__(self) -> str: 60 | errors = self.errors() 61 | no_errors = len(errors) 62 | return ( 63 | f'{no_errors} validation error{"" if no_errors == 1 else "s"} for {self.model.__name__}\n' 64 | f'{display_errors(errors)}' 65 | ) 66 | 67 | def __repr_args__(self) -> 'ReprArgs': 68 | return [('model', self.model.__name__), ('errors', self.errors())] 69 | 70 | 71 | def display_errors(errors: List[Dict[str, Any]]) -> str: 72 | return '\n'.join(f'{_display_error_loc(e)}\n {e["msg"]} ({_display_error_type_and_ctx(e)})' for e in errors) 73 | 74 | 75 | def _display_error_loc(error: Dict[str, Any]) -> str: 76 | return ' -> '.join(str(e) for e in error['loc']) 77 | 78 | 79 | def _display_error_type_and_ctx(error: Dict[str, Any]) -> str: 80 | t = 'type=' + error['type'] 81 | ctx = error.get('ctx') 82 | if ctx: 83 | return t + ''.join(f'; {k}={v}' for k, v in ctx.items()) 84 | else: 85 | return t 86 | 87 | 88 | def flatten_errors( 89 | errors: Sequence[Any], config: Type['BaseConfig'], loc: Optional['Loc'] = None 90 | ) -> Generator[Dict[str, Any], None, None]: 91 | for error in errors: 92 | if isinstance(error, ErrorWrapper): 93 | 94 | if loc: 95 | error_loc = loc + error.loc_tuple() 96 | else: 97 | error_loc = error.loc_tuple() 98 | 99 | if isinstance(error.exc, ValidationError): 100 | yield from flatten_errors(error.exc.raw_errors, config, error_loc) 101 | else: 102 | yield error_dict(error.exc, config, error_loc) 103 | elif isinstance(error, list): 104 | yield from flatten_errors(error, config, loc=loc) 105 | else: 106 | raise RuntimeError(f'Unknown error object: {error}') 107 | 108 | 109 | def error_dict(exc: Exception, config: Type['BaseConfig'], loc: 'Loc') -> Dict[str, Any]: 110 | type_ = get_exc_type(exc.__class__) 111 | msg_template = config.error_msg_templates.get(type_) or getattr(exc, 'msg_template', None) 112 | ctx = exc.__dict__ 113 | if msg_template: 114 | msg = msg_template.format(**ctx) 115 | else: 116 | msg = str(exc) 117 | 118 | d: Dict[str, Any] = {'loc': loc, 'msg': msg, 'type': type_} 119 | 120 | if ctx: 121 | d['ctx'] = ctx 122 | 123 | return d 124 | 125 | 126 | _EXC_TYPE_CACHE: Dict[Type[Exception], str] = {} 127 | 128 | 129 | def get_exc_type(cls: Type[Exception]) -> str: 130 | # slightly more efficient than using lru_cache since we don't need to worry about the cache filling up 131 | try: 132 | return _EXC_TYPE_CACHE[cls] 133 | except KeyError: 134 | r = _get_exc_type(cls) 135 | _EXC_TYPE_CACHE[cls] = r 136 | return r 137 | 138 | 139 | def _get_exc_type(cls: Type[Exception]) -> str: 140 | if issubclass(cls, AssertionError): 141 | return 'assertion_error' 142 | 143 | base_name = 'type_error' if issubclass(cls, TypeError) else 'value_error' 144 | if cls in (TypeError, ValueError): 145 | # just TypeError or ValueError, no extra code 146 | return base_name 147 | 148 | # if it's not a TypeError or ValueError, we just take the lowercase of the exception name 149 | # no chaining or snake case logic, use "code" for more complex error types. 150 | code = getattr(cls, 'code', None) or cls.__name__.replace('Error', '').lower() 151 | return base_name + '.' + code 152 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pydantic/json.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import re 3 | import sys 4 | from collections import deque 5 | from decimal import Decimal 6 | from enum import Enum 7 | from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network 8 | from pathlib import Path 9 | from types import GeneratorType 10 | from typing import Any, Callable, Dict, Type, Union 11 | from uuid import UUID 12 | 13 | if sys.version_info >= (3, 7): 14 | Pattern = re.Pattern 15 | else: 16 | # python 3.6 17 | Pattern = re.compile('a').__class__ 18 | 19 | from .color import Color 20 | from .types import SecretBytes, SecretStr 21 | 22 | __all__ = 'pydantic_encoder', 'custom_pydantic_encoder', 'timedelta_isoformat' 23 | 24 | 25 | def isoformat(o: Union[datetime.date, datetime.time]) -> str: 26 | return o.isoformat() 27 | 28 | 29 | def decimal_encoder(dec_value: Decimal) -> Union[int, float]: 30 | """ 31 | Encodes a Decimal as int of there's no exponent, otherwise float 32 | 33 | This is useful when we use ConstrainedDecimal to represent Numeric(x,0) 34 | where a integer (but not int typed) is used. Encoding this as a float 35 | results in failed round-tripping between encode and prase. 36 | Our Id type is a prime example of this. 37 | 38 | >>> decimal_encoder(Decimal("1.0")) 39 | 1.0 40 | 41 | >>> decimal_encoder(Decimal("1")) 42 | 1 43 | """ 44 | if dec_value.as_tuple().exponent >= 0: 45 | return int(dec_value) 46 | else: 47 | return float(dec_value) 48 | 49 | 50 | ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = { 51 | bytes: lambda o: o.decode(), 52 | Color: str, 53 | datetime.date: isoformat, 54 | datetime.datetime: isoformat, 55 | datetime.time: isoformat, 56 | datetime.timedelta: lambda td: td.total_seconds(), 57 | Decimal: decimal_encoder, 58 | Enum: lambda o: o.value, 59 | frozenset: list, 60 | deque: list, 61 | GeneratorType: list, 62 | IPv4Address: str, 63 | IPv4Interface: str, 64 | IPv4Network: str, 65 | IPv6Address: str, 66 | IPv6Interface: str, 67 | IPv6Network: str, 68 | Path: str, 69 | Pattern: lambda o: o.pattern, 70 | SecretBytes: str, 71 | SecretStr: str, 72 | set: list, 73 | UUID: str, 74 | } 75 | 76 | 77 | def pydantic_encoder(obj: Any) -> Any: 78 | from dataclasses import asdict, is_dataclass 79 | 80 | from .main import BaseModel 81 | 82 | if isinstance(obj, BaseModel): 83 | return obj.dict() 84 | elif is_dataclass(obj): 85 | return asdict(obj) 86 | 87 | # Check the class type and its superclasses for a matching encoder 88 | for base in obj.__class__.__mro__[:-1]: 89 | try: 90 | encoder = ENCODERS_BY_TYPE[base] 91 | except KeyError: 92 | continue 93 | return encoder(obj) 94 | else: # We have exited the for loop without finding a suitable encoder 95 | raise TypeError(f"Object of type '{obj.__class__.__name__}' is not JSON serializable") 96 | 97 | 98 | def custom_pydantic_encoder(type_encoders: Dict[Any, Callable[[Type[Any]], Any]], obj: Any) -> Any: 99 | # Check the class type and its superclasses for a matching encoder 100 | for base in obj.__class__.__mro__[:-1]: 101 | try: 102 | encoder = type_encoders[base] 103 | except KeyError: 104 | continue 105 | return encoder(obj) 106 | else: # We have exited the for loop without finding a suitable encoder 107 | return pydantic_encoder(obj) 108 | 109 | 110 | def timedelta_isoformat(td: datetime.timedelta) -> str: 111 | """ 112 | ISO 8601 encoding for timedeltas. 113 | """ 114 | minutes, seconds = divmod(td.seconds, 60) 115 | hours, minutes = divmod(minutes, 60) 116 | return f'P{td.days}DT{hours:d}H{minutes:d}M{seconds:d}.{td.microseconds:06d}S' 117 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pydantic/parse.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pickle 3 | from enum import Enum 4 | from pathlib import Path 5 | from typing import Any, Callable, Union 6 | 7 | from .types import StrBytes 8 | 9 | 10 | class Protocol(str, Enum): 11 | json = 'json' 12 | pickle = 'pickle' 13 | 14 | 15 | def load_str_bytes( 16 | b: StrBytes, 17 | *, 18 | content_type: str = None, 19 | encoding: str = 'utf8', 20 | proto: Protocol = None, 21 | allow_pickle: bool = False, 22 | json_loads: Callable[[str], Any] = json.loads, 23 | ) -> Any: 24 | if proto is None and content_type: 25 | if content_type.endswith(('json', 'javascript')): 26 | pass 27 | elif allow_pickle and content_type.endswith('pickle'): 28 | proto = Protocol.pickle 29 | else: 30 | raise TypeError(f'Unknown content-type: {content_type}') 31 | 32 | proto = proto or Protocol.json 33 | 34 | if proto == Protocol.json: 35 | if isinstance(b, bytes): 36 | b = b.decode(encoding) 37 | return json_loads(b) 38 | elif proto == Protocol.pickle: 39 | if not allow_pickle: 40 | raise RuntimeError('Trying to decode with pickle with allow_pickle=False') 41 | bb = b if isinstance(b, bytes) else b.encode() 42 | return pickle.loads(bb) 43 | else: 44 | raise TypeError(f'Unknown protocol: {proto}') 45 | 46 | 47 | def load_file( 48 | path: Union[str, Path], 49 | *, 50 | content_type: str = None, 51 | encoding: str = 'utf8', 52 | proto: Protocol = None, 53 | allow_pickle: bool = False, 54 | json_loads: Callable[[str], Any] = json.loads, 55 | ) -> Any: 56 | path = Path(path) 57 | b = path.read_bytes() 58 | if content_type is None: 59 | if path.suffix in ('.js', '.json'): 60 | proto = Protocol.json 61 | elif path.suffix == '.pkl': 62 | proto = Protocol.pickle 63 | 64 | return load_str_bytes( 65 | b, proto=proto, content_type=content_type, encoding=encoding, allow_pickle=allow_pickle, json_loads=json_loads 66 | ) 67 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pydantic/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/src/qgis_stac/lib/pydantic/py.typed -------------------------------------------------------------------------------- /src/qgis_stac/lib/pydantic/tools.py: -------------------------------------------------------------------------------- 1 | import json 2 | from functools import lru_cache 3 | from pathlib import Path 4 | from typing import Any, Callable, Optional, Type, TypeVar, Union 5 | 6 | from .parse import Protocol, load_file, load_str_bytes 7 | from .types import StrBytes 8 | from .typing import display_as_type 9 | 10 | __all__ = ('parse_file_as', 'parse_obj_as', 'parse_raw_as') 11 | 12 | NameFactory = Union[str, Callable[[Type[Any]], str]] 13 | 14 | 15 | def _generate_parsing_type_name(type_: Any) -> str: 16 | return f'ParsingModel[{display_as_type(type_)}]' 17 | 18 | 19 | @lru_cache(maxsize=2048) 20 | def _get_parsing_type(type_: Any, *, type_name: Optional[NameFactory] = None) -> Any: 21 | from pydantic.main import create_model 22 | 23 | if type_name is None: 24 | type_name = _generate_parsing_type_name 25 | if not isinstance(type_name, str): 26 | type_name = type_name(type_) 27 | return create_model(type_name, __root__=(type_, ...)) 28 | 29 | 30 | T = TypeVar('T') 31 | 32 | 33 | def parse_obj_as(type_: Type[T], obj: Any, *, type_name: Optional[NameFactory] = None) -> T: 34 | model_type = _get_parsing_type(type_, type_name=type_name) # type: ignore[arg-type] 35 | return model_type(__root__=obj).__root__ 36 | 37 | 38 | def parse_file_as( 39 | type_: Type[T], 40 | path: Union[str, Path], 41 | *, 42 | content_type: str = None, 43 | encoding: str = 'utf8', 44 | proto: Protocol = None, 45 | allow_pickle: bool = False, 46 | json_loads: Callable[[str], Any] = json.loads, 47 | type_name: Optional[NameFactory] = None, 48 | ) -> T: 49 | obj = load_file( 50 | path, 51 | proto=proto, 52 | content_type=content_type, 53 | encoding=encoding, 54 | allow_pickle=allow_pickle, 55 | json_loads=json_loads, 56 | ) 57 | return parse_obj_as(type_, obj, type_name=type_name) 58 | 59 | 60 | def parse_raw_as( 61 | type_: Type[T], 62 | b: StrBytes, 63 | *, 64 | content_type: str = None, 65 | encoding: str = 'utf8', 66 | proto: Protocol = None, 67 | allow_pickle: bool = False, 68 | json_loads: Callable[[str], Any] = json.loads, 69 | type_name: Optional[NameFactory] = None, 70 | ) -> T: 71 | obj = load_str_bytes( 72 | b, 73 | proto=proto, 74 | content_type=content_type, 75 | encoding=encoding, 76 | allow_pickle=allow_pickle, 77 | json_loads=json_loads, 78 | ) 79 | return parse_obj_as(type_, obj, type_name=type_name) 80 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pydantic/version.py: -------------------------------------------------------------------------------- 1 | __all__ = 'VERSION', 'version_info' 2 | 3 | VERSION = '1.8.2' 4 | 5 | 6 | def version_info() -> str: 7 | import platform 8 | import sys 9 | from importlib import import_module 10 | from pathlib import Path 11 | 12 | from .main import compiled 13 | 14 | optional_deps = [] 15 | for p in ('devtools', 'dotenv', 'email-validator', 'typing-extensions'): 16 | try: 17 | import_module(p.replace('-', '_')) 18 | except ImportError: 19 | continue 20 | optional_deps.append(p) 21 | 22 | info = { 23 | 'pydantic version': VERSION, 24 | 'pydantic compiled': compiled, 25 | 'install path': Path(__file__).resolve().parent, 26 | 'python version': sys.version, 27 | 'platform': platform.platform(), 28 | 'optional deps. installed': optional_deps, 29 | } 30 | return '\n'.join('{:>30} {}'.format(k + ':', str(v).replace('\n', ' ')) for k, v in info.items()) 31 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019, 2020, 2021 the authors 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 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac/asset.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union 3 | 4 | from pystac import common_metadata 5 | from pystac import utils 6 | 7 | if TYPE_CHECKING: 8 | from pystac.collection import Collection as Collection_Type 9 | from pystac.common_metadata import CommonMetadata as CommonMetadata_Type 10 | from pystac.item import Item as Item_Type 11 | 12 | 13 | class Asset: 14 | """An object that contains a link to data associated with an Item or Collection that 15 | can be downloaded or streamed. 16 | 17 | Args: 18 | href : Link to the asset object. Relative and absolute links are both 19 | allowed. 20 | title : Optional displayed title for clients and users. 21 | description : A description of the Asset providing additional details, 22 | such as how it was processed or created. CommonMark 0.29 syntax MAY be used 23 | for rich text representation. 24 | media_type : Optional description of the media type. Registered Media Types 25 | are preferred. See :class:`~pystac.MediaType` for common media types. 26 | roles : Optional, Semantic roles (i.e. thumbnail, overview, 27 | data, metadata) of the asset. 28 | extra_fields : Optional, additional fields for this asset. This is used 29 | by extensions as a way to serialize and deserialize properties on asset 30 | object JSON. 31 | """ 32 | 33 | href: str 34 | """Link to the asset object. Relative and absolute links are both allowed.""" 35 | 36 | title: Optional[str] 37 | """Optional displayed title for clients and users.""" 38 | 39 | description: Optional[str] 40 | """A description of the Asset providing additional details, such as how it was 41 | processed or created. CommonMark 0.29 syntax MAY be used for rich text 42 | representation.""" 43 | 44 | media_type: Optional[str] 45 | """Optional description of the media type. Registered Media Types are preferred. 46 | See :class:`~pystac.MediaType` for common media types.""" 47 | 48 | roles: Optional[List[str]] 49 | """Optional, Semantic roles (i.e. thumbnail, overview, data, metadata) of the 50 | asset.""" 51 | 52 | owner: Optional[Union["Item_Type", "Collection_Type"]] 53 | """The :class:`~pystac.Item` or :class:`~pystac.Collection` that this asset belongs 54 | to, or ``None`` if it has no owner.""" 55 | 56 | extra_fields: Dict[str, Any] 57 | """Optional, additional fields for this asset. This is used by extensions as a 58 | way to serialize and deserialize properties on asset object JSON.""" 59 | 60 | def __init__( 61 | self, 62 | href: str, 63 | title: Optional[str] = None, 64 | description: Optional[str] = None, 65 | media_type: Optional[str] = None, 66 | roles: Optional[List[str]] = None, 67 | extra_fields: Optional[Dict[str, Any]] = None, 68 | ) -> None: 69 | self.href = href 70 | self.title = title 71 | self.description = description 72 | self.media_type = media_type 73 | self.roles = roles 74 | self.extra_fields = extra_fields or {} 75 | 76 | # The Item which owns this Asset. 77 | self.owner = None 78 | 79 | def set_owner(self, obj: Union["Collection_Type", "Item_Type"]) -> None: 80 | """Sets the owning item of this Asset. 81 | 82 | The owning item will be used to resolve relative HREFs of this asset. 83 | 84 | Args: 85 | obj: The Collection or Item that owns this asset. 86 | """ 87 | self.owner = obj 88 | 89 | def get_absolute_href(self) -> Optional[str]: 90 | """Gets the absolute href for this asset, if possible. 91 | 92 | If this Asset has no associated Item, and the asset HREF is a relative path, 93 | this method will return None. 94 | 95 | Returns: 96 | str: The absolute HREF of this asset, or None if an absolute HREF could not 97 | be determined. 98 | """ 99 | if utils.is_absolute_href(self.href): 100 | return self.href 101 | else: 102 | if self.owner is not None: 103 | return utils.make_absolute_href(self.href, self.owner.get_self_href()) 104 | else: 105 | return None 106 | 107 | def to_dict(self) -> Dict[str, Any]: 108 | """Generate a dictionary representing the JSON of this Asset. 109 | 110 | Returns: 111 | dict: A serialization of the Asset that can be written out as JSON. 112 | """ 113 | 114 | d: Dict[str, Any] = {"href": self.href} 115 | 116 | if self.media_type is not None: 117 | d["type"] = self.media_type 118 | 119 | if self.title is not None: 120 | d["title"] = self.title 121 | 122 | if self.description is not None: 123 | d["description"] = self.description 124 | 125 | if self.extra_fields is not None and len(self.extra_fields) > 0: 126 | for k, v in self.extra_fields.items(): 127 | d[k] = v 128 | 129 | if self.roles is not None: 130 | d["roles"] = self.roles 131 | 132 | return d 133 | 134 | def clone(self) -> "Asset": 135 | """Clones this asset. 136 | 137 | Returns: 138 | Asset: The clone of this asset. 139 | """ 140 | cls = self.__class__ 141 | return cls( 142 | href=self.href, 143 | title=self.title, 144 | description=self.description, 145 | media_type=self.media_type, 146 | roles=self.roles, 147 | extra_fields=self.extra_fields, 148 | ) 149 | 150 | @property 151 | def common_metadata(self) -> "CommonMetadata_Type": 152 | """Access the asset's common metadata fields as a 153 | :class:`~pystac.CommonMetadata` object.""" 154 | return common_metadata.CommonMetadata(self) 155 | 156 | def __repr__(self) -> str: 157 | return "".format(self.href) 158 | 159 | @classmethod 160 | def from_dict(cls, d: Dict[str, Any]) -> "Asset": 161 | """Constructs an Asset from a dict. 162 | 163 | Returns: 164 | Asset: The Asset deserialized from the JSON dict. 165 | """ 166 | d = copy(d) 167 | href = d.pop("href") 168 | media_type = d.pop("type", None) 169 | title = d.pop("title", None) 170 | description = d.pop("description", None) 171 | roles = d.pop("roles", None) 172 | properties = None 173 | if any(d): 174 | properties = d 175 | 176 | return cls( 177 | href=href, 178 | media_type=media_type, 179 | title=title, 180 | description=description, 181 | roles=roles, 182 | extra_fields=properties, 183 | ) 184 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac/errors.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, Union 2 | 3 | 4 | class STACError(Exception): 5 | """A STACError is raised for errors relating to STAC, e.g. for 6 | invalid formats or trying to operate on a STAC that does not have 7 | the required information available. 8 | """ 9 | 10 | pass 11 | 12 | 13 | class STACTypeError(Exception): 14 | """A STACTypeError is raised when encountering a representation of 15 | a STAC entity that is not correct for the context; for example, if 16 | a Catalog JSON was read in as an Item. 17 | """ 18 | 19 | pass 20 | 21 | 22 | class DuplicateObjectKeyError(Exception): 23 | """Raised when deserializing a JSON object containing a duplicate key.""" 24 | 25 | pass 26 | 27 | 28 | class ExtensionTypeError(Exception): 29 | """An ExtensionTypeError is raised when an extension is used against 30 | an object that the extension does not apply to 31 | """ 32 | 33 | pass 34 | 35 | 36 | class ExtensionAlreadyExistsError(Exception): 37 | """An ExtensionAlreadyExistsError is raised when extension hooks 38 | are registered with PySTAC if there are already hooks registered 39 | for an extension with the same ID.""" 40 | 41 | pass 42 | 43 | 44 | class ExtensionNotImplemented(Exception): 45 | """Attempted to extend a STAC object that does not implement the given 46 | extension.""" 47 | 48 | 49 | class RequiredPropertyMissing(Exception): 50 | """This error is raised when a required value was expected 51 | to be there but was missing or None. This will happen, for example, 52 | in an extension that has required properties, where the required 53 | property is missing from the extended object 54 | 55 | Args: 56 | obj: Description of the object that will have a property missing. 57 | Should include a __repr__ that identifies the object for the 58 | error message, or be a string that describes the object. 59 | prop: The property that is missing 60 | """ 61 | 62 | def __init__( 63 | self, obj: Union[str, Any], prop: str, msg: Optional[str] = None 64 | ) -> None: 65 | msg = msg or f"{repr(obj)} does not have required property {prop}" 66 | super().__init__(msg) 67 | 68 | 69 | class STACValidationError(Exception): 70 | """Represents a validation error. Thrown by validation calls if the STAC JSON 71 | is invalid. 72 | 73 | Args: 74 | source : Source of the exception. Type will be determined by the 75 | validation implementation. For the default JsonSchemaValidator this will a 76 | the ``jsonschema.ValidationError``. 77 | """ 78 | 79 | def __init__(self, message: str, source: Optional[Any] = None): 80 | super().__init__(message) 81 | self.source = source 82 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac/extensions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/src/qgis_stac/lib/pystac/extensions/__init__.py -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac/extensions/hooks.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from functools import lru_cache 3 | from typing import Any, Dict, Iterable, List, Optional, Set, TYPE_CHECKING, Union 4 | 5 | import pystac 6 | from pystac.serialization.identify import STACJSONDescription, STACVersionID 7 | 8 | if TYPE_CHECKING: 9 | from pystac.stac_object import STACObject as STACObject_Type 10 | 11 | 12 | class ExtensionHooks(ABC): 13 | @property 14 | @abstractmethod 15 | def schema_uri(self) -> str: 16 | """The schema_uri for the current version of this extension""" 17 | raise NotImplementedError 18 | 19 | @property 20 | @abstractmethod 21 | def prev_extension_ids(self) -> Set[str]: 22 | """A set of previous extension IDs (schema URIs or old short ids) 23 | that should be migrated to the latest schema URI in the 'stac_extensions' 24 | property. Override with a class attribute so that the set of previous 25 | IDs is only created once. 26 | """ 27 | raise NotImplementedError 28 | 29 | @property 30 | @abstractmethod 31 | def stac_object_types(self) -> Set[pystac.STACObjectType]: 32 | """A set of STACObjectType for which migration logic will be applied.""" 33 | raise NotImplementedError 34 | 35 | @lru_cache() 36 | def _get_stac_object_types(self) -> Set[str]: 37 | """Translation of stac_object_types to strings, cached""" 38 | return set([x.value for x in self.stac_object_types]) 39 | 40 | def get_object_links( 41 | self, obj: "STACObject_Type" 42 | ) -> Optional[List[Union[str, pystac.RelType]]]: 43 | return None 44 | 45 | def migrate( 46 | self, obj: Dict[str, Any], version: STACVersionID, info: STACJSONDescription 47 | ) -> None: 48 | """Migrate a STAC Object in dict format from a previous version. 49 | The base implementation will update the stac_extensions to the latest 50 | schema ID. This method will only be called for STAC objects that have been 51 | identified as a previous version of STAC. Implementations should directly 52 | manipulate the obj dict. Remember to call super() in order to change out 53 | the old 'stac_extension' entry with the latest schema URI. 54 | """ 55 | # Migrate schema versions 56 | for prev_id in self.prev_extension_ids: 57 | if prev_id in info.extensions: 58 | try: 59 | i = obj["stac_extensions"].index(prev_id) 60 | obj["stac_extensions"][i] = self.schema_uri 61 | except ValueError: 62 | obj["stac_extensions"].append(self.schema_uri) 63 | break 64 | 65 | 66 | class RegisteredExtensionHooks: 67 | hooks: Dict[str, ExtensionHooks] 68 | 69 | def __init__(self, hooks: Iterable[ExtensionHooks]): 70 | self.hooks = dict([(e.schema_uri, e) for e in hooks]) 71 | 72 | def add_extension_hooks(self, hooks: ExtensionHooks) -> None: 73 | e_id = hooks.schema_uri 74 | if e_id in self.hooks: 75 | raise pystac.ExtensionAlreadyExistsError( 76 | "ExtensionDefinition with id '{}' already exists.".format(e_id) 77 | ) 78 | 79 | self.hooks[e_id] = hooks 80 | 81 | def remove_extension_hooks(self, extension_id: str) -> None: 82 | if extension_id in self.hooks: 83 | del self.hooks[extension_id] 84 | 85 | def get_extended_object_links( 86 | self, obj: "STACObject_Type" 87 | ) -> List[Union[str, pystac.RelType]]: 88 | result: Optional[List[Union[str, pystac.RelType]]] = None 89 | for ext in obj.stac_extensions: 90 | if ext in self.hooks: 91 | ext_result = self.hooks[ext].get_object_links(obj) 92 | if ext_result is not None: 93 | if result is None: 94 | result = ext_result 95 | else: 96 | result.extend(ext_result) 97 | return result or [] 98 | 99 | def migrate( 100 | self, obj: Dict[str, Any], version: STACVersionID, info: STACJSONDescription 101 | ) -> None: 102 | for hooks in self.hooks.values(): 103 | if info.object_type in hooks._get_stac_object_types(): 104 | hooks.migrate(obj, version, info) 105 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac/media_type.py: -------------------------------------------------------------------------------- 1 | from pystac.utils import StringEnum 2 | 3 | 4 | class MediaType(StringEnum): 5 | """A list of common media types that can be used in STAC Asset and Link metadata.""" 6 | 7 | COG = "image/tiff; application=geotiff; profile=cloud-optimized" 8 | GEOJSON = "application/geo+json" 9 | GEOPACKAGE = "application/geopackage+sqlite3" 10 | GEOTIFF = "image/tiff; application=geotiff" 11 | HDF = "application/x-hdf" # Hierarchical Data Format versions 4 and earlier. 12 | HDF5 = "application/x-hdf5" # Hierarchical Data Format version 5 13 | JPEG = "image/jpeg" 14 | JPEG2000 = "image/jp2" 15 | JSON = "application/json" 16 | PNG = "image/png" 17 | TEXT = "text/plain" 18 | TIFF = "image/tiff" 19 | XML = "application/xml" 20 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac/provider.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Optional 2 | 3 | from pystac.utils import StringEnum 4 | 5 | 6 | class ProviderRole(StringEnum): 7 | """Enumerates the allows values of the Provider "role" field.""" 8 | 9 | LICENSOR = "licensor" 10 | PRODUCER = "producer" 11 | PROCESSOR = "processor" 12 | HOST = "host" 13 | 14 | 15 | class Provider: 16 | """Provides information about a provider of STAC data. A provider is any of the 17 | organizations that captured or processed the content of the collection and therefore 18 | influenced the data offered by this collection. May also include information about 19 | the final storage provider hosting the data. 20 | 21 | Args: 22 | name : The name of the organization or the individual. 23 | description : Optional multi-line description to add further provider 24 | information such as processing details for processors and producers, 25 | hosting details for hosts or basic contact information. 26 | roles : Optional roles of the provider. Any of 27 | licensor, producer, processor or host. 28 | url : Optional homepage on which the provider describes the dataset 29 | and publishes contact information. 30 | extra_fields : Optional dictionary containing additional top-level fields 31 | defined on the Provider object. 32 | """ 33 | 34 | name: str 35 | """The name of the organization or the individual.""" 36 | 37 | description: Optional[str] 38 | """Optional multi-line description to add further provider 39 | information such as processing details for processors and producers, 40 | hosting details for hosts or basic contact information.""" 41 | 42 | roles: Optional[List[ProviderRole]] 43 | """Optional roles of the provider. Any of 44 | licensor, producer, processor or host.""" 45 | 46 | url: Optional[str] 47 | """Optional homepage on which the provider describes the dataset 48 | and publishes contact information.""" 49 | 50 | extra_fields: Dict[str, Any] 51 | """Dictionary containing additional top-level fields defined on the Provider 52 | object.""" 53 | 54 | def __init__( 55 | self, 56 | name: str, 57 | description: Optional[str] = None, 58 | roles: Optional[List[ProviderRole]] = None, 59 | url: Optional[str] = None, 60 | extra_fields: Optional[Dict[str, Any]] = None, 61 | ): 62 | self.name = name 63 | self.description = description 64 | self.roles = roles 65 | self.url = url 66 | self.extra_fields = extra_fields or {} 67 | 68 | def __eq__(self, o: object) -> bool: 69 | if not isinstance(o, Provider): 70 | return NotImplemented 71 | return self.to_dict() == o.to_dict() 72 | 73 | def to_dict(self) -> Dict[str, Any]: 74 | """Generate a dictionary representing the JSON of this Provider. 75 | 76 | Returns: 77 | dict: A serialization of the Provider that can be written out as JSON. 78 | """ 79 | d: Dict[str, Any] = {"name": self.name} 80 | if self.description is not None: 81 | d["description"] = self.description 82 | if self.roles is not None: 83 | d["roles"] = self.roles 84 | if self.url is not None: 85 | d["url"] = self.url 86 | 87 | d.update(self.extra_fields) 88 | 89 | return d 90 | 91 | @staticmethod 92 | def from_dict(d: Dict[str, Any]) -> "Provider": 93 | """Constructs an Provider from a dict. 94 | 95 | Returns: 96 | Provider: The Provider deserialized from the JSON dict. 97 | """ 98 | return Provider( 99 | name=d["name"], 100 | description=d.get("description"), 101 | roles=d.get( 102 | "roles", 103 | ), 104 | url=d.get("url"), 105 | extra_fields={ 106 | k: v 107 | for k, v in d.items() 108 | if k not in {"name", "description", "roles", "url"} 109 | }, 110 | ) 111 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/src/qgis_stac/lib/pystac/py.typed -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac/rel_type.py: -------------------------------------------------------------------------------- 1 | from pystac.utils import StringEnum 2 | 3 | 4 | class RelType(StringEnum): 5 | """A list of common rel types that can be used in STAC Link metadata. 6 | See :stac-spec:`"Using Relation Types ` 7 | in the STAC Best Practices for guidelines on using relation types. You may also want 8 | to refer to the "Relation type" documentation for 9 | :stac-spec:`Catalogs `, 10 | :stac-spec:`Collections `, 11 | or :stac-spec:`Items ` for relation types 12 | specific to those STAC objects. 13 | """ 14 | 15 | ALTERNATE = "alternate" 16 | CANONICAL = "canonical" 17 | CHILD = "child" 18 | COLLECTION = "collection" 19 | ITEM = "item" 20 | ITEMS = "items" 21 | LICENSE = "license" 22 | DERIVED_FROM = "derived_from" 23 | NEXT = "next" 24 | PARENT = "parent" 25 | PREV = "prev" 26 | PREVIEW = "preview" 27 | ROOT = "root" 28 | SELF = "self" 29 | VIA = "via" 30 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac/serialization/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "merge_common_properties", 3 | "migrate_to_latest", 4 | "STACVersionRange", 5 | "identify_stac_object", 6 | "identify_stac_object_type", 7 | ] 8 | from pystac.serialization.identify import ( 9 | STACVersionRange, 10 | identify_stac_object, 11 | identify_stac_object_type, 12 | ) 13 | from pystac.serialization.common_properties import merge_common_properties 14 | from pystac.serialization.migrate import migrate_to_latest 15 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac/serialization/common_properties.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Iterable, List, Optional, Union, cast 2 | 3 | import pystac 4 | from pystac.cache import CollectionCache 5 | from pystac.serialization.identify import STACVersionID 6 | from pystac.utils import make_absolute_href 7 | 8 | 9 | def merge_common_properties( 10 | item_dict: Dict[str, Any], 11 | collection_cache: Optional[CollectionCache] = None, 12 | json_href: Optional[str] = None, 13 | ) -> bool: 14 | """Merges Collection properties into an Item. 15 | 16 | Note: This is only applicable to reading old STAC versions (pre 1.0.0-beta.1). 17 | 18 | Args: 19 | item_dict : JSON dict of the Item which properties should be merged 20 | into. 21 | collection_cache : Optional CollectionCache 22 | that will be used to read and write cached collections. 23 | json_href: The HREF of the file that this JSON comes from. Used 24 | to resolve relative paths. 25 | 26 | Returns: 27 | bool: True if Collection properties have been merged, otherwise False. 28 | """ 29 | properties_merged = False 30 | 31 | collection: Optional[Union[pystac.Collection, Dict[str, Any]]] = None 32 | collection_href: Optional[str] = None 33 | 34 | stac_version = item_dict.get("stac_version") 35 | 36 | # The commons extension was removed in 1.0.0-beta.1, so if this is an earlier STAC 37 | # item we don't have to bother with merging. 38 | if stac_version is not None and STACVersionID(stac_version) > "0.9.0": 39 | return False 40 | 41 | # Check to see if this is a 0.9.0 item that 42 | # doesn't extend the commons extension, in which case 43 | # we don't have to merge. 44 | if stac_version is not None and stac_version == "0.9.0": 45 | stac_extensions = item_dict.get("stac_extensions") 46 | if isinstance(stac_extensions, list): 47 | if "commons" not in stac_extensions: 48 | return False 49 | else: 50 | return False 51 | 52 | # Try the cache if we have a collection ID. 53 | collection_id = item_dict.get("collection") 54 | if collection_id is not None: 55 | if collection_cache is not None: 56 | collection = collection_cache.get_by_id(collection_id) 57 | 58 | # Next, try the collection link. 59 | if collection is None: 60 | # Account for 0.5 links, which were dicts 61 | if isinstance(item_dict["links"], dict): 62 | links = list(cast(Iterable[Dict[str, Any]], item_dict["links"].values())) 63 | else: 64 | links = cast(List[Dict[str, Any]], item_dict["links"]) 65 | 66 | collection_link = next( 67 | (link for link in links if link["rel"] == pystac.RelType.COLLECTION), None 68 | ) 69 | if collection_link is not None: 70 | collection_href = collection_link.get("href") 71 | if collection_href is not None: 72 | if json_href is not None: 73 | collection_href = make_absolute_href(collection_href, json_href) 74 | if collection_cache is not None: 75 | collection = collection_cache.get_by_href(collection_href) 76 | 77 | if collection is None: 78 | collection = pystac.StacIO.default().read_json(collection_href) 79 | 80 | if collection is not None: 81 | collection_props: Optional[Dict[str, Any]] = None 82 | if isinstance(collection, pystac.Collection): 83 | collection_id = collection.id 84 | collection_props = collection.extra_fields.get("properties") 85 | elif isinstance(collection, dict): 86 | collection_id = collection["id"] 87 | if "properties" in collection: 88 | collection_props = collection["properties"] 89 | else: 90 | raise ValueError( 91 | "{} is expected to be a Collection or " 92 | "dict but is neither.".format(collection) 93 | ) 94 | 95 | if collection_props is not None: 96 | for k in collection_props: 97 | if k not in item_dict["properties"]: 98 | properties_merged = True 99 | item_dict["properties"][k] = collection_props[k] 100 | 101 | if ( 102 | collection_cache is not None 103 | and collection_id is not None 104 | and not collection_cache.contains_id(collection_id) 105 | ): 106 | collection_cache.cache(collection, href=collection_href) 107 | 108 | return properties_merged 109 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac/version.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Optional 3 | 4 | __version__ = "1.3.0" 5 | """Library version""" 6 | 7 | 8 | class STACVersion: 9 | DEFAULT_STAC_VERSION = "1.0.0" 10 | """Latest STAC version supported by PySTAC""" 11 | 12 | # Version that holds a user-set STAC version to use. 13 | _override_version: Optional[str] = None 14 | 15 | OVERRIDE_VERSION_ENV_VAR = "PYSTAC_STAC_VERSION_OVERRIDE" 16 | 17 | @classmethod 18 | def get_stac_version(cls) -> str: 19 | if cls._override_version is not None: 20 | return cls._override_version 21 | 22 | env_version = os.environ.get(cls.OVERRIDE_VERSION_ENV_VAR) 23 | if env_version is not None: 24 | return env_version 25 | 26 | return cls.DEFAULT_STAC_VERSION 27 | 28 | @classmethod 29 | def set_stac_version(cls, stac_version: Optional[str]) -> None: 30 | cls._override_version = stac_version 31 | 32 | 33 | def get_stac_version() -> str: 34 | """Returns the STAC version PySTAC writes as the "stac_version" property for 35 | any object it serializes into JSON. 36 | 37 | If a call to ``set_stac_version`` was made, this will return the value it was 38 | called with. Next it will check the environment for a PYSTAC_STAC_VERSION_OVERRIDE 39 | variable. Otherwise it will return the latest STAC version that this version of 40 | PySTAC supports. 41 | 42 | Returns: 43 | str: The STAC Version PySTAC is set up to use. 44 | """ 45 | return STACVersion.get_stac_version() 46 | 47 | 48 | def set_stac_version(stac_version: Optional[str]) -> None: 49 | """Sets the STAC version that PySTAC should use. 50 | 51 | This is the version that will be set as the "stac_version" property 52 | on any JSON STAC objects written by PySTAC. If set to None, the override version 53 | will be cleared if previously set and the default or an override taken from the 54 | environment will be used. 55 | 56 | You can also set the environment variable PYSTAC_STAC_VERSION_OVERRIDE to override 57 | the version. 58 | 59 | Args: 60 | stac_version : The STAC version to use instead of the latest STAC version 61 | that PySTAC supports (described in STACVersion.DEFAULT_STAC_VERSION). 62 | If None, clear to use the default for this version of PySTAC. 63 | 64 | Note: 65 | Setting the STAC version to something besides the default version will not 66 | effect the format of STAC read or written; it will only override the 67 | ``stac_version`` property of the objects being written. Setting this 68 | incorrectly can produce invalid STAC. 69 | """ 70 | STACVersion.set_stac_version(stac_version) 71 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac_client/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Jon Duckworth 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 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac_client/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from pystac_client.version import __version__ 4 | from pystac_client.item_search import ItemSearch 5 | from pystac_client.client import Client 6 | from pystac_client.collection_client import CollectionClient 7 | from pystac_client.conformance import ConformanceClasses 8 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac_client/collection_client.py: -------------------------------------------------------------------------------- 1 | from typing import (Iterable, TYPE_CHECKING) 2 | 3 | import pystac 4 | from pystac_client.item_search import ItemSearch 5 | 6 | if TYPE_CHECKING: 7 | from pystac.item import Item as Item_Type 8 | 9 | 10 | class CollectionClient(pystac.Collection): 11 | def __repr__(self): 12 | return ''.format(self.id) 13 | 14 | def get_items(self) -> Iterable["Item_Type"]: 15 | """Return all items in this Collection. 16 | 17 | If the Collection contains a link of with a `rel` value of `items`, that link will be 18 | used to iterate through items. Otherwise, the default PySTAC behavior is assumed. 19 | 20 | Return: 21 | Iterable[Item]: Generator of items whose parent is this catalog. 22 | """ 23 | 24 | link = self.get_single_link('items') 25 | if link is not None: 26 | search = ItemSearch(link.href, method='GET', stac_io=self.get_root()._stac_io) 27 | yield from search.get_items() 28 | else: 29 | yield from super().get_items() 30 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac_client/conformance.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | import re 3 | 4 | 5 | class ConformanceClasses(Enum): 6 | """Enumeration class for Conformance Classes 7 | 8 | """ 9 | 10 | stac_prefix = re.escape("https://api.stacspec.org/v1.0.") 11 | 12 | # defined conformance classes regexes 13 | CORE = fr"{stac_prefix}(.*){re.escape('/core')}" 14 | ITEM_SEARCH = fr"{stac_prefix}(.*){re.escape('/item-search')}" 15 | CONTEXT = fr"{stac_prefix}(.*){re.escape('/item-search#context')}" 16 | FIELDS = fr"{stac_prefix}(.*){re.escape('/item-search#fields')}" 17 | SORT = fr"{stac_prefix}(.*){re.escape('/item-search#sort')}" 18 | QUERY = fr"{stac_prefix}(.*){re.escape('/item-search#query')}" 19 | FILTER = fr"{stac_prefix}(.*){re.escape('/item-search#filter')}" 20 | COLLECTIONS = re.escape("http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30") 21 | 22 | 23 | CONFORMANCE_URIS = {c.name: c.value for c in ConformanceClasses} 24 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac_client/exceptions.py: -------------------------------------------------------------------------------- 1 | class APIError(Exception): 2 | """Raised when unexpected server error.""" 3 | 4 | 5 | class ParametersError(Exception): 6 | """Raised when invalid parameters are used in a query""" 7 | -------------------------------------------------------------------------------- /src/qgis_stac/lib/pystac_client/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.3.2' 2 | -------------------------------------------------------------------------------- /src/qgis_stac/ui/asset_widget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | AssetWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 931 10 | 60 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | 21 | 16777215 22 | 222 23 | 24 | 25 | 26 | Asset 27 | 28 | 29 | 30 | 4 31 | 32 | 33 | 5 34 | 35 | 36 | 5 37 | 38 | 39 | 5 40 | 41 | 42 | 43 | 44 | QLayout::SetDefaultConstraint 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | true 53 | 54 | 55 | 56 | 57 | 58 | 59 | Select to add as a layer 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | true 70 | 71 | 72 | 73 | 74 | 75 | 76 | Select to download 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | Qt::Horizontal 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/qgis_stac/ui/item_assets_widget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | AssetsDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1012 10 | 485 11 | 12 | 13 | 14 | Assets 15 | 16 | 17 | 18 | 19 | 20 | font: 14pt; 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | font-size: 15px; font-weight: 300; 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Qt::Horizontal 41 | 42 | 43 | 44 | 40 45 | 20 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Asset file type. 56 | 57 | 58 | font-weight:bold; 59 | 60 | 61 | Type 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | <html><head/><body><p>Asset name, as defined in the STAC asset.</p></body></html> 83 | 84 | 85 | 86 | 87 | 88 | font-weight:bold; 89 | 90 | 91 | Name 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | false 101 | 102 | 103 | background:white; 104 | 105 | 106 | QFrame::StyledPanel 107 | 108 | 109 | true 110 | 111 | 112 | 113 | 114 | 0 115 | 0 116 | 992 117 | 304 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | Qt::Horizontal 129 | 130 | 131 | 132 | 40 133 | 20 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | false 142 | 143 | 144 | Loads the selected assets as layers. 145 | 146 | 147 | Add assets as layers 148 | 149 | 150 | 151 | 152 | 153 | 154 | false 155 | 156 | 157 | <html><head/><body><p>Downloads the select assets into the filesystem. The asset can be loaded after download is finished if the related plugin setting is enabled.</p></body></html> 158 | 159 | 160 | Download the assets 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | Qt::Horizontal 170 | 171 | 172 | 173 | 40 174 | 20 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /src/qgis_stac/ui/queryable_property.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | QueryablePropertyWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 649 10 | 37 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | 21 | 16777215 22 | 222 23 | 24 | 25 | 26 | Asset 27 | 28 | 29 | 30 | 4 31 | 32 | 33 | 5 34 | 35 | 36 | 5 37 | 38 | 39 | 5 40 | 41 | 42 | 43 | 44 | QLayout::SetDefaultConstraint 45 | 46 | 47 | 48 | 49 | 50 | 0 51 | 0 52 | 53 | 54 | 55 | QFrame::NoFrame 56 | 57 | 58 | QFrame::Raised 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 0 67 | 0 68 | 69 | 70 | 71 | QFrame::NoFrame 72 | 73 | 74 | QFrame::Raised 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 0 83 | 0 84 | 85 | 86 | 87 | Operator to be used when filtering, defaults to equal operator. 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /src/qgis_stac/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Plugin utilities 4 | """ 5 | 6 | import datetime 7 | import os 8 | import subprocess 9 | import sys 10 | import uuid 11 | 12 | from osgeo import gdal 13 | 14 | from qgis.PyQt import QtCore, QtGui 15 | from qgis.core import Qgis, QgsMessageLog 16 | 17 | from .api.models import ApiCapability 18 | from .conf import ( 19 | ConnectionSettings, 20 | settings_manager 21 | ) 22 | 23 | from .definitions.catalog import CATALOGS, SITE 24 | 25 | 26 | def tr(message): 27 | """Get the translation for a string using Qt translation API. 28 | We implement this ourselves since we do not inherit QObject. 29 | 30 | :param message: String for translation. 31 | :type message: str, QString 32 | 33 | :returns: Translated version of message. 34 | :rtype: QString 35 | """ 36 | # noinspection PyTypeChecker,PyArgumentList,PyCallByClass 37 | return QtCore.QCoreApplication.translate("QgisStac", message) 38 | 39 | 40 | def log( 41 | message: str, 42 | name: str = "qgis_stac", 43 | info: bool = True, 44 | notify: bool = True, 45 | ): 46 | """ Logs the message into QGIS logs using qgis_stac as the default 47 | log instance. 48 | If notify_user is True, user will be notified about the log. 49 | 50 | :param message: The log message 51 | :type message: str 52 | 53 | :param name: Name of te log instance, qgis_stac is the default 54 | :type message: str 55 | 56 | :param info: Whether the message is about info or a 57 | warning 58 | :type info: bool 59 | 60 | :param notify: Whether to notify user about the log 61 | :type notify: bool 62 | """ 63 | level = Qgis.Info if info else Qgis.Warning 64 | QgsMessageLog.logMessage( 65 | message, 66 | name, 67 | level=level, 68 | notifyUser=notify, 69 | ) 70 | 71 | 72 | def config_defaults_catalogs(): 73 | """ Initialize the plugin connections settings with the default 74 | catalogs and set the current connection active. 75 | """ 76 | 77 | for catalog in CATALOGS: 78 | connection_id = uuid.UUID(catalog['id']) 79 | 80 | capability = ApiCapability(catalog["capability"]) \ 81 | if catalog["capability"] else None 82 | if not settings_manager.is_connection( 83 | connection_id 84 | ): 85 | connection_settings = ConnectionSettings( 86 | id=connection_id, 87 | name=catalog['name'], 88 | url=catalog['url'], 89 | page_size=5, 90 | collections=[], 91 | conformances=[], 92 | capability=capability, 93 | created_date=datetime.datetime.now(), 94 | auth_config=None, 95 | sas_subscription_key=None, 96 | search_items=[], 97 | ) 98 | settings_manager.save_connection_settings(connection_settings) 99 | 100 | if catalog['selected']: 101 | settings_manager.set_current_connection(connection_id) 102 | 103 | settings_manager.set_value("default_catalogs_set", True) 104 | 105 | 106 | def open_folder(path): 107 | """ Opens the folder located at the passed path 108 | 109 | :param path: Folder path 110 | :type path: str 111 | 112 | :returns message: Message about whether the operation was 113 | successful or not. 114 | :rtype tuple 115 | """ 116 | if not path: 117 | return False, tr("Path is not set") 118 | 119 | if not os.path.exists(path): 120 | return False, tr('Path do not exist: {}').format(path) 121 | 122 | if not os.access(path, mode=os.R_OK | os.W_OK): 123 | return False, tr('No read or write permission on path: {}').format(path) 124 | 125 | if sys.platform == 'darwin': 126 | subprocess.check_call(['open', path]) 127 | elif sys.platform in ['linux', 'linux1', 'linux2']: 128 | subprocess.check_call(['xdg-open', path]) 129 | elif sys.platform == 'win32': 130 | subprocess.check_call(['explorer', path]) 131 | else: 132 | raise NotImplementedError 133 | 134 | return True, tr("Success") 135 | 136 | 137 | def open_documentation(): 138 | """ Opens documentation website in the default browser""" 139 | QtGui.QDesktopServices.openUrl( 140 | QtCore.QUrl(SITE) 141 | ) 142 | 143 | 144 | def check_gdal_version(): 145 | """ Checks if the installed gdal version matches the 146 | required version by the plugin 147 | """ 148 | gdal_version = gdal.VersionInfo("RELEASE_NAME") 149 | if int(gdal.VersionInfo("VERSION_NUM")) < 1000000: 150 | msg = tr( 151 | "Make sure you are using GDAL >= 1.10 " 152 | "You seem to have gdal {} installed".format(gdal_version)) 153 | log(msg) 154 | 155 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'timlinux' 2 | -------------------------------------------------------------------------------- /test/mock/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/test/mock/__init__.py -------------------------------------------------------------------------------- /test/mock/data/catalog.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Catalog", 3 | "id": "mock_id", 4 | "title": "STAC API QGIS plugin tests", 5 | "description": "Sample description", 6 | "stac_version": "1.0.0", 7 | "conformsTo": [ 8 | "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core", 9 | "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson", 10 | "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30", 11 | "http://www.opengis.net/spec/ogcapi-features-1/1.0/req/oas30", 12 | "https://api.stacspec.org/v1.0.0-beta.3/core", 13 | "https://api.stacspec.org/v1.0.0-beta.3/item-search", 14 | "https://api.stacspec.org/v1.0.0-beta.3/item-search#filter", 15 | "https://api.stacspec.org/v1.0.0-beta.3/item-search#filter:basic-spatial-operators", 16 | "https://api.stacspec.org/v1.0.0-beta.3/item-search#filter:basic-temporal-operators", 17 | "https://api.stacspec.org/v1.0.0-beta.3/item-search#filter:cql-json", 18 | "https://api.stacspec.org/v1.0.0-beta.3/item-search#filter:filter", 19 | "https://api.stacspec.org/v1.0.0-beta.3/item-search#filter:item-search-filter", 20 | "https://api.stacspec.org/v1.0.0-beta.3/item-search/#fields", 21 | "https://api.stacspec.org/v1.0.0-beta.3/item-search/#query", 22 | "https://api.stacspec.org/v1.0.0-beta.3/item-search#sort", 23 | "https://api.stacspec.org/v1.0.0-beta.3/ogcapi-features" 24 | ], 25 | "links": [ 26 | { 27 | "rel": "root", 28 | "type": "application/json", 29 | "href": "http://localhost:5000/" 30 | }, 31 | { 32 | "rel": "data", 33 | "type": "application/json", 34 | "href": "http://localhost:5000/collections" 35 | }, 36 | { 37 | "rel": "docs", 38 | "type": "application/json", 39 | "title": "OpenAPI docs", 40 | "href": "http://localhost:5000/docs" 41 | }, 42 | { 43 | "rel": "conformance", 44 | "type": "application/json", 45 | "title": "STAC/WFS3 conformance classes implemented by this server", 46 | "href": "http://localhost:5000/conformance" 47 | }, 48 | { 49 | "rel": "search", 50 | "type": "application/geo+json", 51 | "title": "STAC search", 52 | "href": "http://localhost:5000/search", 53 | "method": "GET" 54 | }, 55 | { 56 | "rel": "search", 57 | "type": "application/json", 58 | "title": "STAC search", 59 | "href": "http://localhost:5000/search", 60 | "method": "POST" 61 | }, 62 | { 63 | "rel": "child", 64 | "type": "application/json", 65 | "title": "Sample child", 66 | "href": "http://localhost:5000/collections" 67 | }, 68 | { 69 | "rel": "owner", 70 | "type": "application/json", 71 | "title": "Sample Catalog", 72 | "href": "http://localhost:5000/" 73 | } 74 | ], 75 | "stac_extensions": [] 76 | } -------------------------------------------------------------------------------- /test/mock/data/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "simple-collection", 3 | "type": "Collection", 4 | "stac_extensions": [], 5 | "stac_version": "1.0.0", 6 | "description": "A simple collection demonstrating core catalog fields with links to a couple of items", 7 | "title": "Simple Example Collection", 8 | "providers": [ 9 | { 10 | "name": "Provider", 11 | "description": "Descriptions", 12 | "roles": [ 13 | "test" 14 | ], 15 | "url": "http://localhost:5000/" 16 | } 17 | ], 18 | "extent": { 19 | "spatial": { 20 | "bbox": [ 21 | [ 22 | 172.91173669923782, 23 | 1.3438851951615003, 24 | 172.95469614953714, 25 | 1.3690476620161975 26 | ] 27 | ] 28 | }, 29 | "temporal": { 30 | "interval": [ 31 | [ 32 | "2020-12-11T22:38:32.125Z", 33 | "2020-12-14T18:02:31.437Z" 34 | ] 35 | ] 36 | } 37 | }, 38 | "license": "license", 39 | "summaries": { 40 | "platform": [ 41 | "platform1", 42 | "platform2" 43 | ], 44 | "constellation": [ 45 | "test" 46 | ], 47 | "instruments": [ 48 | "test1", 49 | "test2" 50 | ], 51 | "gsd": { 52 | "minimum": 0.512, 53 | "maximum": 0.66 54 | }, 55 | "eo:cloud_cover": { 56 | "minimum": 1.2, 57 | "maximum": 1.2 58 | }, 59 | "proj:epsg": { 60 | "minimum": 32659, 61 | "maximum": 32659 62 | }, 63 | "view:sun_elevation": { 64 | "minimum": 54.9, 65 | "maximum": 54.9 66 | }, 67 | "view:off_nadir": { 68 | "minimum": 3.8, 69 | "maximum": 3.8 70 | }, 71 | "view:sun_azimuth": { 72 | "minimum": 135.7, 73 | "maximum": 135.7 74 | } 75 | }, 76 | "links": [ 77 | { 78 | "rel": "root", 79 | "href": "http://localhost:5000/", 80 | "type": "application/json", 81 | "title": "Example Collection" 82 | }, 83 | { 84 | "rel": "item", 85 | "href": "./first_item.json", 86 | "type": "application/geo+json", 87 | "title": "First Item" 88 | }, 89 | { 90 | "rel": "item", 91 | "href": "./second_item.json", 92 | "type": "application/geo+json", 93 | "title": "Second Item" 94 | }, 95 | { 96 | "rel": "item", 97 | "href": "./third-item.json", 98 | "type": "application/geo+json", 99 | "title": "Third Item" 100 | }, 101 | { 102 | "rel": "self", 103 | "href": "http://localhost:5000/collections/simple-collection", 104 | "type": "application/json" 105 | } 106 | ] 107 | } -------------------------------------------------------------------------------- /test/mock/data/collections.json: -------------------------------------------------------------------------------- 1 | { 2 | "collections": [ 3 | { 4 | "id": "simple-collection", 5 | "type": "Collection", 6 | "stac_extensions": [], 7 | "stac_version": "1.0.0", 8 | "description": "A simple collection demonstrating core catalog fields with links to a couple of items", 9 | "title": "Simple Example Collection", 10 | "providers": [ 11 | { 12 | "name": "Provider", 13 | "description": "Descriptions", 14 | "roles": [ 15 | "test" 16 | ], 17 | "url": "http://test" 18 | } 19 | ], 20 | "extent": { 21 | "spatial": { 22 | "bbox": [ 23 | [ 24 | 172.91173669923782, 25 | 1.3438851951615003, 26 | 172.95469614953714, 27 | 1.3690476620161975 28 | ] 29 | ] 30 | }, 31 | "temporal": { 32 | "interval": [ 33 | [ 34 | "2020-12-11T22:38:32.125Z", 35 | "2020-12-14T18:02:31.437Z" 36 | ] 37 | ] 38 | } 39 | }, 40 | "license": "CC-BY-4.0", 41 | "summaries": { 42 | "platform": [ 43 | "cool_sat1", 44 | "cool_sat2" 45 | ], 46 | "constellation": [ 47 | "ion" 48 | ], 49 | "instruments": [ 50 | "cool_sensor_v1", 51 | "cool_sensor_v2" 52 | ], 53 | "gsd": { 54 | "minimum": 0.512, 55 | "maximum": 0.66 56 | }, 57 | "eo:cloud_cover": { 58 | "minimum": 1.2, 59 | "maximum": 1.2 60 | }, 61 | "proj:epsg": { 62 | "minimum": 32659, 63 | "maximum": 32659 64 | }, 65 | "view:sun_elevation": { 66 | "minimum": 54.9, 67 | "maximum": 54.9 68 | }, 69 | "view:off_nadir": { 70 | "minimum": 3.8, 71 | "maximum": 3.8 72 | }, 73 | "view:sun_azimuth": { 74 | "minimum": 135.7, 75 | "maximum": 135.7 76 | } 77 | }, 78 | "links": [ 79 | { 80 | "rel": "root", 81 | "href": "http://localhost:5000/", 82 | "type": "application/json", 83 | "title": "Example Collection" 84 | }, 85 | { 86 | "rel": "item", 87 | "href": "./first_item.json", 88 | "type": "application/geo+json", 89 | "title": "First Item" 90 | }, 91 | { 92 | "rel": "item", 93 | "href": "./second_item.json", 94 | "type": "application/geo+json", 95 | "title": "Second Item" 96 | }, 97 | { 98 | "rel": "self", 99 | "href": "http://localhost:5000/collections/simple-collection", 100 | "type": "application/json" 101 | } 102 | ] 103 | } 104 | ] 105 | } -------------------------------------------------------------------------------- /test/mock/data/conformance.json: -------------------------------------------------------------------------------- 1 | { 2 | "conformsTo": [ 3 | "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core", 4 | "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson", 5 | "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30", 6 | "http://www.opengis.net/spec/ogcapi-features-1/1.0/req/oas30", 7 | "https://api.stacspec.org/v1.0.0-beta.3/core", 8 | "https://api.stacspec.org/v1.0.0-beta.3/item-search", 9 | "https://api.stacspec.org/v1.0.0-beta.3/item-search#filter", 10 | "https://api.stacspec.org/v1.0.0-beta.3/item-search#filter:basic-spatial-operators", 11 | "https://api.stacspec.org/v1.0.0-beta.3/item-search#filter:basic-temporal-operators", 12 | "https://api.stacspec.org/v1.0.0-beta.3/item-search#filter:cql-json", 13 | "https://api.stacspec.org/v1.0.0-beta.3/item-search#filter:filter", 14 | "https://api.stacspec.org/v1.0.0-beta.3/item-search#filter:item-search-filter", 15 | "https://api.stacspec.org/v1.0.0-beta.3/item-search#fields", 16 | "https://api.stacspec.org/v1.0.0-beta.3/item-search#query", 17 | "https://api.stacspec.org/v1.0.0-beta.3/item-search#sort", 18 | "https://api.stacspec.org/v1.0.0-beta.3/ogcapi-features" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test/mock/data/first_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS1", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "datetime": "2020-12-11T22:38:32.125000Z" 41 | }, 42 | "collection": "simple-collection", 43 | "links": [ 44 | { 45 | "rel": "collection", 46 | "href": "http://localhost:5000/collections/simple-collection", 47 | "type": "application/json", 48 | "title": "Simple Example Collection" 49 | }, 50 | { 51 | "rel": "root", 52 | "href": "http://localhost:5000/collections/simple-collection", 53 | "type": "application/json", 54 | "title": "Simple Example Collection" 55 | }, 56 | { 57 | "rel": "parent", 58 | "href": "http://localhost:5000/collections/simple-collection", 59 | "type": "application/json", 60 | "title": "Simple Example Collection" 61 | } 62 | ], 63 | "assets": { 64 | "visual": { 65 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 66 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 67 | "title": "3-Band Visual", 68 | "roles": [ 69 | "visual" 70 | ] 71 | }, 72 | "thumbnail": { 73 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 74 | "title": "Thumbnail", 75 | "type": "image/jpeg", 76 | "roles": [ 77 | "thumbnail" 78 | ] 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/mock/data/fourth_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS4", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "title": "First Item", 41 | "description": "A sample STAC Item that includes examples of all common metadata", 42 | "datetime": null, 43 | "start_datetime": "2020-12-11T22:38:32.125Z", 44 | "end_datetime": "2020-12-11T22:38:32.327Z", 45 | "created": "2020-12-12T01:48:13.725Z", 46 | "updated": "2020-12-12T01:48:13.725Z", 47 | "platform": "cool_sat1", 48 | "instruments": [ 49 | "test1" 50 | ], 51 | "constellation": "test", 52 | "mission": "collection 5624", 53 | "gsd": 0.512 54 | }, 55 | "collection": "simple-collection", 56 | "links": [ 57 | { 58 | "rel": "collection", 59 | "href": "http://localhost:5000/collections/simple-collection", 60 | "type": "application/json", 61 | "title": "Simple Example Collection" 62 | }, 63 | { 64 | "rel": "root", 65 | "href": "http://localhost:5000/collections/simple-collection", 66 | "type": "application/json", 67 | "title": "Simple Example Collection" 68 | }, 69 | { 70 | "rel": "parent", 71 | "href": "http://localhost:5000/collections/simple-collection", 72 | "type": "application/json", 73 | "title": "Simple Example Collection" 74 | } 75 | ], 76 | "assets": { 77 | "analytic": { 78 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 79 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 80 | "title": "4-Band Analytic", 81 | "roles": [ 82 | "data" 83 | ] 84 | }, 85 | "thumbnail": { 86 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 87 | "title": "Thumbnail", 88 | "type": "image/png", 89 | "roles": [ 90 | "thumbnail" 91 | ] 92 | }, 93 | "visual": { 94 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 95 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 96 | "title": "3-Band Visual", 97 | "roles": [ 98 | "visual" 99 | ] 100 | }, 101 | "udm": { 102 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 103 | "title": "Unusable Data Mask", 104 | "type": "image/tiff; application=geotiff;" 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/mock/data/second_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS2", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "title": "First Item", 41 | "description": "A sample STAC Item that includes examples of all common metadata", 42 | "datetime": null, 43 | "start_datetime": "2020-12-11T22:38:32.125Z", 44 | "end_datetime": "2020-12-11T22:38:32.327Z", 45 | "created": "2020-12-12T01:48:13.725Z", 46 | "updated": "2020-12-12T01:48:13.725Z", 47 | "platform": "cool_sat1", 48 | "instruments": [ 49 | "test1" 50 | ], 51 | "constellation": "test", 52 | "mission": "collection 5624", 53 | "gsd": 0.512 54 | }, 55 | "collection": "simple-collection", 56 | "links": [ 57 | { 58 | "rel": "collection", 59 | "href": "http://localhost:5000/collections/simple-collection", 60 | "type": "application/json", 61 | "title": "Simple Example Collection" 62 | }, 63 | { 64 | "rel": "root", 65 | "href": "http://localhost:5000/collections/simple-collection", 66 | "type": "application/json", 67 | "title": "Simple Example Collection" 68 | }, 69 | { 70 | "rel": "parent", 71 | "href": "http://localhost:5000/collections/simple-collection", 72 | "type": "application/json", 73 | "title": "Simple Example Collection" 74 | } 75 | ], 76 | "assets": { 77 | "analytic": { 78 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 79 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 80 | "title": "4-Band Analytic", 81 | "roles": [ 82 | "data" 83 | ] 84 | }, 85 | "thumbnail": { 86 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 87 | "title": "Thumbnail", 88 | "type": "image/png", 89 | "roles": [ 90 | "thumbnail" 91 | ] 92 | }, 93 | "visual": { 94 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 95 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 96 | "title": "3-Band Visual", 97 | "roles": [ 98 | "visual" 99 | ] 100 | }, 101 | "udm": { 102 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 103 | "title": "Unusable Data Mask", 104 | "type": "image/tiff; application=geotiff;" 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/mock/data/third_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "stac_extensions": [], 4 | "type": "Feature", 5 | "id": "20201211_223832_CS3", 6 | "bbox": [ 7 | 172.91173669923782, 8 | 1.3438851951615003, 9 | 172.95469614953714, 10 | 1.3690476620161975 11 | ], 12 | "geometry": { 13 | "type": "Polygon", 14 | "coordinates": [ 15 | [ 16 | [ 17 | 172.91173669923782, 18 | 1.3438851951615003 19 | ], 20 | [ 21 | 172.95469614953714, 22 | 1.3438851951615003 23 | ], 24 | [ 25 | 172.95469614953714, 26 | 1.3690476620161975 27 | ], 28 | [ 29 | 172.91173669923782, 30 | 1.3690476620161975 31 | ], 32 | [ 33 | 172.91173669923782, 34 | 1.3438851951615003 35 | ] 36 | ] 37 | ] 38 | }, 39 | "properties": { 40 | "title": "First Item", 41 | "description": "A sample STAC Item that includes examples of all common metadata", 42 | "datetime": null, 43 | "start_datetime": "2020-12-11T22:38:32.125Z", 44 | "end_datetime": "2020-12-11T22:38:32.327Z", 45 | "created": "2020-12-12T01:48:13.725Z", 46 | "updated": "2020-12-12T01:48:13.725Z", 47 | "platform": "cool_sat1", 48 | "instruments": [ 49 | "test1" 50 | ], 51 | "constellation": "test", 52 | "mission": "collection 5624", 53 | "gsd": 0.512 54 | }, 55 | "collection": "simple-collection", 56 | "links": [ 57 | { 58 | "rel": "collection", 59 | "href": "http://localhost:5000/collections/simple-collection", 60 | "type": "application/json", 61 | "title": "Simple Example Collection" 62 | }, 63 | { 64 | "rel": "root", 65 | "href": "http://localhost:5000/collections/simple-collection", 66 | "type": "application/json", 67 | "title": "Simple Example Collection" 68 | }, 69 | { 70 | "rel": "parent", 71 | "href": "http://localhost:5000/collections/simple-collection", 72 | "type": "application/json", 73 | "title": "Simple Example Collection" 74 | } 75 | ], 76 | "assets": { 77 | "analytic": { 78 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif", 79 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 80 | "title": "4-Band Analytic", 81 | "roles": [ 82 | "data" 83 | ] 84 | }, 85 | "thumbnail": { 86 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", 87 | "title": "Thumbnail", 88 | "type": "image/png", 89 | "roles": [ 90 | "thumbnail" 91 | ] 92 | }, 93 | "visual": { 94 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.tif", 95 | "type": "image/tiff; application=geotiff; profile=cloud-optimized", 96 | "title": "3-Band Visual", 97 | "roles": [ 98 | "visual" 99 | ] 100 | }, 101 | "udm": { 102 | "href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic_udm.tif", 103 | "title": "Unusable Data Mask", 104 | "type": "image/tiff; application=geotiff;" 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/mock/mock_http_server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | 4 | import multiprocessing 5 | 6 | from flask import jsonify, request 7 | from threading import Thread 8 | 9 | from .stac_api_server_app import app 10 | from .stac_api_auth_server_app import app as auth_app 11 | 12 | 13 | class MockSTACApiServer(Thread): 14 | """ Mock a live """ 15 | def __init__(self, port=5000, auth=False): 16 | super().__init__() 17 | self.port = port 18 | 19 | if not auth: 20 | self.app = app 21 | else: 22 | self.app = auth_app 23 | 24 | self.url = "http://localhost:%s" % self.port 25 | 26 | try: 27 | self.app.add_url_rule("/shutdown", view_func=self._shutdown_server) 28 | except AssertionError as ae: 29 | pass 30 | 31 | def _shutdown_server(self): 32 | if not 'werkzeug.server.shutdown' in request.environ: 33 | raise RuntimeError('Error shutting down server') 34 | request.environ['werkzeug.server.shutdown']() 35 | return 'Shutting down' 36 | 37 | def shutdown_server(self): 38 | requests.get("http://localhost:%s/shutdown" % self.port) 39 | self.join() 40 | 41 | def run(self): 42 | self.app.run(port=self.port) 43 | -------------------------------------------------------------------------------- /test/mock/stac_api_auth_server_app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | from pathlib import Path 5 | 6 | from flask import Flask, jsonify, request 7 | 8 | app = Flask(__name__) 9 | 10 | DATA_PATH = Path(__file__).parent / "data" 11 | 12 | 13 | @app.route("/") 14 | def catalog(): 15 | catalog = DATA_PATH / "catalog.json" 16 | 17 | with catalog.open() as fl: 18 | return json.load(fl) 19 | 20 | 21 | @app.route("/collections") 22 | def collections(): 23 | headers = request.headers 24 | auth = headers.get("APIHeaderKey") 25 | if auth == 'test_api_header_key': 26 | collections = DATA_PATH / "collections.json" 27 | with collections.open() as fl: 28 | return json.load(fl) 29 | else: 30 | return jsonify({"message": "Unauthorized"}), 401 31 | -------------------------------------------------------------------------------- /test/mock/stac_api_server_app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | from pathlib import Path 5 | 6 | from flask import Flask, request 7 | 8 | app = Flask(__name__) 9 | 10 | DATA_PATH = Path(__file__).parent / "data" 11 | 12 | 13 | @app.route("/") 14 | def catalog(): 15 | catalog = DATA_PATH / "catalog.json" 16 | 17 | with catalog.open() as fl: 18 | return json.load(fl) 19 | 20 | 21 | @app.route("/collections") 22 | def collections(): 23 | collections = DATA_PATH / "collections.json" 24 | 25 | with collections.open() as fl: 26 | return json.load(fl) 27 | 28 | 29 | @app.route("/collections/") 30 | def collection(collection_id): 31 | if collection_id == "simple-collection": 32 | collection = DATA_PATH / "collection.json" 33 | 34 | with collection.open() as fl: 35 | return json.load(fl) 36 | 37 | 38 | @app.route("/collections//items", methods=['GET', 'POST']) 39 | def items(collection_id): 40 | items_dict = {} 41 | if collection_id == "simple-collection": 42 | items_dict = { 43 | "type": "FeatureCollection", 44 | "features": [] 45 | } 46 | sort_requested = False 47 | 48 | if request.method == 'POST': 49 | sort_params = request.json.get('sortby') 50 | sort_requested = sort_params is not None and ( 51 | sort_params[0].get('field') == 'id' and 52 | sort_params[0].get('direction') == 'asc' 53 | ) 54 | 55 | if sort_requested: 56 | files = [ 57 | DATA_PATH / "first_item.json", 58 | DATA_PATH / "second_item.json", 59 | DATA_PATH / "third_item.json", 60 | DATA_PATH / "fourth_item.json", 61 | ] 62 | else: 63 | files = [ 64 | DATA_PATH / "third_item.json", 65 | DATA_PATH / "fourth_item.json", 66 | DATA_PATH / "first_item.json", 67 | DATA_PATH / "second_item.json", 68 | ] 69 | 70 | for f in files: 71 | with f.open() as fl: 72 | item = json.load(fl) 73 | items_dict["features"].append(item) 74 | return items_dict 75 | 76 | 77 | @app.route("/search", methods=['GET', 'POST']) 78 | def search(): 79 | sort_requested = False 80 | 81 | if request.method == 'POST': 82 | sort_params = request.json.get('sortby') 83 | sort_requested = sort_params is not None and ( 84 | sort_params[0].get('field') == 'id' and 85 | sort_params[0].get('direction') == 'asc' 86 | ) 87 | if sort_requested: 88 | search_file = DATA_PATH / "search_sorted.json" 89 | else: 90 | search_file = DATA_PATH / "search.json" 91 | 92 | with search_file.open() as fl: 93 | return json.load(fl) 94 | 95 | 96 | @app.route("/conformance", methods=['GET']) 97 | def conformance(): 98 | conformance_file = DATA_PATH / "conformance.json" 99 | 100 | with conformance_file.open() as fl: 101 | return json.load(fl) 102 | -------------------------------------------------------------------------------- /test/qgis_interface.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """QGIS plugin implementation. 3 | 4 | .. note:: This program is free software; you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation; either version 2 of the License, or 7 | (at your option) any later version. 8 | 9 | .. note:: This source code was copied from the 'postgis viewer' application 10 | with original authors: 11 | Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk 12 | Copyright (c) 2011 German Carrillo, geotux_tuxman@linuxmail.org 13 | 14 | """ 15 | 16 | __author__ = 'tim@kartoza.com' 17 | __revision__ = '$Format:%H$' 18 | __date__ = '10/01/2011' 19 | __copyright__ = ( 20 | 'Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk and ' 21 | 'Copyright (c) 2011 German Carrillo, geotux_tuxman@linuxmail.org' 22 | 'Copyright (c) 2014 Tim Sutton, tim@kartoza.com' 23 | ) 24 | 25 | import logging 26 | from qgis.PyQt.QtCore import QObject, pyqtSlot, pyqtSignal 27 | from qgis.core import QgsMapLayerRegistry 28 | from qgis.gui import QgsMapCanvasLayer 29 | LOGGER = logging.getLogger('qgis_stac') 30 | 31 | 32 | #noinspection PyMethodMayBeStatic,PyPep8Naming 33 | class QgisInterface(QObject): 34 | """Class to expose QGIS objects and functions to plugins. 35 | 36 | This class is here for enabling us to run unit tests only, 37 | so most methods are simply stubs. 38 | """ 39 | currentLayerChanged = pyqtSignal(QgsMapCanvasLayer) 40 | 41 | def __init__(self, canvas): 42 | """Constructor 43 | :param canvas: 44 | """ 45 | QObject.__init__(self) 46 | self.canvas = canvas 47 | # Set up slots so we can mimic the behaviour of QGIS when layers 48 | # are added. 49 | LOGGER.debug('Initialising canvas...') 50 | # noinspection PyArgumentList 51 | QgsMapLayerRegistry.instance().layersAdded.connect(self.addLayers) 52 | # noinspection PyArgumentList 53 | QgsMapLayerRegistry.instance().layerWasAdded.connect(self.addLayer) 54 | # noinspection PyArgumentList 55 | QgsMapLayerRegistry.instance().removeAll.connect(self.removeAllLayers) 56 | 57 | # For processing module 58 | self.destCrs = None 59 | 60 | @pyqtSlot('QStringList') 61 | def addLayers(self, layers): 62 | """Handle layers being added to the registry so they show up in canvas. 63 | 64 | :param layers: list list of map layers that were added 65 | 66 | .. note:: The QgsInterface api does not include this method, 67 | it is added here as a helper to facilitate testing. 68 | """ 69 | #LOGGER.debug('addLayers called on qgis_interface') 70 | #LOGGER.debug('Number of layers being added: %s' % len(layers)) 71 | #LOGGER.debug('Layer Count Before: %s' % len(self.canvas.layers())) 72 | current_layers = self.canvas.layers() 73 | final_layers = [] 74 | for layer in current_layers: 75 | final_layers.append(QgsMapCanvasLayer(layer)) 76 | for layer in layers: 77 | final_layers.append(QgsMapCanvasLayer(layer)) 78 | 79 | self.canvas.setLayerSet(final_layers) 80 | #LOGGER.debug('Layer Count After: %s' % len(self.canvas.layers())) 81 | 82 | @pyqtSlot('QgsMapLayer') 83 | def addLayer(self, layer): 84 | """Handle a layer being added to the registry so it shows up in canvas. 85 | 86 | :param layer: list list of map layers that were added 87 | 88 | .. note: The QgsInterface api does not include this method, it is added 89 | here as a helper to facilitate testing. 90 | 91 | .. note: The addLayer method was deprecated in QGIS 1.8 so you should 92 | not need this method much. 93 | """ 94 | pass 95 | 96 | @pyqtSlot() 97 | def removeAllLayers(self): 98 | """Remove layers from the canvas before they get deleted.""" 99 | self.canvas.setLayerSet([]) 100 | 101 | def newProject(self): 102 | """Create new project.""" 103 | # noinspection PyArgumentList 104 | QgsMapLayerRegistry.instance().removeAllMapLayers() 105 | 106 | # ---------------- API Mock for QgsInterface follows ------------------- 107 | 108 | def zoomFull(self): 109 | """Zoom to the map full extent.""" 110 | pass 111 | 112 | def zoomToPrevious(self): 113 | """Zoom to previous view extent.""" 114 | pass 115 | 116 | def zoomToNext(self): 117 | """Zoom to next view extent.""" 118 | pass 119 | 120 | def zoomToActiveLayer(self): 121 | """Zoom to extent of active layer.""" 122 | pass 123 | 124 | def addVectorLayer(self, path, base_name, provider_key): 125 | """Add a vector layer. 126 | 127 | :param path: Path to layer. 128 | :type path: str 129 | 130 | :param base_name: Base name for layer. 131 | :type base_name: str 132 | 133 | :param provider_key: Provider key e.g. 'ogr' 134 | :type provider_key: str 135 | """ 136 | pass 137 | 138 | def addRasterLayer(self, path, base_name): 139 | """Add a raster layer given a raster layer file name 140 | 141 | :param path: Path to layer. 142 | :type path: str 143 | 144 | :param base_name: Base name for layer. 145 | :type base_name: str 146 | """ 147 | pass 148 | 149 | def activeLayer(self): 150 | """Get pointer to the active layer (layer selected in the legend).""" 151 | # noinspection PyArgumentList 152 | layers = QgsMapLayerRegistry.instance().mapLayers() 153 | for item in layers: 154 | return layers[item] 155 | 156 | def addToolBarIcon(self, action): 157 | """Add an icon to the plugins toolbar. 158 | 159 | :param action: Action to add to the toolbar. 160 | :type action: QAction 161 | """ 162 | pass 163 | 164 | def removeToolBarIcon(self, action): 165 | """Remove an action (icon) from the plugin toolbar. 166 | 167 | :param action: Action to add to the toolbar. 168 | :type action: QAction 169 | """ 170 | pass 171 | 172 | def addToolBar(self, name): 173 | """Add toolbar with specified name. 174 | 175 | :param name: Name for the toolbar. 176 | :type name: str 177 | """ 178 | pass 179 | 180 | def mapCanvas(self): 181 | """Return a pointer to the map canvas.""" 182 | return self.canvas 183 | 184 | def mainWindow(self): 185 | """Return a pointer to the main window. 186 | 187 | In case of QGIS it returns an instance of QgisApp. 188 | """ 189 | pass 190 | 191 | def addDockWidget(self, area, dock_widget): 192 | """Add a dock widget to the main window. 193 | 194 | :param area: Where in the ui the dock should be placed. 195 | :type area: 196 | 197 | :param dock_widget: A dock widget to add to the UI. 198 | :type dock_widget: QDockWidget 199 | """ 200 | pass 201 | 202 | def legendInterface(self): 203 | """Get the legend.""" 204 | return self.canvas 205 | -------------------------------------------------------------------------------- /test/tenbytenraster.asc: -------------------------------------------------------------------------------- 1 | NCOLS 10 2 | NROWS 10 3 | XLLCENTER 1535380.000000 4 | YLLCENTER 5083260.000000 5 | DX 10 6 | DY 10 7 | NODATA_VALUE -9999 8 | 0 1 2 3 4 5 6 7 8 9 9 | 0 1 2 3 4 5 6 7 8 9 10 | 0 1 2 3 4 5 6 7 8 9 11 | 0 1 2 3 4 5 6 7 8 9 12 | 0 1 2 3 4 5 6 7 8 9 13 | 0 1 2 3 4 5 6 7 8 9 14 | 0 1 2 3 4 5 6 7 8 9 15 | 0 1 2 3 4 5 6 7 8 9 16 | 0 1 2 3 4 5 6 7 8 9 17 | 0 1 2 3 4 5 6 7 8 9 18 | CRS 19 | NOTES 20 | -------------------------------------------------------------------------------- /test/tenbytenraster.asc.aux.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Point 4 | 5 | 6 | 7 | 9 8 | 4.5 9 | 0 10 | 2.872281323269 11 | 100 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/tenbytenraster.keywords: -------------------------------------------------------------------------------- 1 | title: Tenbytenraster 2 | -------------------------------------------------------------------------------- /test/tenbytenraster.lic: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tim Sutton, Linfiniti Consulting CC 5 | 6 | 7 | 8 | tenbytenraster.asc 9 | 2700044251 10 | Yes 11 | Tim Sutton 12 | Tim Sutton (QGIS Source Tree) 13 | Tim Sutton 14 | This data is publicly available from QGIS Source Tree. The original 15 | file was created and contributed to QGIS by Tim Sutton. 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/tenbytenraster.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /test/tenbytenraster.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 0 26 | 27 | -------------------------------------------------------------------------------- /test/tenbytenraster.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/test/tenbytenraster.tif -------------------------------------------------------------------------------- /test/tenbytenraster.tif.aux.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 5 | 4.6 6 | 0 7 | 2.8705400188815 8 | 100 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/test_init.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Tests QGIS plugin init.""" 3 | 4 | from future import standard_library 5 | standard_library.install_aliases() 6 | __author__ = 'Tim Sutton ' 7 | __revision__ = '$Format:%H$' 8 | __date__ = '17/10/2010' 9 | __license__ = "GPL" 10 | __copyright__ = 'Copyright 2012, Australia Indonesia Facility for ' 11 | __copyright__ += 'Disaster Reduction' 12 | 13 | import os 14 | import unittest 15 | import logging 16 | import configparser 17 | 18 | LOGGER = logging.getLogger('QGIS') 19 | 20 | 21 | class TestInit(unittest.TestCase): 22 | """Test that the plugin init is usable for QGIS. 23 | 24 | Based heavily on the validator class by Alessandro 25 | Passoti available here: 26 | 27 | http://github.com/qgis/qgis-django/blob/master/qgis-app/ 28 | plugins/validator.py 29 | 30 | """ 31 | 32 | def test_read_init(self): 33 | """Test that the plugin __init__ will validate on plugins.qgis.org.""" 34 | 35 | # You should update this list according to the latest in 36 | # https://github.com/qgis/qgis-django/blob/master/qgis-app/ 37 | # plugins/validator.py 38 | 39 | required_metadata = [ 40 | 'name', 41 | 'description', 42 | 'version', 43 | 'qgisMinimumVersion', 44 | 'email', 45 | 'author'] 46 | 47 | file_path = os.path.abspath(os.path.join( 48 | os.path.dirname(__file__), os.pardir, 49 | 'metadata.txt')) 50 | LOGGER.info(file_path) 51 | metadata = [] 52 | parser = configparser.ConfigParser() 53 | parser.optionxform = str 54 | parser.read(file_path) 55 | message = 'Cannot find a section named "general" in %s' % file_path 56 | assert parser.has_section('general'), message 57 | metadata.extend(parser.items('general')) 58 | 59 | for expectation in required_metadata: 60 | message = ('Cannot find metadata "%s" in metadata source (%s).' % ( 61 | expectation, file_path)) 62 | 63 | self.assertIn(expectation, dict(metadata), message) 64 | 65 | if __name__ == '__main__': 66 | unittest.main() 67 | -------------------------------------------------------------------------------- /test/test_qgis_environment.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Tests for QGIS functionality. 3 | 4 | 5 | .. note:: This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | """ 11 | 12 | __author__ = 'tim@kartoza.com' 13 | __date__ = '20/01/2011' 14 | __copyright__ = ('Copyright 2012, Australia Indonesia Facility for ' 15 | 'Disaster Reduction') 16 | 17 | import os 18 | import unittest 19 | from qgis.core import ( 20 | QgsProviderRegistry, 21 | QgsCoordinateReferenceSystem, 22 | QgsRasterLayer) 23 | 24 | from utilities_for_testing import get_qgis_app 25 | QGIS_APP = get_qgis_app() 26 | 27 | 28 | class QGISTest(unittest.TestCase): 29 | """Test the QGIS Environment""" 30 | 31 | def test_qgis_environment(self): 32 | """QGIS environment has the expected providers""" 33 | 34 | r = QgsProviderRegistry.instance() 35 | self.assertIn('gdal', r.providerList()) 36 | self.assertIn('ogr', r.providerList()) 37 | self.assertIn('postgres', r.providerList()) 38 | 39 | def test_projection(self): 40 | """Test that QGIS properly parses a wkt string. 41 | """ 42 | crs = QgsCoordinateReferenceSystem() 43 | wkt = ( 44 | 'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",' 45 | 'SPHEROID["WGS_1984",6378137.0,298.257223563]],' 46 | 'PRIMEM["Greenwich",0.0],UNIT["Degree",' 47 | '0.0174532925199433]]') 48 | crs.createFromWkt(wkt) 49 | auth_id = crs.authid() 50 | expected_auth_id = 'EPSG:4326' 51 | self.assertEqual(auth_id, expected_auth_id) 52 | 53 | # now test for a loaded layer 54 | path = os.path.join(os.path.dirname(__file__), 'tenbytenraster.tif') 55 | title = 'TestRaster' 56 | layer = QgsRasterLayer(path, title) 57 | auth_id = layer.crs().authid() 58 | self.assertEqual(auth_id, expected_auth_id) 59 | 60 | if __name__ == '__main__': 61 | unittest.main() 62 | -------------------------------------------------------------------------------- /test/test_settings_manager.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Tests for the plugin settings manager. 3 | 4 | """ 5 | 6 | import unittest 7 | 8 | import uuid 9 | 10 | 11 | from qgis_stac.conf import settings_manager 12 | from qgis_stac.conf import ConnectionSettings 13 | 14 | 15 | class SettingsManagerTest(unittest.TestCase): 16 | """Test the plugins setting manager""" 17 | 18 | def test_read_write_settings(self): 19 | """Settings manager can store and retrieve settings""" 20 | 21 | self.assertEqual( 22 | settings_manager.get_value( 23 | "non_existing_key", 24 | "none" 25 | ), 26 | "none" 27 | ) 28 | self.assertIsNone( 29 | settings_manager.get_value( 30 | "non_existing_key") 31 | ) 32 | settings_manager.set_value("name", "test") 33 | self.assertEqual( 34 | settings_manager.get_value( 35 | "name" 36 | ), 37 | "test" 38 | ) 39 | settings_manager.remove("name") 40 | self.assertIsNone( 41 | settings_manager.get_value( 42 | "name") 43 | ) 44 | 45 | def test_connection_settings(self): 46 | """Connection settings can be added, edited and removed""" 47 | 48 | self.assertEqual( 49 | settings_manager.get_value( 50 | "connections", 51 | "none"), 52 | "none" 53 | ) 54 | 55 | # Check adding connection settings 56 | connection_id = uuid.uuid4() 57 | connection_name = f"connections/{str(connection_id)}/name" 58 | 59 | connection = ConnectionSettings( 60 | id=connection_id, 61 | name="test_connection", 62 | url="http:://test", 63 | page_size=10, 64 | collections=[], 65 | conformances=[], 66 | capability=None, 67 | sas_subscription_key=None, 68 | search_items=None, 69 | ) 70 | 71 | self.assertIsNone( 72 | settings_manager.get_value( 73 | connection_name 74 | ) 75 | ) 76 | 77 | settings_manager.save_connection_settings(connection) 78 | 79 | self.assertIsNotNone( 80 | settings_manager.get_value( 81 | connection_name) 82 | ) 83 | 84 | stored_connection = \ 85 | settings_manager.get_connection_settings( 86 | connection_id 87 | ) 88 | self.assertEqual( 89 | connection.auth_config, 90 | stored_connection.auth_config 91 | ) 92 | 93 | self.assertEqual( 94 | connection.created_date.replace(second=0, microsecond=0), 95 | stored_connection.created_date.replace(second=0, microsecond=0) 96 | ) 97 | 98 | # Adding a second connection, setting it a current selected 99 | # connection and checking if changes are in effect. 100 | second_connection_id = uuid.uuid4() 101 | second_connection = ConnectionSettings( 102 | id=second_connection_id, 103 | name="second_test_connection", 104 | url="http:://second_test", 105 | page_size=10, 106 | collections=[], 107 | conformances=[], 108 | capability=None, 109 | sas_subscription_key=None, 110 | search_items=None, 111 | ) 112 | settings_manager.save_connection_settings( 113 | second_connection 114 | ) 115 | second_stored_connection = \ 116 | settings_manager.get_connection_settings( 117 | second_connection_id 118 | ) 119 | self.assertEqual( 120 | second_connection.auth_config, 121 | second_stored_connection.auth_config 122 | ) 123 | 124 | self.assertEqual( 125 | second_connection.created_date.replace(second=0, microsecond=0), 126 | second_stored_connection.created_date.replace(second=0, microsecond=0) 127 | ) 128 | settings_manager.set_current_connection( 129 | second_connection_id 130 | ) 131 | current_connection = settings_manager.get_current_connection() 132 | 133 | self.assertTrue(settings_manager.is_current_connection( 134 | second_connection_id 135 | )) 136 | 137 | self.assertEqual( 138 | second_connection.auth_config, 139 | current_connection.auth_config 140 | ) 141 | 142 | self.assertEqual( 143 | second_connection.created_date.replace(second=0, microsecond=0), 144 | current_connection.created_date.replace(second=0, microsecond=0) 145 | ) 146 | 147 | # Retrieve all the connections 148 | 149 | connections = settings_manager.list_connections() 150 | self.assertEqual(len(connections), 2) 151 | 152 | # Clear current connection 153 | settings_manager.clear_current_connection() 154 | current_connection = settings_manager.get_current_connection() 155 | self.assertIsNone(current_connection) 156 | 157 | # Remove connections 158 | settings_manager.delete_connection(second_connection_id) 159 | connections = settings_manager.list_connections() 160 | self.assertEqual(len(connections), 1) 161 | -------------------------------------------------------------------------------- /test/test_stac_api_client_auth.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Tests for the plugin STAC API client. 3 | 4 | """ 5 | import unittest 6 | import logging 7 | 8 | from multiprocessing import Process 9 | 10 | from mock.mock_http_server import MockSTACApiServer 11 | from qgis.PyQt.QtTest import QSignalSpy 12 | 13 | from qgis_stac.api.client import Client 14 | from qgis.core import QgsApplication 15 | from qgis.core import QgsAuthMethodConfig 16 | 17 | 18 | class STACApiClientAuthTest(unittest.TestCase): 19 | 20 | def setUp(self): 21 | self.app_server = MockSTACApiServer(auth=True) 22 | 23 | self.server = Process(target=self.app_server.run) 24 | self.server.start() 25 | 26 | self.api_client = Client(self.app_server.url) 27 | self.response = None 28 | self.error = None 29 | 30 | def set_auth_method( 31 | self, 32 | config_name, 33 | config_method, 34 | config_map 35 | ): 36 | AUTHDB_MASTERPWD = "password" 37 | 38 | auth_manager = QgsApplication.authManager() 39 | if not auth_manager.masterPasswordHashInDatabase(): 40 | auth_manager.setMasterPassword(AUTHDB_MASTERPWD, True) 41 | # Create config 42 | auth_manager.authenticationDatabasePath() 43 | auth_manager.masterPasswordIsSet() 44 | 45 | cfg = QgsAuthMethodConfig() 46 | cfg.setName(config_name) 47 | cfg.setMethod(config_method) 48 | cfg.setConfigMap(config_map) 49 | auth_manager.storeAuthenticationConfig(cfg) 50 | 51 | return cfg.id() 52 | 53 | def test_auth_collections_fetch(self): 54 | # check if auth works correctly 55 | cfg_id = self.set_auth_method( 56 | "STAC_API_AUTH_TEST", 57 | "APIHeader", 58 | {"APIHeaderKey": "test_api_header_key"} 59 | ) 60 | 61 | api_client = Client( 62 | self.app_server.url, 63 | auth_config=cfg_id 64 | ) 65 | 66 | spy = QSignalSpy(api_client.collections_received) 67 | api_client.collections_received.connect(self.app_response) 68 | api_client.error_received.connect(self.error_response) 69 | 70 | api_client.get_collections() 71 | result = spy.wait(timeout=1000) 72 | 73 | self.assertTrue(result) 74 | self.assertIsNotNone(self.response) 75 | self.assertIsNone(self.error) 76 | self.assertEqual(len(self.response), 2) 77 | 78 | cfg_id = self.set_auth_method( 79 | "STAC_API_AUTH_TEST", 80 | "APIHeader", 81 | {"APIHeaderKey": "unauthorized_api_header_key"} 82 | ) 83 | 84 | api_client = Client( 85 | self.app_server.url, 86 | auth_config=cfg_id 87 | ) 88 | 89 | spy = QSignalSpy(api_client.collections_received) 90 | api_client.collections_received.connect(self.app_response) 91 | api_client.error_received.connect(self.error_response) 92 | 93 | api_client.get_collections() 94 | result = spy.wait(timeout=1000) 95 | 96 | self.assertFalse(result) 97 | self.assertIsNotNone(self.error) 98 | 99 | def app_response(self, *response_args): 100 | self.response = response_args 101 | 102 | def error_response(self, *response_args): 103 | self.error = response_args 104 | 105 | def tearDown(self): 106 | self.server.terminate() 107 | self.server.join() 108 | 109 | 110 | if __name__ == '__main__': 111 | unittest.main() 112 | -------------------------------------------------------------------------------- /test/test_stac_api_client_functions.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Tests for the plugin STAC API client. 3 | 4 | """ 5 | import unittest 6 | import logging 7 | 8 | from multiprocessing import Process 9 | 10 | from mock.mock_http_server import MockSTACApiServer 11 | from qgis.PyQt.QtTest import QSignalSpy 12 | 13 | from qgis_stac.api.client import Client 14 | from qgis_stac.api.models import ItemSearch, SortField, SortOrder 15 | 16 | 17 | class STACApiClientTest(unittest.TestCase): 18 | 19 | def setUp(self): 20 | 21 | self.app_server = MockSTACApiServer() 22 | 23 | self.server = Process(target=self.app_server.run) 24 | self.server.start() 25 | 26 | self.api_client = Client(self.app_server.url) 27 | self.response = None 28 | self.error = None 29 | 30 | def test_resources_fetch(self): 31 | # check items searching 32 | spy = QSignalSpy(self.api_client.items_received) 33 | self.api_client.items_received.connect(self.app_response) 34 | self.api_client.get_items(ItemSearch(collections=['simple-collection'])) 35 | result = spy.wait(timeout=1000) 36 | 37 | self.assertTrue(result) 38 | self.assertIsNotNone(self.response) 39 | self.assertEqual(len(self.response), 2) 40 | 41 | items = self.response[0] 42 | 43 | self.assertEqual(len(items), 4) 44 | self.assertEqual(items[0].id, "20201211_223832_CS3") 45 | self.assertEqual(items[1].id, "20201211_223832_CS4") 46 | 47 | def test_items_sort(self): 48 | # check items searching with sorting enabled 49 | spy = QSignalSpy(self.api_client.items_received) 50 | self.api_client.items_received.connect(self.app_response) 51 | 52 | self.api_client.get_items( 53 | ItemSearch( 54 | collections=['simple-collection'], 55 | sortby=SortField.ID, 56 | sort_order=SortOrder.ASCENDING 57 | ) 58 | ) 59 | result = spy.wait(timeout=1000) 60 | 61 | self.assertTrue(result) 62 | self.assertIsNotNone(self.response) 63 | self.assertEqual(len(self.response), 2) 64 | items = self.response[0] 65 | 66 | self.assertEqual(len(items), 4) 67 | self.assertEqual(items[0].id, "20201211_223832_CS1") 68 | self.assertEqual(items[1].id, "20201211_223832_CS2") 69 | 70 | def test_collections_search(self): 71 | 72 | api_client = Client(self.app_server.url) 73 | spy = QSignalSpy(api_client.collections_received) 74 | api_client.collections_received.connect(self.app_response) 75 | api_client.get_collections() 76 | result = spy.wait(timeout=1000) 77 | 78 | self.assertTrue(result) 79 | self.assertIsNotNone(self.response) 80 | self.assertEqual(len(self.response), 2) 81 | collections = self.response[0] 82 | 83 | self.assertEqual(len(collections), 1) 84 | self.assertEqual(collections[0].id, "simple-collection") 85 | self.assertEqual(collections[0].title, "Simple Example Collection") 86 | 87 | def test_conformance_search(self): 88 | # check conformance fetching 89 | spy = QSignalSpy(self.api_client.conformance_received) 90 | self.api_client.conformance_received.connect(self.app_response) 91 | self.api_client.get_conformance() 92 | result = spy.wait(timeout=1000) 93 | 94 | self.assertTrue(result) 95 | self.assertIsNotNone(self.response) 96 | self.assertEqual(len(self.response), 2) 97 | 98 | conformance_classes = self.response[0] 99 | 100 | self.assertEqual(len(conformance_classes), 16) 101 | self.assertEqual(conformance_classes[0].name, 'core') 102 | self.assertEqual( 103 | conformance_classes[0].uri, 104 | "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core" 105 | ) 106 | 107 | def app_response(self, *response_args): 108 | self.response = response_args 109 | 110 | def error_response(self, *response_args): 111 | self.error = response_args 112 | logging(0, self.error) 113 | 114 | def tearDown(self): 115 | self.server.terminate() 116 | self.server.join() 117 | 118 | 119 | if __name__ == '__main__': 120 | unittest.main() 121 | 122 | -------------------------------------------------------------------------------- /test/test_translations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stac-utils/qgis-stac-plugin/f07f89d452f81cbdf3b68d293908d338997f7547/test/test_translations.py -------------------------------------------------------------------------------- /test/utilities_for_testing.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Common functionality used by regression tests.""" 3 | 4 | 5 | import sys 6 | import logging 7 | 8 | 9 | LOGGER = logging.getLogger('QGIS') 10 | QGIS_APP = None # Static variable used to hold hand to running QGIS app 11 | CANVAS = None 12 | PARENT = None 13 | IFACE = None 14 | 15 | 16 | def get_qgis_app(): 17 | """ Start one QGIS application to test against. 18 | 19 | :returns: Handle to QGIS app, canvas, iface and parent. If there are any 20 | errors the tuple members will be returned as None. 21 | :rtype: (QgsApplication, CANVAS, IFACE, PARENT) 22 | 23 | If QGIS is already running the handle to that app will be returned. 24 | """ 25 | 26 | try: 27 | from qgis.PyQt import QtGui, QtCore 28 | from qgis.core import QgsApplication 29 | from qgis.gui import QgsMapCanvas 30 | from .qgis_interface import QgisInterface 31 | except ImportError: 32 | return None, None, None, None 33 | 34 | global QGIS_APP # pylint: disable=W0603 35 | 36 | if QGIS_APP is None: 37 | gui_flag = True # All test will run qgis in gui mode 38 | #noinspection PyPep8Naming 39 | QGIS_APP = QgsApplication(sys.argv, gui_flag) 40 | # Make sure QGIS_PREFIX_PATH is set in your env if needed! 41 | QGIS_APP.initQgis() 42 | s = QGIS_APP.showSettings() 43 | LOGGER.debug(s) 44 | 45 | global PARENT # pylint: disable=W0603 46 | if PARENT is None: 47 | #noinspection PyPep8Naming 48 | PARENT = QtGui.QWidget() 49 | 50 | global CANVAS # pylint: disable=W0603 51 | if CANVAS is None: 52 | #noinspection PyPep8Naming 53 | CANVAS = QgsMapCanvas(PARENT) 54 | CANVAS.resize(QtCore.QSize(400, 400)) 55 | 56 | global IFACE # pylint: disable=W0603 57 | if IFACE is None: 58 | # QgisInterface is a stub implementation of the QGIS plugin interface 59 | #noinspection PyPep8Naming 60 | IFACE = QgisInterface(CANVAS) 61 | 62 | return QGIS_APP, CANVAS, IFACE, PARENT 63 | -------------------------------------------------------------------------------- /test_suite.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import unittest 4 | import qgis # NOQA For SIP API to V2 if run outside of QGIS 5 | 6 | try: 7 | from pip import main as pipmain 8 | except ImportError: 9 | from pip._internal import main as pipmain 10 | 11 | try: 12 | import coverage 13 | except ImportError: 14 | pipmain(['install', 'coverage']) 15 | import coverage 16 | import tempfile 17 | from osgeo import gdal 18 | from qgis.PyQt import Qt 19 | 20 | from qgis.core import Qgis 21 | 22 | 23 | def _run_tests(test_suite, package_name, with_coverage=False): 24 | """Core function to test a test suite.""" 25 | count = test_suite.countTestCases() 26 | 27 | version = str(Qgis.QGIS_VERSION_INT) 28 | version = int(version) 29 | 30 | print('########') 31 | print('%s tests has been discovered in %s' % (count, package_name)) 32 | print('QGIS : %s' % version) 33 | print('Python GDAL : %s' % gdal.VersionInfo('VERSION_NUM')) 34 | print('QT : %s' % Qt.QT_VERSION_STR) 35 | print('Run slow tests : %s' % (not os.environ.get('ON_TRAVIS', False))) 36 | print('########') 37 | if with_coverage: 38 | cov = coverage.Coverage( 39 | source=['./'], 40 | omit=['*/test/*', './definitions/*'], 41 | ) 42 | cov.start() 43 | 44 | unittest.TextTestRunner(verbosity=3, stream=sys.stdout).run(test_suite) 45 | 46 | if with_coverage: 47 | cov.stop() 48 | cov.save() 49 | report = tempfile.NamedTemporaryFile(delete=False) 50 | cov.report(file=report) 51 | # Produce HTML reports in the `htmlcov` folder and open index.html 52 | # cov.html_report() 53 | report.close() 54 | with open(report.name, 'r') as fin: 55 | print(fin.read()) 56 | 57 | 58 | def test_package(package='test'): 59 | """Test package. 60 | This function is called by Github actions or travis without arguments. 61 | :param package: The package to test. 62 | :type package: str 63 | """ 64 | test_loader = unittest.defaultTestLoader 65 | try: 66 | test_suite = test_loader.discover(package) 67 | except ImportError: 68 | test_suite = unittest.TestSuite() 69 | _run_tests(test_suite, package) 70 | --------------------------------------------------------------------------------