├── .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 | 
4 | 
5 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
102 |
103 | _Available filters_
104 |
105 |
106 |
107 | 
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 | 
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 | 
131 |
132 | _Assets dialog_
133 |
134 |
135 |
136 | 
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 | 
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 |
--------------------------------------------------------------------------------