From 305ed6955835183ee8a4307ae0ea9b0ef8f05fa9 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Sat, 26 Feb 2022 08:31:50 -0500 Subject: [PATCH 01/60] Fixes to CHANGELOG --- CHANGELOG.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27a888dc2..e37491a39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,12 +20,11 @@ - Optional `StacIO` instance as argument to `Catalog.save`/`Catalog.normalize_and_save` ([#751](https://github.com/stac-utils/pystac/pull/751)) - More thorough docstrings for `pystac.utils` functions and classes ([#735](https://github.com/stac-utils/pystac/pull/735)) -### Removed - ### Changed - Label Extension version updated to `v1.0.1` ([#726](https://github.com/stac-utils/pystac/pull/726)) -- Option to filter by `media_type` in `get_links` and `get_single_link` [#704](https://github.com/stac-utils/pystac/pull/704)) +- Option to filter by `media_type` in `STACObject.get_links` and `STACObject.get_single_link` + ([#704](https://github.com/stac-utils/pystac/pull/704)) ### Fixed @@ -35,8 +34,6 @@ with no owner ([#746](https://github.com/stac-utils/pystac/pull/746)) - Type errors when initializing `TemporalExtent` using a list of `datetime` objects ([#744](https://github.com/stac-utils/pystac/pull/744)) -### Deprecated - ## [v1.3.0] ### Added From 0692ffeaa870df17fa9f43bc5b15ddbbf98362d7 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 3 Mar 2022 15:54:21 -0500 Subject: [PATCH 02/60] Add conda badge to main README (#755) * Add conda badge to main README * Link to package on conda registry * Remove benchmark files --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 66e5929a3..50d23b0de 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ ## PySTAC ![Build Status](https://github.com/stac-utils/pystac/workflows/CI/badge.svg?branch=main) [![PyPI version](https://badge.fury.io/py/pystac.svg)](https://badge.fury.io/py/pystac) +[![Conda (channel only)](https://img.shields.io/conda/vn/conda-forge/pystac)](https://anaconda.org/conda-forge/pystac) [![Documentation](https://readthedocs.org/projects/pystac/badge/?version=latest)](https://pystac.readthedocs.io/en/latest/) [![codecov](https://codecov.io/gh/stac-utils/pystac/branch/main/graph/badge.svg)](https://codecov.io/gh/stac-utils/pystac) [![Gitter](https://badges.gitter.im/SpatioTemporal-Asset-Catalog/python.svg)](https://gitter.im/SpatioTemporal-Asset-Catalog/python?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) From ea678f095684e61f5dd906099b36d6808a38ac4b Mon Sep 17 00:00:00 2001 From: Kendall Smith Date: Fri, 4 Mar 2022 09:13:15 -0700 Subject: [PATCH 03/60] Adde enum MediaType for PDF files (#758) * added pdf enum type * updated CHANGELOG.md Co-authored-by: Jon Duckworth --- CHANGELOG.md | 2 ++ pystac/media_type.py | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e37491a39..18e2bc8d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added +- Enum MediaType entry for PDF documents ([#758](https://github.com/stac-utils/pystac/pull/758)) + ### Removed ### Changed diff --git a/pystac/media_type.py b/pystac/media_type.py index 57c47e256..33f4d316a 100644 --- a/pystac/media_type.py +++ b/pystac/media_type.py @@ -17,3 +17,4 @@ class MediaType(StringEnum): TEXT = "text/plain" TIFF = "image/tiff" XML = "application/xml" + PDF = "application/pdf" From d70dea5c70a243450ac64120e0eba84d786574b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Mar 2022 11:53:19 -0500 Subject: [PATCH 04/60] build(deps): bump actions/checkout from 2 to 3 (#759) Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/continuous-integration.yml | 8 ++++---- .github/workflows/release.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 6d4dc472c..816d34563 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -43,7 +43,7 @@ jobs: experimental: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 @@ -100,7 +100,7 @@ jobs: name: coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.8 uses: actions/setup-python@v2 @@ -157,7 +157,7 @@ jobs: - "3.11.0-alpha.3" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 @@ -182,7 +182,7 @@ jobs: vanilla: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-python@v2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e3a904424..d964f8fa9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest if: ${{ github.repository }} == 'stac-utils/pystac' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.x uses: actions/setup-python@v2 From 15a31fadac7cda1aec2087a5b06171988267f7c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 10:20:45 -0400 Subject: [PATCH 05/60] build(deps): bump ipython from 8.0.1 to 8.1.1 (#761) Bumps [ipython](https://github.com/ipython/ipython) from 8.0.1 to 8.1.1. - [Release notes](https://github.com/ipython/ipython/releases) - [Commits](https://github.com/ipython/ipython/compare/8.0.1...8.1.1) --- updated-dependencies: - dependency-name: ipython dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 52ae5c0d0..579fd1841 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,4 +1,4 @@ -ipython==8.0.1 +ipython==8.1.1 Sphinx==4.4.0 sphinxcontrib-fulltoc==1.2.0 nbsphinx==0.8.8 From bcdf2e7db771866ef0f095979e65ce38025b22f8 Mon Sep 17 00:00:00 2001 From: Kendall Smith Date: Thu, 17 Mar 2022 09:31:34 -0700 Subject: [PATCH 06/60] update stac_io from owner root (#762) * update stac_io from owner root * updating CHANGELOG * Update pystac/link.py Adding @lossyrob suggestion to avoid root link from resolving when it is the root. Co-authored-by: Rob Emanuele Co-authored-by: Rob Emanuele Co-authored-by: Jon Duckworth --- CHANGELOG.md | 1 + pystac/link.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18e2bc8d6..5b1ac483c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - Enum MediaType entry for PDF documents ([#758](https://github.com/stac-utils/pystac/pull/758)) +- Updated Link to obtain stac_io from owner root ([#762](https://github.com/stac-utils/pystac/pull/762)) ### Removed diff --git a/pystac/link.py b/pystac/link.py index e5e36d40e..9035848b7 100644 --- a/pystac/link.py +++ b/pystac/link.py @@ -298,6 +298,10 @@ def resolve_stac_object(self, root: Optional["Catalog_Type"] = None) -> "Link": if self.owner is not None: if isinstance(self.owner, pystac.Catalog): stac_io = self.owner._stac_io + elif self.rel != pystac.RelType.ROOT: + owner_root = self.owner.get_root() + if owner_root is not None: + stac_io = owner_root._stac_io if stac_io is None: stac_io = pystac.StacIO.default() From a48e2dc52907a416893a72100230a1d6c0087ac5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Mar 2022 12:41:32 -0400 Subject: [PATCH 07/60] build(deps): bump mypy from 0.931 to 0.940 (#765) * build(deps): bump mypy from 0.931 to 0.940 Bumps [mypy](https://github.com/python/mypy) from 0.931 to 0.940. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.931...v0.940) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Update Python 3.11. alpha version and add note on failed install * Install tomli before orjson * Revert "Install tomli before orjson" This reverts commit c6ed34f4b6f3f1ed307384ae488f9bc62dfe5dc1. * Revert "Update Python 3.11. alpha version and add note on failed install" This reverts commit 921364eccf0eb176357d272f492a4c8ef615d771. * Add note on failing orjson install in Python 3.11 Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jon Duckworth --- README.md | 4 +++- requirements-test.txt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 50d23b0de..07aa7291e 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,9 @@ Note that while we support Python 3.10.\*, wheels for the `orjson` library are n platforms. If you install PySTAC with the `orjson` extra, you may need to have the Rust toolchain installed (e.g. via [rustup](https://rustup.rs/)) in order to build the package from source. -Support for Python 3.11 should be considered experimental until further notice. +Support for Python 3.11 should be considered experimental until further notice. There is a known issue with failing build of `orjson` on 3.11.0 alpha releases prior to alpha.6 (see +[#765(comment)](https://github.com/stac-utils/pystac/pull/765#pullrequestreview-908908772) for +some additional detail). PySTAC has a single required dependency (`python-dateutil`). PySTAC can be installed from pip or the source repository. diff --git a/requirements-test.txt b/requirements-test.txt index d6d16c626..8b453f29c 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -mypy==0.931 +mypy==0.940 flake8==4.0.1 black==22.1.0 From 9d2eb02729fb6243a4c18605b2e5eeb94df421e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Mar 2022 12:50:12 -0400 Subject: [PATCH 08/60] build(deps): bump actions/setup-python from 2 to 3 (#756) * build(deps): bump actions/setup-python from 2 to 3 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 3. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Use setup-python built-in dependency caching Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jon Duckworth --- .github/workflows/continuous-integration.yml | 52 ++++---------------- .github/workflows/release.yml | 2 +- 2 files changed, 11 insertions(+), 43 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 816d34563..58d936544 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -46,9 +46,11 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: requirements-test.txt - uses: actions-rs/toolchain@v1 # Wheels for orjson are not available for Python 3.11. This sets up the Rust @@ -60,30 +62,6 @@ jobs: default: true profile: minimal - - name: Cache dependencies (Linux) - if: startsWith(runner.os, 'Linux') - uses: actions/cache@v2 - with: - path: ~/.cache/pip - # Cache based on OS, Python version, and dependency hash - key: pip-test-${{ runner.os }}-python${{ matrix.python-version }}-${{ hashFiles('requirements-test.txt') }} - - - name: Cache dependencies (macOS) - if: startsWith(runner.os, 'macOS') - uses: actions/cache@v2 - with: - path: ~/Library/Caches/pip - # Cache based on OS, Python version, and dependency hash - key: pip-test-${{ runner.os }}-python${{ matrix.python-version }}-${{ hashFiles('requirements-test.txt') }} - - - name: Cache dependencies (Windows) - if: startsWith(runner.os, 'Windows') - uses: actions/cache@v2 - with: - path: ~\AppData\Local\pip\Cache - # Cache based on OS, Python version, and dependency hash - key: pip-test-${{ runner.os }}-python${{ matrix.python-version }}-${{ hashFiles('requirements-test.txt') }} - - name: Install dependencies run: | pip install --upgrade pip @@ -103,16 +81,11 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python 3.8 - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: "3.8" - - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ~/.cache/pip - # Cache based on OS, Python version, and dependency hash - key: pip-test-${{ runner.os }}-python3.8-${{ hashFiles('requirements-test.txt') }} + cache: 'pip' + cache-dependency-path: requirements-test.txt - name: Install dependencies run: | @@ -160,16 +133,11 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ~/.cache/pip - # Cache based on OS, Python version, and dependency hash - key: lint-${{ runner.os }}-python${{ matrix.python-version }}-${{ hashFiles('requirements-test.txt') }} + cache: 'pip' + cache-dependency-path: requirements-test.txt - name: Install dependencies run: | @@ -184,7 +152,7 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v3 with: python-version: "3.8" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d964f8fa9..946250af3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python 3.x - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: "3.x" From 407a3c61f9b0ec7d7e4881edad362441c64f199e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Mar 2022 15:04:38 -0400 Subject: [PATCH 09/60] build(deps): bump mypy from 0.940 to 0.941 (#767) Bumps [mypy](https://github.com/python/mypy) from 0.940 to 0.941. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.940...v0.941) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 8b453f29c..2ae98e13c 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -mypy==0.940 +mypy==0.941 flake8==4.0.1 black==22.1.0 From a19365b4f3431b2b4de27d6a853bf135e9270007 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Mar 2022 15:13:32 -0400 Subject: [PATCH 10/60] build(deps): bump types-python-dateutil from 2.8.9 to 2.8.10 (#766) Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.9 to 2.8.10. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-python-dateutil dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jon Duckworth --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 2ae98e13c..e99fab492 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -8,7 +8,7 @@ jsonschema==4.4.0 coverage==6.3.2 doc8==0.10.1 -types-python-dateutil==2.8.9 +types-python-dateutil==2.8.10 types-orjson==3.6.2 pre-commit==2.17.0 From c44c79c3c45999d8a3a29599b558983ecdf83d69 Mon Sep 17 00:00:00 2001 From: Alex G Rice Date: Wed, 30 Mar 2022 07:47:23 -0600 Subject: [PATCH 11/60] Fix/576 AssetDefinition missing create, apply (#768) * Add missing create, apply to AssetDefinition. * Fix docstring references to old commonmark version. * Add missing property in unit test for EO Band. * Changelog * Changelog. --- CHANGELOG.md | 1 + pystac/catalog.py | 2 +- pystac/collection.py | 2 +- pystac/extensions/item_assets.py | 62 +++++++++++++++++++++++++++- tests/extensions/test_eo.py | 3 +- tests/extensions/test_item_assets.py | 13 ++++++ 6 files changed, 78 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b1ac483c..1e4babb6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Enum MediaType entry for PDF documents ([#758](https://github.com/stac-utils/pystac/pull/758)) - Updated Link to obtain stac_io from owner root ([#762](https://github.com/stac-utils/pystac/pull/762)) +- Updated AssetDefinition to have create, apply methods ([#768](https://github.com/stac-utils/pystac/pull/768)) ### Removed diff --git a/pystac/catalog.py b/pystac/catalog.py index ce13e5e67..95f268df7 100644 --- a/pystac/catalog.py +++ b/pystac/catalog.py @@ -115,7 +115,7 @@ class Catalog(STACObject): Args: id : Identifier for the catalog. Must be unique within the STAC. description : Detailed multi-line description to fully explain the catalog. - `CommonMark 0.28 syntax `_ MAY be used for rich + `CommonMark 0.29 syntax `_ MAY be used for rich text representation. title : Optional short descriptive one-line title for the catalog. stac_extensions : Optional list of extensions the Catalog implements. diff --git a/pystac/collection.py b/pystac/collection.py index 86d4ccd54..af99a4320 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -421,7 +421,7 @@ class Collection(Catalog): Args: id : Identifier for the collection. Must be unique within the STAC. description : Detailed multi-line description to fully explain the - collection. `CommonMark 0.28 syntax `_ MAY + collection. `CommonMark 0.29 syntax `_ MAY be used for rich text representation. extent : Spatial and temporal extents that describe the bounds of all items contained within this Collection. diff --git a/pystac/extensions/item_assets.py b/pystac/extensions/item_assets.py index c92b1282f..38caae9a0 100644 --- a/pystac/extensions/item_assets.py +++ b/pystac/extensions/item_assets.py @@ -36,6 +36,64 @@ def __eq__(self, o: object) -> bool: return NotImplemented return self.to_dict() == o.to_dict() + @classmethod + def create( + cls, + title: Optional[str], + description: Optional[str], + media_type: Optional[str], + roles: Optional[List[str]], + ) -> "AssetDefinition": + """ + Creates a new asset definition. + + Args: + title : Displayed title for clients and users. + description : Description of the Asset providing additional details, + such as how it was processed or created. + `CommonMark 0.29 `__ syntax MAY be used + for rich text representation. + media_type : `media type + `__ + of the asset. + roles : `semantic roles + `__ + of the asset, similar to the use of rel in links. + """ + asset_defn = cls({}) + asset_defn.apply( + title=title, description=description, media_type=media_type, roles=roles + ) + return asset_defn + + def apply( + self, + title: Optional[str], + description: Optional[str], + media_type: Optional[str], + roles: Optional[List[str]], + ) -> None: + """ + Sets the properties for this asset definition. + + Args: + title : Displayed title for clients and users. + description : Description of the Asset providing additional details, + such as how it was processed or created. + `CommonMark 0.29 `__ syntax MAY be used + for rich text representation. + media_type : `media type + `__ + of the asset. + roles : `semantic roles + `__ + of the asset, similar to the use of rel in links. + """ + self.title = title + self.description = description + self.media_type = media_type + self.roles = roles + @property def title(self) -> Optional[str]: """Gets or sets the displayed title for clients and users.""" @@ -65,7 +123,7 @@ def description(self, v: Optional[str]) -> None: @property def media_type(self) -> Optional[str]: """Gets or sets the `media type - `__ + `__ of the asset.""" return self.properties.get(ASSET_TYPE_PROP) @@ -79,7 +137,7 @@ def media_type(self, v: Optional[str]) -> None: @property def roles(self) -> Optional[List[str]]: """Gets or sets the `semantic roles - `__ + `__ of the asset, similar to the use of rel in links.""" return self.properties.get(ASSET_ROLES_PROP) diff --git a/tests/extensions/test_eo.py b/tests/extensions/test_eo.py index be879f9f3..656fb3e55 100644 --- a/tests/extensions/test_eo.py +++ b/tests/extensions/test_eo.py @@ -21,6 +21,7 @@ def test_create(self) -> None: description=Band.band_description("red"), center_wavelength=0.65, full_width_half_max=0.1, + solar_illumination=42.0, ) self.assertEqual(band.name, "B01") @@ -28,7 +29,7 @@ def test_create(self) -> None: self.assertEqual(band.description, "Common name: red, Range: 0.6 to 0.7") self.assertEqual(band.center_wavelength, 0.65) self.assertEqual(band.full_width_half_max, 0.1) - + self.assertEqual(band.solar_illumination, 42.0) self.assertEqual(band.__repr__(), "") def test_band_description_unknown_band(self) -> None: diff --git a/tests/extensions/test_item_assets.py b/tests/extensions/test_item_assets.py index 67267d075..28e243d0c 100644 --- a/tests/extensions/test_item_assets.py +++ b/tests/extensions/test_item_assets.py @@ -46,6 +46,19 @@ def setUp(self) -> None: TestCases.get_path("data-files/item-assets/example-landsat8.json") ) + def test_create(self) -> None: + title = "Coastal Band (B1)" + description = "Coastal Band Top Of the Atmosphere" + media_type = "image/tiff; application=geotiff" + roles = ["data"] + asset_defn = AssetDefinition.create( + title=title, description=description, media_type=media_type, roles=roles + ) + self.assertEqual(asset_defn.title, title) + self.assertEqual(asset_defn.description, description) + self.assertEqual(asset_defn.media_type, media_type) + self.assertEqual(asset_defn.roles, roles) + def test_title(self) -> None: asset_defn = AssetDefinition({}) title = "Very Important Asset" From d882436191d73d9282732cb807ed262ff91f7fa1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Mar 2022 09:59:54 -0400 Subject: [PATCH 12/60] build(deps): bump black from 22.1.0 to 22.3.0 (#776) Bumps [black](https://github.com/psf/black) from 22.1.0 to 22.3.0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/22.1.0...22.3.0) --- updated-dependencies: - dependency-name: black dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index e99fab492..5a13961bf 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,6 @@ mypy==0.941 flake8==4.0.1 -black==22.1.0 +black==22.3.0 codespell==2.1.0 From 3209bf8d5163972b458ec94dcc2244827484ba06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Mar 2022 10:55:16 -0400 Subject: [PATCH 13/60] build(deps): bump mypy from 0.941 to 0.942 (#770) Bumps [mypy](https://github.com/python/mypy) from 0.941 to 0.942. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.941...v0.942) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 5a13961bf..1ebf1a5cb 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -mypy==0.941 +mypy==0.942 flake8==4.0.1 black==22.3.0 From 443828f06a10199718f246bf9037fbdc798276fd Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Wed, 30 Mar 2022 09:20:15 -0600 Subject: [PATCH 14/60] Fix the "how to create STAC catalogs" tutorial (#775) * docs: fix creating catalogs tutorial * docs: fix concepts python * chore: update changelog Co-authored-by: Jon Duckworth --- CHANGELOG.md | 2 + docs/concepts.rst | 7 +- .../how-to-create-stac-catalogs.ipynb | 1152 ++++++++--------- 3 files changed, 509 insertions(+), 652 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e4babb6a..27fedbf79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ ### Fixed +- "How to create STAC catalogs" tutorial ([#775](https://github.com/stac-utils/pystac/pull/775)) + ## [v1.4.0] ### Added diff --git a/docs/concepts.rst b/docs/concepts.rst index 8d2ac863d..bdf82024c 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -279,7 +279,7 @@ for reading from AWS's S3 cloud object storage using `boto3 from pystac.stac_io import DefaultStacIO, StacIO class CustomStacIO(DefaultStacIO): - def __init__(): + def __init__(self): self.s3 = boto3.resource("s3") def read_text( @@ -302,8 +302,7 @@ for reading from AWS's S3 cloud object storage using `boto3 if parsed.scheme == "s3": bucket = parsed.netloc key = parsed.path[1:] - s3 = boto3.resource("s3") - s3.Object(bucket, key).put(Body=txt, ContentEncoding="utf-8") + self.s3.Object(bucket, key).put(Body=txt, ContentEncoding="utf-8") else: super().write_text(dest, txt, *args, **kwargs) @@ -322,7 +321,7 @@ to take advantage of connection pooling using a `requests.Session from pystac.stac_io import DefaultStacIO, StacIO class ConnectionPoolingIO(DefaultStacIO): - def __init__(): + def __init__(self): self.session = requests.Session() def read_text( diff --git a/docs/tutorials/how-to-create-stac-catalogs.ipynb b/docs/tutorials/how-to-create-stac-catalogs.ipynb index 81d7433d3..3e8a6f8a3 100644 --- a/docs/tutorials/how-to-create-stac-catalogs.ipynb +++ b/docs/tutorials/how-to-create-stac-catalogs.ipynb @@ -12,7 +12,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This notebook runs through some of the basics of using PySTAC to create a static STAC. It was part of a 30 minute presentation at the [community STAC sprint](https://github.com/radiantearth/community-sprints/tree/master/11052019-arlignton-va) in Arlington, VA in November 2019." + "This notebook runs through some of the basics of using PySTAC to create a static STAC. It was part of a 30 minute presentation at the [community STAC sprint](https://github.com/radiantearth/community-sprints/tree/master/11052019-arlignton-va) in Arlington, VA in November 2019, updated to work with current PySTAC." ] }, { @@ -24,44 +24,38 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Requirement already satisfied: boto3 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (1.10.8)\n", - "Requirement already satisfied: botocore<1.14.0,>=1.13.8 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from boto3) (1.13.8)\n", - "Requirement already satisfied: s3transfer<0.3.0,>=0.2.0 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from boto3) (0.2.1)\n", - "Requirement already satisfied: jmespath<1.0.0,>=0.7.1 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from boto3) (0.9.4)\n", - "Requirement already satisfied: urllib3<1.26,>=1.20; python_version >= \"3.4\" in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from botocore<1.14.0,>=1.13.8->boto3) (1.25.6)\n", - "Requirement already satisfied: docutils<0.16,>=0.10 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from botocore<1.14.0,>=1.13.8->boto3) (0.15.2)\n", - "Requirement already satisfied: python-dateutil<3.0.0,>=2.1; python_version >= \"2.7\" in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from botocore<1.14.0,>=1.13.8->boto3) (2.8.1)\n", - "Requirement already satisfied: six>=1.5 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from python-dateutil<3.0.0,>=2.1; python_version >= \"2.7\"->botocore<1.14.0,>=1.13.8->boto3) (1.12.0)\n", - "\u001b[33mWARNING: You are using pip version 20.1.1; however, version 20.2 is available.\n", - "You should consider upgrading via the '/Users/rob/proj/stac/pystac/venv/bin/python -m pip install --upgrade pip' command.\u001b[0m\n", - "Requirement already satisfied: rasterio in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (1.1.0)\n", - "Requirement already satisfied: numpy in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from rasterio) (1.17.3)\n", - "Requirement already satisfied: snuggs>=1.4.1 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from rasterio) (1.4.7)\n", - "Requirement already satisfied: click<8,>=4.0 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from rasterio) (7.0)\n", - "Requirement already satisfied: click-plugins in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from rasterio) (1.1.1)\n", - "Requirement already satisfied: attrs in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from rasterio) (19.3.0)\n", - "Requirement already satisfied: cligj>=0.5 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from rasterio) (0.5.0)\n", - "Requirement already satisfied: affine in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from rasterio) (2.3.0)\n", - "Requirement already satisfied: pyparsing>=2.1.6 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from snuggs>=1.4.1->rasterio) (2.4.2)\n", - "\u001b[33mWARNING: You are using pip version 20.1.1; however, version 20.2 is available.\n", - "You should consider upgrading via the '/Users/rob/proj/stac/pystac/venv/bin/python -m pip install --upgrade pip' command.\u001b[0m\n", - "Requirement already satisfied: shapely in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (1.6.4.post2)\n", - "\u001b[33mWARNING: You are using pip version 20.1.1; however, version 20.2 is available.\n", - "You should consider upgrading via the '/Users/rob/proj/stac/pystac/venv/bin/python -m pip install --upgrade pip' command.\u001b[0m\n" + "Requirement already satisfied: boto3 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (1.21.28)\n", + "Requirement already satisfied: rasterio in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (1.2.10)\n", + "Requirement already satisfied: shapely in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (1.8.1.post1)\n", + "Requirement already satisfied: pystac in /Users/gadomski/Code/pystac (1.3.0)\n", + "Requirement already satisfied: jmespath<2.0.0,>=0.7.1 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from boto3) (1.0.0)\n", + "Requirement already satisfied: botocore<1.25.0,>=1.24.28 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from boto3) (1.24.28)\n", + "Requirement already satisfied: s3transfer<0.6.0,>=0.5.0 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from boto3) (0.5.2)\n", + "Requirement already satisfied: certifi in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (2021.5.30)\n", + "Requirement already satisfied: numpy in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (1.22.3)\n", + "Requirement already satisfied: click-plugins in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (1.1.1)\n", + "Requirement already satisfied: click>=4.0 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (8.0.1)\n", + "Requirement already satisfied: snuggs>=1.4.1 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (1.4.7)\n", + "Requirement already satisfied: cligj>=0.5 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (0.7.2)\n", + "Requirement already satisfied: affine in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (2.3.1)\n", + "Requirement already satisfied: setuptools in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (56.0.0)\n", + "Requirement already satisfied: attrs in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (21.2.0)\n", + "Requirement already satisfied: python-dateutil>=2.7.0 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from pystac) (2.8.1)\n", + "Requirement already satisfied: urllib3<1.27,>=1.25.4 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from botocore<1.25.0,>=1.24.28->boto3) (1.26.5)\n", + "Requirement already satisfied: six>=1.5 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from python-dateutil>=2.7.0->pystac) (1.16.0)\n", + "Requirement already satisfied: pyparsing>=2.1.6 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from snuggs>=1.4.1->rasterio) (2.4.7)\n" ] } ], "source": [ - "!pip install boto3\n", - "!pip install rasterio\n", - "!pip install shapely" + "%pip install boto3 rasterio shapely pystac" ] }, { @@ -73,7 +67,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -96,7 +90,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -110,17 +104,17 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "('/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/image.tif',\n", - " )" + "('/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/image.tif',\n", + " )" ] }, - "execution_count": 4, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -141,7 +135,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -155,24 +149,16 @@ " as well as :class:`~pystac.Item` s.\n", "\n", " Args:\n", - " id (str): Identifier for the catalog. Must be unique within the STAC.\n", - " description (str): Detailed multi-line description to fully explain the catalog.\n", - " `CommonMark 0.28 syntax `_ MAY be used for rich text\n", - " representation.\n", - " title (str or None): Optional short descriptive one-line title for the catalog.\n", - " stac_extensions (List[str]): Optional list of extensions the Catalog implements.\n", - " href (str or None): Optional HREF for this catalog, which be set as the catalog's\n", - " self link's HREF.\n", - "\n", - " Attributes:\n", - " id (str): Identifier for the catalog.\n", - " description (str): Detailed multi-line description to fully explain the catalog.\n", - " title (str or None): Optional short descriptive one-line title for the catalog.\n", - " stac_extensions (List[str] or None): Optional list of extensions the Catalog implements.\n", - " extra_fields (dict or None): Extra fields that are part of the top-level JSON properties\n", - " of the Catalog.\n", - " links (List[Link]): A list of :class:`~pystac.Link` objects representing\n", - " all links associated with this Catalog.\n", + " id : Identifier for the catalog. Must be unique within the STAC.\n", + " description : Detailed multi-line description to fully explain the catalog.\n", + " `CommonMark 0.28 syntax `_ MAY be used for rich\n", + " text representation.\n", + " title : Optional short descriptive one-line title for the catalog.\n", + " stac_extensions : Optional list of extensions the Catalog implements.\n", + " href : Optional HREF for this catalog, which be set as the\n", + " catalog's self link's HREF.\n", + " catalog_type : Optional catalog type for this catalog. Must\n", + " be one of the values in :class:`~pystac.CatalogType`.\n", " \n" ] } @@ -190,7 +176,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -206,7 +192,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -232,7 +218,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -244,45 +230,24 @@ " satellite imagery, derived data, DEM's, etc.\n", "\n", " Args:\n", - " id (str): Provider identifier. Must be unique within the STAC.\n", - " geometry (dict): Defines the full footprint of the asset represented by this item,\n", - " formatted according to `RFC 7946, section 3.1 (GeoJSON)\n", - " `_.\n", - " bbox (List[float] or None): Bounding Box of the asset represented by this item using\n", - " either 2D or 3D geometries. The length of the array must be 2*n where n is the\n", - " number of dimensions. Could also be None in the case of a null geometry.\n", - " datetime (datetime or None): Datetime associated with this item. If None,\n", + " id : Provider identifier. Must be unique within the STAC.\n", + " geometry : Defines the full footprint of the asset represented by this\n", + " item, formatted according to\n", + " `RFC 7946, section 3.1 (GeoJSON) `_.\n", + " bbox : Bounding Box of the asset represented by this item\n", + " using either 2D or 3D geometries. The length of the array must be 2*n\n", + " where n is the number of dimensions. Could also be None in the case of a\n", + " null geometry.\n", + " datetime : Datetime associated with this item. If None,\n", " a start_datetime and end_datetime must be supplied in the properties.\n", - " properties (dict): A dictionary of additional metadata for the item.\n", - " stac_extensions (List[str]): Optional list of extensions the Item implements.\n", - " href (str or None): Optional HREF for this item, which be set as the item's\n", + " properties : A dictionary of additional metadata for the item.\n", + " stac_extensions : Optional list of extensions the Item implements.\n", + " href : Optional HREF for this item, which be set as the item's\n", " self link's HREF.\n", - " collection (Collection or str): The Collection or Collection ID that this item\n", + " collection : The Collection or Collection ID that this item\n", " belongs to.\n", - " extra_fields (dict or None): Extra fields that are part of the top-level JSON properties\n", - " of the Item.\n", - "\n", - " Attributes:\n", - " id (str): Provider identifier. Unique within the STAC.\n", - " geometry (dict): Defines the full footprint of the asset represented by this item,\n", - " formatted according to `RFC 7946, section 3.1 (GeoJSON)\n", - " `_.\n", - " bbox (List[float] or None): Bounding Box of the asset represented by this item using\n", - " either 2D or 3D geometries. The length of the array is 2*n where n is the\n", - " number of dimensions. Could also be None in the case of a null geometry.\n", - " datetime (datetime or None): Datetime associated with this item. If None,\n", - " the start_datetime and end_datetime in the common_metadata\n", - " will supply the datetime range of the Item.\n", - " properties (dict): A dictionary of additional metadata for the item.\n", - " stac_extensions (List[str] or None): Optional list of extensions the Item implements.\n", - " collection (Collection or None): Collection that this item is a part of.\n", - " links (List[Link]): A list of :class:`~pystac.Link` objects representing\n", - " all links associated with this STACObject.\n", - " assets (Dict[str, Asset]): Dictionary of asset objects that can be downloaded,\n", - " each with a unique key.\n", - " collection_id (str or None): The Collection ID that this item belongs to, if any.\n", - " extra_fields (dict or None): Extra fields that are part of the top-level JSON properties\n", - " of the Item.\n", + " extra_fields : Extra fields that are part of the top-level JSON\n", + " properties of the Item.\n", " \n" ] } @@ -300,7 +265,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -323,7 +288,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -350,7 +315,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ @@ -372,27 +337,16 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 14, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "item.get_parent() is None" + "assert item.get_parent() is None" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -401,7 +355,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -410,7 +364,7 @@ "" ] }, - "execution_count": 14, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -428,7 +382,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -455,42 +409,30 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "An object that contains a link to data associated with the Item that can be\n", - " downloaded or streamed.\n", + "An object that contains a link to data associated with an Item or Collection that\n", + " can be downloaded or streamed.\n", "\n", " Args:\n", - " href (str): Link to the asset object. Relative and absolute links are both allowed.\n", - " title (str): Optional displayed title for clients and users.\n", - " description (str): A description of the Asset providing additional details, such as\n", - " how it was processed or created. CommonMark 0.29 syntax MAY be used for rich\n", - " text representation.\n", - " media_type (str): Optional description of the media type. Registered Media Types\n", - " are preferred. See :class:`~pystac.MediaType` for common media types.\n", - " roles ([str]): Optional, Semantic roles (i.e. thumbnail, overview, data, metadata)\n", - " of the asset.\n", - " properties (dict): Optional, additional properties for this asset. This is used by\n", - " extensions as a way to serialize and deserialize properties on asset\n", - " object JSON.\n", - "\n", - " Attributes:\n", - " href (str): Link to the asset object. Relative and absolute links are both allowed.\n", - " title (str): Optional displayed title for clients and users.\n", - " description (str): A description of the Asset providing additional details, such as\n", - " how it was processed or created. CommonMark 0.29 syntax MAY be used for rich\n", - " text representation.\n", - " media_type (str): Optional description of the media type. Registered Media Types\n", + " href : Link to the asset object. Relative and absolute links are both\n", + " allowed.\n", + " title : Optional displayed title for clients and users.\n", + " description : A description of the Asset providing additional details,\n", + " such as how it was processed or created. CommonMark 0.29 syntax MAY be used\n", + " for rich text representation.\n", + " media_type : Optional description of the media type. Registered Media Types\n", " are preferred. See :class:`~pystac.MediaType` for common media types.\n", - " properties (dict): Optional, additional properties for this asset. This is used by\n", - " extensions as a way to serialize and deserialize properties on asset\n", + " roles : Optional, Semantic roles (i.e. thumbnail, overview,\n", + " data, metadata) of the asset.\n", + " extra_fields : Optional, additional fields for this asset. This is used\n", + " by extensions as a way to serialize and deserialize properties on asset\n", " object JSON.\n", - " owner (Item or None): The Item this asset belongs to.\n", " \n" ] } @@ -501,20 +443,9 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 19, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "item.add_asset(\n", " key='image', \n", @@ -534,7 +465,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 20, "metadata": {}, "outputs": [ { @@ -543,10 +474,10 @@ "text": [ "{\n", " \"type\": \"Feature\",\n", - " \"stac_version\": \"1.0.0-beta.2\",\n", + " \"stac_version\": \"1.0.0\",\n", " \"id\": \"local-image\",\n", " \"properties\": {\n", - " \"datetime\": \"2020-08-03T03:47:48.786929Z\"\n", + " \"datetime\": \"2022-03-29T12:47:45.754444Z\"\n", " },\n", " \"geometry\": {\n", " \"type\": \"Polygon\",\n", @@ -589,7 +520,7 @@ " ],\n", " \"assets\": {\n", " \"image\": {\n", - " \"href\": \"/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/image.tif\",\n", + " \"href\": \"/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/image.tif\",\n", " \"type\": \"image/tiff; application=geotiff\"\n", " }\n", " },\n", @@ -598,7 +529,8 @@ " 55.73478197572927,\n", " 37.66573047610874,\n", " 55.73882710285011\n", - " ]\n", + " ],\n", + " \"stac_extensions\": []\n", "}\n" ] } @@ -631,7 +563,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 21, "metadata": {}, "outputs": [ { @@ -657,20 +589,9 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 22, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "catalog.normalize_hrefs(os.path.join(tmp_dir.name, 'stac'))" ] @@ -684,15 +605,15 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 23, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/stac/catalog.json\n", - "/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/stac/local-image/local-image.json\n" + "/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/stac/catalog.json\n", + "/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/stac/local-image/local-image.json\n" ] } ], @@ -712,7 +633,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 24, "metadata": {}, "outputs": [], "source": [ @@ -721,17 +642,17 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 25, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/stac/catalog.json\r\n", - "\r\n", - "/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/stac/local-image:\r\n", - "local-image.json\r\n" + "/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/stac/catalog.json\n", + "\n", + "/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/stac/local-image:\n", + "local-image.json\n" ] } ], @@ -741,7 +662,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 26, "metadata": {}, "outputs": [ { @@ -749,33 +670,35 @@ "output_type": "stream", "text": [ "{\n", - " \"id\": \"test-catalog\",\n", - " \"stac_version\": \"1.0.0-beta.2\",\n", - " \"description\": \"Tutorial catalog.\",\n", - " \"links\": [\n", - " {\n", - " \"rel\": \"root\",\n", - " \"href\": \"./catalog.json\",\n", - " \"type\": \"application/json\"\n", - " },\n", - " {\n", - " \"rel\": \"item\",\n", - " \"href\": \"./local-image/local-image.json\",\n", - " \"type\": \"application/json\"\n", - " }\n", - " ]\n", + " \"type\": \"Catalog\",\n", + " \"id\": \"test-catalog\",\n", + " \"stac_version\": \"1.0.0\",\n", + " \"description\": \"Tutorial catalog.\",\n", + " \"links\": [\n", + " {\n", + " \"rel\": \"root\",\n", + " \"href\": \"./catalog.json\",\n", + " \"type\": \"application/json\"\n", + " },\n", + " {\n", + " \"rel\": \"item\",\n", + " \"href\": \"./local-image/local-image.json\",\n", + " \"type\": \"application/json\"\n", + " }\n", + " ],\n", + " \"stac_extensions\": []\n", "}\n" ] } ], "source": [ - "with open(catalog.get_self_href()) as f:\n", + "with open(item.self_href) as f:\n", " print(f.read())" ] }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 27, "metadata": {}, "outputs": [ { @@ -783,69 +706,70 @@ "output_type": "stream", "text": [ "{\n", - " \"type\": \"Feature\",\n", - " \"stac_version\": \"1.0.0-beta.2\",\n", - " \"id\": \"local-image\",\n", - " \"properties\": {\n", - " \"datetime\": \"2020-08-03T03:47:48.786929Z\"\n", - " },\n", - " \"geometry\": {\n", - " \"type\": \"Polygon\",\n", - " \"coordinates\": [\n", - " [\n", - " [\n", - " 37.6616853489879,\n", - " 55.73478197572927\n", - " ],\n", - " [\n", - " 37.6616853489879,\n", - " 55.73882710285011\n", - " ],\n", - " [\n", - " 37.66573047610874,\n", - " 55.73882710285011\n", - " ],\n", - " [\n", - " 37.66573047610874,\n", - " 55.73478197572927\n", - " ],\n", - " [\n", - " 37.6616853489879,\n", - " 55.73478197572927\n", - " ]\n", - " ]\n", + " \"type\": \"Feature\",\n", + " \"stac_version\": \"1.0.0\",\n", + " \"id\": \"local-image\",\n", + " \"properties\": {\n", + " \"datetime\": \"2022-03-29T12:47:45.754444Z\"\n", + " },\n", + " \"geometry\": {\n", + " \"type\": \"Polygon\",\n", + " \"coordinates\": [\n", + " [\n", + " [\n", + " 37.6616853489879,\n", + " 55.73478197572927\n", + " ],\n", + " [\n", + " 37.6616853489879,\n", + " 55.73882710285011\n", + " ],\n", + " [\n", + " 37.66573047610874,\n", + " 55.73882710285011\n", + " ],\n", + " [\n", + " 37.66573047610874,\n", + " 55.73478197572927\n", + " ],\n", + " [\n", + " 37.6616853489879,\n", + " 55.73478197572927\n", " ]\n", - " },\n", - " \"links\": [\n", - " {\n", - " \"rel\": \"root\",\n", - " \"href\": \"../catalog.json\",\n", - " \"type\": \"application/json\"\n", - " },\n", - " {\n", - " \"rel\": \"parent\",\n", - " \"href\": \"../catalog.json\",\n", - " \"type\": \"application/json\"\n", - " }\n", - " ],\n", - " \"assets\": {\n", - " \"image\": {\n", - " \"href\": \"/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/image.tif\",\n", - " \"type\": \"image/tiff; application=geotiff\"\n", - " }\n", - " },\n", - " \"bbox\": [\n", - " 37.6616853489879,\n", - " 55.73478197572927,\n", - " 37.66573047610874,\n", - " 55.73882710285011\n", + " ]\n", " ]\n", + " },\n", + " \"links\": [\n", + " {\n", + " \"rel\": \"root\",\n", + " \"href\": \"../catalog.json\",\n", + " \"type\": \"application/json\"\n", + " },\n", + " {\n", + " \"rel\": \"parent\",\n", + " \"href\": \"../catalog.json\",\n", + " \"type\": \"application/json\"\n", + " }\n", + " ],\n", + " \"assets\": {\n", + " \"image\": {\n", + " \"href\": \"/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/image.tif\",\n", + " \"type\": \"image/tiff; application=geotiff\"\n", + " }\n", + " },\n", + " \"bbox\": [\n", + " 37.6616853489879,\n", + " 55.73478197572927,\n", + " 37.66573047610874,\n", + " 55.73882710285011\n", + " ],\n", + " \"stac_extensions\": []\n", "}\n" ] } ], "source": [ - "with open(item.get_self_href()) as f:\n", + "with open(item.self_href) as f:\n", " print(f.read())" ] }, @@ -858,7 +782,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 28, "metadata": {}, "outputs": [], "source": [ @@ -874,7 +798,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 29, "metadata": {}, "outputs": [ { @@ -882,68 +806,69 @@ "output_type": "stream", "text": [ "{\n", - " \"type\": \"Feature\",\n", - " \"stac_version\": \"1.0.0-beta.2\",\n", - " \"id\": \"local-image\",\n", - " \"properties\": {\n", - " \"datetime\": \"2020-08-03T03:47:48.786929Z\"\n", - " },\n", - " \"geometry\": {\n", - " \"type\": \"Polygon\",\n", - " \"coordinates\": [\n", - " [\n", - " [\n", - " 37.6616853489879,\n", - " 55.73478197572927\n", - " ],\n", - " [\n", - " 37.6616853489879,\n", - " 55.73882710285011\n", - " ],\n", - " [\n", - " 37.66573047610874,\n", - " 55.73882710285011\n", - " ],\n", - " [\n", - " 37.66573047610874,\n", - " 55.73478197572927\n", - " ],\n", - " [\n", - " 37.6616853489879,\n", - " 55.73478197572927\n", - " ]\n", - " ]\n", + " \"type\": \"Feature\",\n", + " \"stac_version\": \"1.0.0\",\n", + " \"id\": \"local-image\",\n", + " \"properties\": {\n", + " \"datetime\": \"2022-03-29T12:47:45.754444Z\"\n", + " },\n", + " \"geometry\": {\n", + " \"type\": \"Polygon\",\n", + " \"coordinates\": [\n", + " [\n", + " [\n", + " 37.6616853489879,\n", + " 55.73478197572927\n", + " ],\n", + " [\n", + " 37.6616853489879,\n", + " 55.73882710285011\n", + " ],\n", + " [\n", + " 37.66573047610874,\n", + " 55.73882710285011\n", + " ],\n", + " [\n", + " 37.66573047610874,\n", + " 55.73478197572927\n", + " ],\n", + " [\n", + " 37.6616853489879,\n", + " 55.73478197572927\n", " ]\n", + " ]\n", + " ]\n", + " },\n", + " \"links\": [\n", + " {\n", + " \"rel\": \"root\",\n", + " \"href\": \"/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/stac/catalog.json\",\n", + " \"type\": \"application/json\"\n", " },\n", - " \"links\": [\n", - " {\n", - " \"rel\": \"self\",\n", - " \"href\": \"/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/stac/local-image/local-image.json\",\n", - " \"type\": \"application/json\"\n", - " },\n", - " {\n", - " \"rel\": \"root\",\n", - " \"href\": \"/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/stac/catalog.json\",\n", - " \"type\": \"application/json\"\n", - " },\n", - " {\n", - " \"rel\": \"parent\",\n", - " \"href\": \"/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/stac/catalog.json\",\n", - " \"type\": \"application/json\"\n", - " }\n", - " ],\n", - " \"assets\": {\n", - " \"image\": {\n", - " \"href\": \"/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/image.tif\",\n", - " \"type\": \"image/tiff; application=geotiff\"\n", - " }\n", + " {\n", + " \"rel\": \"parent\",\n", + " \"href\": \"/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/stac/catalog.json\",\n", + " \"type\": \"application/json\"\n", " },\n", - " \"bbox\": [\n", - " 37.6616853489879,\n", - " 55.73478197572927,\n", - " 37.66573047610874,\n", - " 55.73882710285011\n", - " ]\n", + " {\n", + " \"rel\": \"self\",\n", + " \"href\": \"/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/stac/local-image/local-image.json\",\n", + " \"type\": \"application/json\"\n", + " }\n", + " ],\n", + " \"assets\": {\n", + " \"image\": {\n", + " \"href\": \"/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/image.tif\",\n", + " \"type\": \"image/tiff; application=geotiff\"\n", + " }\n", + " },\n", + " \"bbox\": [\n", + " 37.6616853489879,\n", + " 55.73478197572927,\n", + " 37.66573047610874,\n", + " 55.73882710285011\n", + " ],\n", + " \"stac_extensions\": []\n", "}\n" ] } @@ -962,7 +887,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 30, "metadata": {}, "outputs": [], "source": [ @@ -972,7 +897,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 31, "metadata": {}, "outputs": [ { @@ -980,63 +905,64 @@ "output_type": "stream", "text": [ "{\n", - " \"type\": \"Feature\",\n", - " \"stac_version\": \"1.0.0-beta.2\",\n", - " \"id\": \"local-image\",\n", - " \"properties\": {\n", - " \"datetime\": \"2020-08-03T03:47:48.786929Z\"\n", - " },\n", - " \"geometry\": {\n", - " \"type\": \"Polygon\",\n", - " \"coordinates\": [\n", - " [\n", - " [\n", - " 37.6616853489879,\n", - " 55.73478197572927\n", - " ],\n", - " [\n", - " 37.6616853489879,\n", - " 55.73882710285011\n", - " ],\n", - " [\n", - " 37.66573047610874,\n", - " 55.73882710285011\n", - " ],\n", - " [\n", - " 37.66573047610874,\n", - " 55.73478197572927\n", - " ],\n", - " [\n", - " 37.6616853489879,\n", - " 55.73478197572927\n", - " ]\n", - " ]\n", + " \"type\": \"Feature\",\n", + " \"stac_version\": \"1.0.0\",\n", + " \"id\": \"local-image\",\n", + " \"properties\": {\n", + " \"datetime\": \"2022-03-29T12:47:45.754444Z\"\n", + " },\n", + " \"geometry\": {\n", + " \"type\": \"Polygon\",\n", + " \"coordinates\": [\n", + " [\n", + " [\n", + " 37.6616853489879,\n", + " 55.73478197572927\n", + " ],\n", + " [\n", + " 37.6616853489879,\n", + " 55.73882710285011\n", + " ],\n", + " [\n", + " 37.66573047610874,\n", + " 55.73882710285011\n", + " ],\n", + " [\n", + " 37.66573047610874,\n", + " 55.73478197572927\n", + " ],\n", + " [\n", + " 37.6616853489879,\n", + " 55.73478197572927\n", " ]\n", - " },\n", - " \"links\": [\n", - " {\n", - " \"rel\": \"root\",\n", - " \"href\": \"../catalog.json\",\n", - " \"type\": \"application/json\"\n", - " },\n", - " {\n", - " \"rel\": \"parent\",\n", - " \"href\": \"../catalog.json\",\n", - " \"type\": \"application/json\"\n", - " }\n", - " ],\n", - " \"assets\": {\n", - " \"image\": {\n", - " \"href\": \"../../image.tif\",\n", - " \"type\": \"image/tiff; application=geotiff\"\n", - " }\n", - " },\n", - " \"bbox\": [\n", - " 37.6616853489879,\n", - " 55.73478197572927,\n", - " 37.66573047610874,\n", - " 55.73882710285011\n", + " ]\n", " ]\n", + " },\n", + " \"links\": [\n", + " {\n", + " \"rel\": \"root\",\n", + " \"href\": \"../catalog.json\",\n", + " \"type\": \"application/json\"\n", + " },\n", + " {\n", + " \"rel\": \"parent\",\n", + " \"href\": \"../catalog.json\",\n", + " \"type\": \"application/json\"\n", + " }\n", + " ],\n", + " \"assets\": {\n", + " \"image\": {\n", + " \"href\": \"../../image.tif\",\n", + " \"type\": \"image/tiff; application=geotiff\"\n", + " }\n", + " },\n", + " \"bbox\": [\n", + " 37.6616853489879,\n", + " 55.73478197572927,\n", + " 37.66573047610874,\n", + " 55.73882710285011\n", + " ],\n", + " \"stac_extensions\": []\n", "}\n" ] } @@ -1064,11 +990,11 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 32, "metadata": {}, "outputs": [], "source": [ - "from pystac.extensions.eo import Band\n", + "from pystac.extensions.eo import Band, EOExtension\n", "\n", "# From: https://www.spaceimagingme.com/downloads/sensors/datasheets/DG_WorldView3_DS_2014.pdf\n", "\n", @@ -1098,7 +1024,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 33, "metadata": {}, "outputs": [], "source": [ @@ -1107,9 +1033,8 @@ " bbox=bbox,\n", " datetime=datetime.utcnow(),\n", " properties={})\n", - "\n", - "eo_item.ext.enable(pystac.Extensions.EO)\n", - "eo_item.ext.eo.apply(bands=wv3_bands)" + "eo = EOExtension.ext(eo_item, add_if_missing=True)\n", + "eo.apply(bands=wv3_bands)" ] }, { @@ -1121,33 +1046,30 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 34, "metadata": {}, "outputs": [], "source": [ "eo_item.common_metadata.platform = \"Maxar\"\n", - "eo_item.common_metadata.instrument=\"WorldView3\"\n", + "eo_item.common_metadata.instruments = [\"WorldView3\"]\n", "eo_item.common_metadata.gsd = 0.3" ] }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 35, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] } ], "source": [ - "eo_item" + "print(eo_item)" ] }, { @@ -1159,52 +1081,15 @@ }, { "cell_type": "code", - "execution_count": 34, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Help on method set_bands in module pystac.extensions.eo:\n", - "\n", - "set_bands(bands, asset=None) method of pystac.extensions.eo.EOItemExt instance\n", - " Set an Item or an Asset bands.\n", - " \n", - " If an Asset is supplied, sets the property on the Asset.\n", - " Otherwise sets the Item's value.\n", - "\n" - ] - } - ], - "source": [ - "eo_ext = eo_item.ext.eo\n", - "help(eo_ext.set_bands)\n", - "\n", - "#eo_item.add_asset(key='image', asset=)" - ] - }, - { - "cell_type": "code", - "execution_count": 35, + "execution_count": 36, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 35, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "asset = pystac.Asset(href=img_path, \n", " media_type=pystac.MediaType.GEOTIFF)\n", - "eo_ext.set_bands(wv3_bands, asset)\n", - "eo_item.add_asset(\"image\", asset)" + "eo_item.add_asset(\"image\", asset)\n", + "eo_on_asset = EOExtension.ext(eo_item.assets[\"image\"])\n", + "eo_on_asset.apply(wv3_bands)" ] }, { @@ -1216,14 +1101,14 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 37, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'href': '/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/image.tif',\n", - " 'type': 'image/tiff; application=geotiff',\n", + "{'href': '/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/image.tif',\n", + " 'type': ,\n", " 'eo:bands': [{'name': 'Coastal',\n", " 'common_name': 'coastal',\n", " 'description': 'Coastal: 400 - 450 nm'},\n", @@ -1246,7 +1131,7 @@ " 'description': 'Near-IR2: 860 - 1040 nm'}]}" ] }, - "execution_count": 36, + "execution_count": 37, "metadata": {}, "output_type": "execute_result" } @@ -1264,7 +1149,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 38, "metadata": {}, "outputs": [ { @@ -1273,7 +1158,7 @@ "[]" ] }, - "execution_count": 37, + "execution_count": 38, "metadata": {}, "output_type": "execute_result" } @@ -1285,7 +1170,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 39, "metadata": {}, "outputs": [ { @@ -1294,7 +1179,7 @@ "[]" ] }, - "execution_count": 38, + "execution_count": 39, "metadata": {}, "output_type": "execute_result" } @@ -1306,7 +1191,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 40, "metadata": {}, "outputs": [], "source": [ @@ -1323,7 +1208,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 41, "metadata": {}, "outputs": [], "source": [ @@ -1332,7 +1217,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 42, "metadata": {}, "outputs": [ { @@ -1341,69 +1226,50 @@ "[]" ] }, - "execution_count": 41, + "execution_count": 42, "metadata": {}, "output_type": "execute_result" } ], "source": [ + "assert isinstance(catalog2, pystac.Catalog)\n", "list(catalog2.get_items())" ] }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 43, "metadata": {}, "outputs": [], "source": [ - "item = next(catalog2.get_all_items())" + "item: pystac.Item = next(catalog2.get_all_items())" ] }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 44, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 43, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "item.ext.implements('eo')" + "assert EOExtension.has_extension(item)" ] }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 45, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "[,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ]" - ] - }, - "execution_count": 44, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "[, , , , , , , ]\n" + ] } ], "source": [ - "item.ext.eo.get_bands(item.assets['image'])" + "eo_on_asset = EOExtension.ext(item.assets[\"image\"])\n", + "print(eo_on_asset.bands)" ] }, { @@ -1419,17 +1285,17 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 46, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "('/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/image.tif',\n", - " )" + "('/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/image.tif',\n", + " )" ] }, - "execution_count": 45, + "execution_count": 46, "metadata": {}, "output_type": "execute_result" } @@ -1444,7 +1310,7 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 47, "metadata": {}, "outputs": [], "source": [ @@ -1460,7 +1326,7 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 48, "metadata": {}, "outputs": [ { @@ -1471,44 +1337,31 @@ " enable discovery.\n", "\n", " Args:\n", - " id (str): Identifier for the collection. Must be unique within the STAC.\n", - " description (str): Detailed multi-line description to fully explain the collection.\n", - " `CommonMark 0.28 syntax `_ MAY be used for rich text\n", - " representation.\n", - " extent (Extent): Spatial and temporal extents that describe the bounds of\n", + " id : Identifier for the collection. Must be unique within the STAC.\n", + " description : Detailed multi-line description to fully explain the\n", + " collection. `CommonMark 0.28 syntax `_ MAY\n", + " be used for rich text representation.\n", + " extent : Spatial and temporal extents that describe the bounds of\n", " all items contained within this Collection.\n", - " title (str or None): Optional short descriptive one-line title for the collection.\n", - " stac_extensions (List[str]): Optional list of extensions the Collection implements.\n", - " href (str or None): Optional HREF for this collection, which be set as the collection's\n", - " self link's HREF.\n", - " license (str): Collection's license(s) as a `SPDX License identifier\n", - " `_, `various`, or `proprietary`. If collection includes\n", - " data with multiple different licenses, use `various` and add a link for each.\n", - " Defaults to 'proprietary'.\n", - " keywords (List[str]): Optional list of keywords describing the collection.\n", - " providers (List[Provider]): Optional list of providers of this Collection.\n", - " properties (dict): Optional dict of common fields across referenced items.\n", - " summaries (dict): An optional map of property summaries,\n", + " title : Optional short descriptive one-line title for the\n", + " collection.\n", + " stac_extensions : Optional list of extensions the Collection\n", + " implements.\n", + " href : Optional HREF for this collection, which be set as the\n", + " collection's self link's HREF.\n", + " catalog_type : Optional catalog type for this catalog. Must\n", + " be one of the values in :class`~pystac.CatalogType`.\n", + " license : Collection's license(s) as a\n", + " `SPDX License identifier `_,\n", + " `various`, or `proprietary`. If collection includes\n", + " data with multiple different licenses, use `various` and add a link for\n", + " each. Defaults to 'proprietary'.\n", + " keywords : Optional list of keywords describing the collection.\n", + " providers : Optional list of providers of this Collection.\n", + " summaries : An optional map of property summaries,\n", " either a set of values or statistics such as a range.\n", - " extra_fields (dict or None): Extra fields that are part of the top-level JSON properties\n", - " of the Collection.\n", - "\n", - " Attributes:\n", - " id (str): Identifier for the collection.\n", - " description (str): Detailed multi-line description to fully explain the collection.\n", - " extent (Extent): Spatial and temporal extents that describe the bounds of\n", - " all items contained within this Collection.\n", - " title (str or None): Optional short descriptive one-line title for the collection.\n", - " stac_extensions (List[str]): Optional list of extensions the Collection implements.\n", - " keywords (List[str] or None): Optional list of keywords describing the collection.\n", - " providers (List[Provider] or None): Optional list of providers of this Collection.\n", - " properties (dict or None): Optional dict of common fields across referenced items.\n", - " summaries (dict or None): An optional map of property summaries,\n", - " either a set of values or statistics such as a range.\n", - " links (List[Link]): A list of :class:`~pystac.Link` objects representing\n", - " all links associated with this Collection.\n", - " extra_fields (dict or None): Extra fields that are part of the top-level JSON properties\n", - " of the Catalog.\n", + " extra_fields : Extra fields that are part of the top-level\n", + " JSON properties of the Collection.\n", " \n" ] } @@ -1526,22 +1379,20 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 49, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Describes the spatio-temporal extents of a Collection.\n", + "Describes the spatiotemporal extents of a Collection.\n", "\n", " Args:\n", - " spatial (SpatialExtent): Potential spatial extent covered by the collection.\n", - " temporal (TemporalExtent): Potential temporal extent covered by the collection.\n", - "\n", - " Attributes:\n", - " spatial (SpatialExtent): Potential spatial extent covered by the collection.\n", - " temporal (TemporalExtent): Potential temporal extent covered by the collection.\n", + " spatial : Potential spatial extent covered by the collection.\n", + " temporal : Potential temporal extent covered by the collection.\n", + " extra_fields : Dictionary containing additional top-level fields defined on the\n", + " Extent object.\n", " \n" ] } @@ -1561,27 +1412,15 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 50, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 49, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "collection_item = pystac.Item(id='local-image-col-1',\n", " geometry=footprint,\n", " bbox=bbox,\n", " datetime=datetime.utcnow(),\n", - " properties={},\n", - " stac_extensions=[pystac.Extensions.EO])\n", + " properties={})\n", "\n", "collection_item.common_metadata.gsd = 0.3\n", "collection_item.common_metadata.platform = 'Maxar'\n", @@ -1589,15 +1428,15 @@ "\n", "asset = pystac.Asset(href=img_path, \n", " media_type=pystac.MediaType.GEOTIFF)\n", - "collection_item.ext.eo.set_bands(wv3_bands, asset)\n", - "collection_item.add_asset('image', asset)\n", + "collection_item.add_asset(\"image\", asset)\n", + "eo = EOExtension.ext(collection_item.assets[\"image\"], add_if_missing=True)\n", + "eo.apply(wv3_bands)\n", "\n", "collection_item2 = pystac.Item(id='local-image-col-2',\n", " geometry=footprint2,\n", " bbox=bbox2,\n", " datetime=datetime.utcnow(),\n", - " properties={},\n", - " stac_extensions=[pystac.Extensions.EO])\n", + " properties={})\n", "\n", "collection_item2.common_metadata.gsd = 0.3\n", "collection_item2.common_metadata.platform = 'Maxar'\n", @@ -1605,11 +1444,11 @@ "\n", "asset2 = pystac.Asset(href=img_path,\n", " media_type=pystac.MediaType.GEOTIFF)\n", - "collection_item2.ext.eo.set_bands([\n", + "collection_item2.add_asset(\"image\", asset2)\n", + "eo = EOExtension.ext(collection_item2.assets[\"image\"], add_if_missing=True)\n", + "eo.apply([\n", " band for band in wv3_bands if band.name in [\"Red\", \"Green\", \"Blue\"]\n", - "], asset2)\n", - "\n", - "collection_item2.add_asset('image', asset2)" + "])\n" ] }, { @@ -1621,7 +1460,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 51, "metadata": {}, "outputs": [], "source": [ @@ -1634,7 +1473,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 52, "metadata": {}, "outputs": [], "source": [ @@ -1644,7 +1483,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 53, "metadata": {}, "outputs": [], "source": [ @@ -1653,7 +1492,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 54, "metadata": {}, "outputs": [], "source": [ @@ -1672,7 +1511,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 55, "metadata": {}, "outputs": [], "source": [ @@ -1681,7 +1520,7 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 56, "metadata": {}, "outputs": [], "source": [ @@ -1692,7 +1531,7 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 57, "metadata": {}, "outputs": [ { @@ -1712,7 +1551,7 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": 58, "metadata": {}, "outputs": [], "source": [ @@ -1731,7 +1570,7 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": 59, "metadata": {}, "outputs": [], "source": [ @@ -1758,42 +1597,50 @@ "source": [ "### Allowing PySTAC to read from AWS S3\n", "\n", - "PySTAC aims to be virtually zero-dependency (notwithstanding the why-isn't-this-in-stdlib datetime-util), so it doesn't have the ability to read from or write to anything but the local file system. However, we can hook into PySTAC's IO in the following way. Learn more about how to use STAC_IO in the [documentation on the topic](https://pystac.readthedocs.io/en/latest/concepts.html#using-stac-io):" + "PySTAC aims to be virtually zero-dependency (notwithstanding the why-isn't-this-in-stdlib datetime-util), so it doesn't have the ability to read from or write to anything but the local file system. However, we can hook into PySTAC's IO in the following way. Learn more about how to customize I/O in STAC from the [documentation](https://pystac.readthedocs.io/en/stable/concepts.html#i-o-in-pystac):" ] }, { "cell_type": "code", - "execution_count": 59, + "execution_count": 60, "metadata": {}, "outputs": [], "source": [ + "from typing import Union, Any\n", "from urllib.parse import urlparse\n", + "\n", "import boto3\n", - "from pystac import STAC_IO\n", + "from pystac import Link\n", + "from pystac.stac_io import DefaultStacIO\n", "\n", - "def my_read_method(uri):\n", - " parsed = urlparse(uri)\n", - " if parsed.scheme == 's3':\n", - " bucket = parsed.netloc\n", - " key = parsed.path[1:]\n", - " s3 = boto3.resource('s3')\n", - " obj = s3.Object(bucket, key)\n", - " return obj.get()['Body'].read().decode('utf-8')\n", - " else:\n", - " return STAC_IO.default_read_text_method(uri)\n", "\n", - "def my_write_method(uri, txt):\n", - " parsed = urlparse(uri)\n", - " if parsed.scheme == 's3':\n", - " bucket = parsed.netloc\n", - " key = parsed.path[1:]\n", - " s3 = boto3.resource(\"s3\")\n", - " s3.Object(bucket, key).put(Body=txt)\n", - " else:\n", - " STAC_IO.default_write_text_method(uri, txt)\n", + "class CustomStacIO(DefaultStacIO):\n", + " def __init__(self):\n", + " self.s3 = boto3.resource(\"s3\")\n", "\n", - "STAC_IO.read_text_method = my_read_method\n", - "STAC_IO.write_text_method = my_write_method" + " def read_text(\n", + " self, source: Union[str, Link], *args: Any, **kwargs: Any\n", + " ) -> str:\n", + " parsed = urlparse(uri)\n", + " if parsed.scheme == \"s3\":\n", + " bucket = parsed.netloc\n", + " key = parsed.path[1:]\n", + "\n", + " obj = self.s3.Object(bucket, key)\n", + " return obj.get()[\"Body\"].read().decode(\"utf-8\")\n", + " else:\n", + " return super().read_text(source, *args, **kwargs)\n", + "\n", + " def write_text(\n", + " self, dest: Union[str, Link], txt: str, *args: Any, **kwargs: Any\n", + " ) -> None:\n", + " parsed = urlparse(uri)\n", + " if parsed.scheme == \"s3\":\n", + " bucket = parsed.netloc\n", + " key = parsed.path[1:]\n", + " self.s3.Object(bucket, key).put(Body=txt, ContentEncoding=\"utf-8\")\n", + " else:\n", + " super().write_text(dest, txt, *args, **kwargs)\n" ] }, { @@ -1805,15 +1652,17 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 63, "metadata": {}, "outputs": [], "source": [ "# From https://alexwlchan.net/2017/07/listing-s3-keys/\n", + "from botocore import UNSIGNED\n", + "from botocore.config import Config\n", "\n", "def get_s3_keys(bucket, prefix):\n", " \"\"\"Generate all the keys in an S3 bucket.\"\"\"\n", - " s3 = boto3.client('s3')\n", + " s3 = boto3.client('s3', config=Config(signature_version=UNSIGNED))\n", " kwargs = {'Bucket': bucket, 'Prefix': prefix}\n", " while True:\n", " resp = s3.list_objects_v2(**kwargs)\n", @@ -1835,17 +1684,17 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 64, "metadata": {}, "outputs": [], "source": [ "moscow_training_chip_uris = list(get_s3_keys(bucket='spacenet-dataset', \n", - " prefix='spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS'))" + " prefix='spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/'))" ] }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 65, "metadata": {}, "outputs": [], "source": [ @@ -1870,7 +1719,7 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 66, "metadata": {}, "outputs": [], "source": [ @@ -1879,7 +1728,7 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 67, "metadata": {}, "outputs": [ { @@ -1897,7 +1746,7 @@ " '1005': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1005.tif'}}" ] }, - "execution_count": 64, + "execution_count": 67, "metadata": {}, "output_type": "execute_result" } @@ -1915,7 +1764,7 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 68, "metadata": {}, "outputs": [], "source": [ @@ -1935,7 +1784,7 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 71, "metadata": {}, "outputs": [ { @@ -1956,6 +1805,9 @@ } ], "source": [ + "import os\n", + "os.environ[\"AWS_NO_SIGN_REQUEST\"] = \"true\"\n", + "\n", "for chip_id in chip_id_to_data:\n", " img_uri = chip_id_to_data[chip_id]['img']\n", " print('Processing {}'.format(img_uri))\n", @@ -1965,18 +1817,19 @@ " geometry=footprint,\n", " bbox=bbox,\n", " datetime=datetime.utcnow(),\n", - " properties={},\n", - " stac_extensions=[pystac.Extensions.EO]) \n", + " properties={})\n", " \n", " item.common_metadata.gsd = 0.3\n", " item.common_metadata.platform = 'Maxar'\n", " item.common_metadata.instruments = ['WorldView3']\n", " \n", - " item.ext.eo.bands = wv3_bands\n", + " eo = EOExtension.ext(item, add_if_missing=True)\n", + " eo.bands = wv3_bands\n", " asset = pystac.Asset(href=img_uri,\n", " media_type=pystac.MediaType.COG)\n", - " item.ext.eo.set_bands(wv3_bands, asset)\n", " item.add_asset(key='ps-ms', asset=asset)\n", + " eo = EOExtension.ext(item.assets[\"ps-ms\"])\n", + " eo.bands = wv3_bands\n", " chip_id_to_items[chip_id] = item" ] }, @@ -1991,7 +1844,7 @@ }, { "cell_type": "code", - "execution_count": 67, + "execution_count": 72, "metadata": {}, "outputs": [], "source": [ @@ -2005,7 +1858,7 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": 73, "metadata": {}, "outputs": [], "source": [ @@ -2016,7 +1869,7 @@ }, { "cell_type": "code", - "execution_count": 69, + "execution_count": 74, "metadata": {}, "outputs": [], "source": [ @@ -2025,7 +1878,7 @@ }, { "cell_type": "code", - "execution_count": 70, + "execution_count": 75, "metadata": {}, "outputs": [], "source": [ @@ -2037,7 +1890,7 @@ }, { "cell_type": "code", - "execution_count": 71, + "execution_count": 76, "metadata": {}, "outputs": [], "source": [ @@ -2046,7 +1899,7 @@ }, { "cell_type": "code", - "execution_count": 72, + "execution_count": 77, "metadata": {}, "outputs": [ { @@ -2080,7 +1933,7 @@ }, { "cell_type": "code", - "execution_count": 73, + "execution_count": 78, "metadata": {}, "outputs": [], "source": [ @@ -2090,7 +1943,7 @@ }, { "cell_type": "code", - "execution_count": 74, + "execution_count": 79, "metadata": {}, "outputs": [ { @@ -2127,7 +1980,7 @@ }, { "cell_type": "code", - "execution_count": 75, + "execution_count": 80, "metadata": {}, "outputs": [], "source": [ @@ -2137,7 +1990,7 @@ }, { "cell_type": "code", - "execution_count": 76, + "execution_count": 81, "metadata": {}, "outputs": [], "source": [ @@ -2156,7 +2009,7 @@ }, { "cell_type": "code", - "execution_count": 77, + "execution_count": 82, "metadata": {}, "outputs": [], "source": [ @@ -2173,7 +2026,7 @@ }, { "cell_type": "code", - "execution_count": 78, + "execution_count": 84, "metadata": {}, "outputs": [ { @@ -2183,31 +2036,32 @@ "Applies label extension properties to the extended Item.\n", "\n", " Args:\n", - " label_description (str): A description of the label, how it was created,\n", + " label_description : A description of the label, how it was created,\n", " and what it is recommended for\n", - " label_type (str): An ENUM of either vector label type or raster label type. Use\n", + " label_type : An Enum of either vector label type or raster label type. Use\n", " one of :class:`~pystac.LabelType`.\n", - " label_properties (list or None): These are the names of the property field(s) in each\n", + " label_properties : These are the names of the property field(s) in each\n", " Feature of the label asset's FeatureCollection that contains the classes\n", " (keywords from label:classes if the property defines classes).\n", " If labels are rasters, this should be None.\n", - " label_classes (List[LabelClass]): Optional, but reqiured if ussing categorical data.\n", - " A list of LabelClasses defining the list of possible class names for each\n", - " label:properties. (e.g., tree, building, car, hippo)\n", - " label_tasks (List[str]): Recommended to be a subset of 'regression', 'classification',\n", + " label_classes : Optional, but required if using categorical data.\n", + " A list of :class:`LabelClasses` instances defining the list of possible\n", + " class names for each label:properties. (e.g., tree, building, car,\n", + " hippo)\n", + " label_tasks : Recommended to be a subset of 'regression', 'classification',\n", " 'detection', or 'segmentation', but may be an arbitrary value.\n", " label_methods: Recommended to be a subset of 'automated' or 'manual',\n", " but may be an arbitrary value.\n", - " label_overviews (List[LabelOverview]): Optional list of LabelOverview classes\n", - " that store counts (for classification-type data) or summary statistics (for\n", - " continuous numerical/regression data).\n", + " label_overviews : Optional list of :class:`LabelOverview` instances\n", + " that store counts (for classification-type data) or summary statistics\n", + " (for continuous numerical/regression data).\n", " \n" ] } ], "source": [ - "from pystac.extensions import label\n", - "print(label.LabelItemExt.apply.__doc__)" + "from pystac.extensions.label import LabelExtension\n", + "print(LabelExtension.apply.__doc__)" ] }, { @@ -2219,25 +2073,28 @@ }, { "cell_type": "code", - "execution_count": 79, + "execution_count": 85, "metadata": {}, "outputs": [], "source": [ + "from pystac.extensions.label import LabelType\n", + "\n", "for chip_id in chip_id_to_data:\n", " img_item = collection.get_item('img_{}'.format(chip_id))\n", + " assert img_item\n", " label_uri = chip_id_to_data[chip_id]['label']\n", " \n", " label_item = pystac.Item(id='label_{}'.format(chip_id),\n", " geometry=img_item.geometry,\n", " bbox=img_item.bbox,\n", " datetime=datetime.utcnow(),\n", - " properties={},\n", - " stac_extensions=[pystac.Extensions.LABEL])\n", - " label_item.ext.label.apply(label_description=\"SpaceNet 5 Road labels\",\n", - " label_type=label.LabelType.VECTOR,\n", + " properties={})\n", + " label = LabelExtension.ext(label_item, add_if_missing=True)\n", + " label.apply(label_description=\"SpaceNet 5 Road labels\",\n", + " label_type=LabelType.VECTOR,\n", " label_tasks=['segmentation', 'regression'])\n", - " label_item.ext.label.add_source(img_item)\n", - " label_item.ext.label.add_geojson_labels(label_uri)\n", + " label.add_source(img_item)\n", + " label.add_geojson_labels(label_uri)\n", " \n", " label_catalog.add_item(label_item)" ] @@ -2251,7 +2108,7 @@ }, { "cell_type": "code", - "execution_count": 80, + "execution_count": 86, "metadata": {}, "outputs": [ { @@ -2290,39 +2147,45 @@ }, { "cell_type": "code", - "execution_count": 81, + "execution_count": 87, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'type': 'Feature',\n", - " 'stac_version': '1.0.0-beta.2',\n", + " 'stac_version': '1.0.0',\n", " 'id': 'label_1',\n", " 'properties': {'label:description': 'SpaceNet 5 Road labels',\n", - " 'label:type': 'vector',\n", + " 'label:type': ,\n", " 'label:properties': None,\n", " 'label:tasks': ['segmentation', 'regression'],\n", - " 'datetime': '2020-08-03T03:47:56.599629Z'},\n", + " 'datetime': '2022-03-29T12:58:05.404487Z'},\n", " 'geometry': {'type': 'Polygon',\n", " 'coordinates': (((37.68191035616281, 55.73478210707574),\n", " (37.68191035616281, 55.73882710285011),\n", " (37.68595535193718, 55.73882710285011),\n", " (37.68595535193718, 55.73478210707574),\n", " (37.68191035616281, 55.73478210707574)),)},\n", - " 'links': [{'rel': 'source', 'href': None, 'type': 'application/json'},\n", - " {'rel': 'root', 'href': None, 'type': 'application/json'},\n", - " {'rel': 'parent', 'href': None, 'type': 'application/json'}],\n", + " 'links': [{'rel': 'source',\n", + " 'href': None,\n", + " 'type': },\n", + " {'rel': ,\n", + " 'href': None,\n", + " 'type': },\n", + " {'rel': ,\n", + " 'href': None,\n", + " 'type': }],\n", " 'assets': {'labels': {'href': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/geojson_roads_speed/SN5_roads_train_AOI_7_Moscow_geojson_roads_speed_chip1.geojson',\n", - " 'type': 'application/geo+json'}},\n", + " 'type': }},\n", " 'bbox': [37.68191035616281,\n", " 55.73478210707574,\n", " 37.68595535193718,\n", " 55.73882710285011],\n", - " 'stac_extensions': ['label']}" + " 'stac_extensions': ['https://stac-extensions.github.io/label/v1.0.1/schema.json']}" ] }, - "execution_count": 81, + "execution_count": 87, "metadata": {}, "output_type": "execute_result" } @@ -2331,13 +2194,6 @@ "label_item = catalog.get_child('spacenet-data-labels').get_item('label_1')\n", "label_item.to_dict()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -2356,7 +2212,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.5" + "version": "3.9.12" } }, "nbformat": 4, From 03602412e85f47252298da14714ee9da52360523 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Mar 2022 15:35:16 -0400 Subject: [PATCH 15/60] build(deps): bump sphinx from 4.4.0 to 4.5.0 (#772) * build(deps): bump sphinx from 4.4.0 to 4.5.0 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 4.4.0 to 4.5.0. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/4.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.4.0...v4.5.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Stop suppressing extlink warnings * Fix RST warnings Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jon Duckworth --- docs/conf.py | 9 --------- pystac/extensions/item_assets.py | 4 ++-- requirements-docs.txt | 2 +- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 4c7a93382..7e5717210 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,7 +17,6 @@ import subprocess from typing import Any, Dict, List -from sphinx.util import logging sys.path.insert(0, os.path.abspath(".")) sys.path.insert(0, os.path.abspath("../")) @@ -239,11 +238,3 @@ # -- Substutition variables rst_epilog = f".. |stac_version| replace:: {STACVersion.DEFAULT_STAC_VERSION}" - -# -- Suppress warnings from the extlinks extension -# We do this to avoid warnings like the following in our Jupyter notebook tutorials -# where we do not want to use Sphinx constructs: -# WARNING: hardcoded link 'https://github.com/stac-extensions/eo' could be replaced -# by an extlink (try using ':stac-ext:`eo`' instead) -linklogger = logging.getLogger("sphinx.ext.extlinks") -linklogger.setLevel(40) # Ignore messages less severe than ERROR diff --git a/pystac/extensions/item_assets.py b/pystac/extensions/item_assets.py index 38caae9a0..a146e72b5 100644 --- a/pystac/extensions/item_assets.py +++ b/pystac/extensions/item_assets.py @@ -53,7 +53,7 @@ def create( such as how it was processed or created. `CommonMark 0.29 `__ syntax MAY be used for rich text representation. - media_type : `media type + media_type : `media type\ `__ of the asset. roles : `semantic roles @@ -82,7 +82,7 @@ def apply( such as how it was processed or created. `CommonMark 0.29 `__ syntax MAY be used for rich text representation. - media_type : `media type + media_type : `media type\ `__ of the asset. roles : `semantic roles diff --git a/requirements-docs.txt b/requirements-docs.txt index 579fd1841..5f4211b4d 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ ipython==8.1.1 -Sphinx==4.4.0 +Sphinx==4.5.0 sphinxcontrib-fulltoc==1.2.0 nbsphinx==0.8.8 pydata-sphinx-theme==0.8.0 From 3d334448267c1787fbd3c28a365dffbb9152d05c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Mar 2022 15:56:50 -0400 Subject: [PATCH 16/60] build(deps): bump doc8 from 0.10.1 to 0.11.0 (#777) Bumps [doc8](https://github.com/pycqa/doc8) from 0.10.1 to 0.11.0. - [Release notes](https://github.com/pycqa/doc8/releases) - [Commits](https://github.com/pycqa/doc8/compare/0.10.1...0.11.0) --- updated-dependencies: - dependency-name: doc8 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 1ebf1a5cb..34d778036 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,7 +6,7 @@ codespell==2.1.0 jsonschema==4.4.0 coverage==6.3.2 -doc8==0.10.1 +doc8==0.11.0 types-python-dateutil==2.8.10 types-orjson==3.6.2 From 037fd733abd9bd41ed078424c6059a3bda52acbd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Mar 2022 16:23:01 -0400 Subject: [PATCH 17/60] build(deps): bump ipython from 8.1.1 to 8.2.0 (#774) Bumps [ipython](https://github.com/ipython/ipython) from 8.1.1 to 8.2.0. - [Release notes](https://github.com/ipython/ipython/releases) - [Commits](https://github.com/ipython/ipython/compare/8.1.1...8.2.0) --- updated-dependencies: - dependency-name: ipython dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 5f4211b4d..9ee5e61e3 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,4 +1,4 @@ -ipython==8.1.1 +ipython==8.2.0 Sphinx==4.5.0 sphinxcontrib-fulltoc==1.2.0 nbsphinx==0.8.8 From d5c597d975770d88bedbd77d998936548937f315 Mon Sep 17 00:00:00 2001 From: Alex G Rice Date: Wed, 30 Mar 2022 19:03:40 -0600 Subject: [PATCH 18/60] Replace test.com with special-use domain name. (#769) * Replace test.com with special-use domain name. * Changelog. Co-authored-by: Jon Duckworth --- CHANGELOG.md | 3 ++- tests/test_catalog.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27fedbf79..f640fdfd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,9 @@ ### Added -- Enum MediaType entry for PDF documents ([#758](https://github.com/stac-utils/pystac/pull/758)) +- Enum MediaType entry for PDF documents ([#758](https://github.com/stac-utils/pystac/pull/758)) - Updated Link to obtain stac_io from owner root ([#762](https://github.com/stac-utils/pystac/pull/762)) +- Replace test.com with special-use domain name. ([#769](https://github.com/stac-utils/pystac/pull/769)) - Updated AssetDefinition to have create, apply methods ([#768](https://github.com/stac-utils/pystac/pull/768)) ### Removed diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 1403d182e..2b46ed01e 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -327,7 +327,7 @@ def test_save_uses_previous_catalog_type(self) -> None: def test_save_to_provided_href(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: catalog = TestCases.test_case_1() - href = "http://test.com" + href = "https://stac.test" folder = os.path.join(tmp_dir, "cat") catalog.normalize_hrefs(href) catalog.save(catalog_type=CatalogType.ABSOLUTE_PUBLISHED, dest_href=folder) @@ -341,7 +341,7 @@ def test_save_to_provided_href(self) -> None: def test_save_relative_published_no_self_links(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: catalog = TestCases.test_case_1() - href = "http://test.com" + href = "https://stac.test" folder = os.path.join(tmp_dir, "cat") catalog.normalize_hrefs(href) catalog.save(catalog_type=CatalogType.RELATIVE_PUBLISHED, dest_href=folder) @@ -394,7 +394,7 @@ def test_save_with_different_stac_io(self) -> None: def test_subcatalogs_saved_to_correct_path(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: catalog = TestCases.test_case_1() - href = "http://test.com" + href = "https://stac.test" catalog.normalize_hrefs(href) catalog.save(catalog_type=CatalogType.ABSOLUTE_PUBLISHED, dest_href=tmp_dir) From f917e7bfaac90383e1fb53d2d4f65bc53c18c97a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Mar 2022 21:08:22 -0400 Subject: [PATCH 19/60] build(deps): bump pydata-sphinx-theme from 0.8.0 to 0.8.1 (#773) Bumps [pydata-sphinx-theme](https://github.com/pydata/pydata-sphinx-theme) from 0.8.0 to 0.8.1. - [Release notes](https://github.com/pydata/pydata-sphinx-theme/releases) - [Commits](https://github.com/pydata/pydata-sphinx-theme/compare/v0.8.0...v0.8.1) --- updated-dependencies: - dependency-name: pydata-sphinx-theme dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 9ee5e61e3..bc2814d93 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -2,6 +2,6 @@ ipython==8.2.0 Sphinx==4.5.0 sphinxcontrib-fulltoc==1.2.0 nbsphinx==0.8.8 -pydata-sphinx-theme==0.8.0 +pydata-sphinx-theme==0.8.1 sphinx-panels==0.6.0 jinja2<4.0 From 07816f5e85b67750ad8dc2d84c446c5ed3233602 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Wed, 30 Mar 2022 21:44:04 -0400 Subject: [PATCH 20/60] Update Python 3.11 alpha-release in CI (#779) --- .github/workflows/continuous-integration.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 58d936544..9942a3d3d 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -32,13 +32,13 @@ jobs: experimental: - false include: - - python-version: "3.11.0-alpha.3" + - python-version: "3.11.0-alpha.6" os: ubuntu-latest experimental: true - - python-version: "3.11.0-alpha.3" + - python-version: "3.11.0-alpha.6" os: windows-latest experimental: true - - python-version: "3.11.0-alpha.3" + - python-version: "3.11.0-alpha.6" os: macos-latest experimental: true @@ -127,7 +127,7 @@ jobs: - "3.8" - "3.9" - "3.10" - - "3.11.0-alpha.3" + - "3.11.0-alpha.6" steps: - uses: actions/checkout@v3 From cf431adfc08fb525f81cdd94df91e611a106689e Mon Sep 17 00:00:00 2001 From: Ian Carroll Date: Mon, 4 Apr 2022 15:53:54 -0400 Subject: [PATCH 21/60] Add variables argument to the DatacubeExtension `apply` method (#782) * let apply set variables * add test for datacube apply * pacify mypy * add to changelog --- CHANGELOG.md | 1 + pystac/extensions/datacube.py | 15 ++++++++++++++- tests/extensions/test_datacube.py | 13 +++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f640fdfd4..76378e76b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### Fixed - "How to create STAC catalogs" tutorial ([#775](https://github.com/stac-utils/pystac/pull/775)) +- Add a `variables` argument, to accompany `dimensions`, for the `apply` method of stac objects extended with datacube ([#782](https://github.com/stac-utils/pystac/pull/782)) ## [v1.4.0] diff --git a/pystac/extensions/datacube.py b/pystac/extensions/datacube.py index d02264a64..ea0857c9b 100644 --- a/pystac/extensions/datacube.py +++ b/pystac/extensions/datacube.py @@ -411,6 +411,12 @@ class VariableType(StringEnum): class Variable: + """Object representing a variable in the datacube. The dimensions field lists + zero or more :stac-ext:`Datacube Dimension Object ` + instances. See the :stac-ext:`Datacube Variable Object + ` docs for details. + """ + properties: Dict[str, Any] def __init__(self, properties: Dict[str, Any]) -> None: @@ -522,15 +528,22 @@ class DatacubeExtension( >>> dc_ext = DatacubeExtension.ext(item) """ - def apply(self, dimensions: Dict[str, Dimension]) -> None: + def apply( + self, + dimensions: Dict[str, Dimension], + variables: Optional[Dict[str, Variable]] = None, + ) -> None: """Applies label extension properties to the extended :class:`~pystac.Collection`, :class:`~pystac.Item` or :class:`~pystac.Asset`. Args: dimensions : Dictionary mapping dimension name to a :class:`Dimension` object. + variables : Dictionary mapping variable name to a :class:`Variable` + object. """ self.dimensions = dimensions + self.variables = variables @property def dimensions(self) -> Dict[str, Dimension]: diff --git a/tests/extensions/test_datacube.py b/tests/extensions/test_datacube.py index ef1f60e8c..f7ee17217 100644 --- a/tests/extensions/test_datacube.py +++ b/tests/extensions/test_datacube.py @@ -87,3 +87,16 @@ def test_set_variables(self) -> None: self.assertEqual( item.properties["cube:variables"], {"temp": new_variable.to_dict()} ) + + def test_apply_variables(self) -> None: + item = pystac.Item.from_file(self.example_uri) + cube = DatacubeExtension.ext(item) + variables = cube.variables + assert variables is not None + key, value = variables.popitem() + target = value.to_dict() + cube.variables = None + cube.apply(dimensions={}, variables={key: value}) + variables = cube.variables + assert variables is not None + self.assertEqual(target, cube.variables[key].to_dict()) From 7b638d3833d8175a2b100b1f174bb65fbd1dc017 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Apr 2022 11:50:38 -0400 Subject: [PATCH 22/60] build(deps): bump codecov/codecov-action from 2.1.0 to 3 (#785) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 2.1.0 to 3. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v2.1.0...v3) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 9942a3d3d..2437671d8 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -104,7 +104,7 @@ jobs: run: coverage xml --fail-under 0 - name: Upload All coverage to Codecov - uses: codecov/codecov-action@v2.1.0 + uses: codecov/codecov-action@v3 if: ${{ env.GITHUB_REPOSITORY }} == 'stac-utils/pystac' with: token: ${{ secrets.CODECOV_TOKEN }} From f03a475b2313dd3cc51895b0726778efd51acf68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 10 Apr 2022 10:04:29 -0400 Subject: [PATCH 23/60] build(deps): bump pre-commit from 2.17.0 to 2.18.1 (#784) Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.17.0 to 2.18.1. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v2.17.0...v2.18.1) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 34d778036..482d8fc83 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -11,7 +11,7 @@ doc8==0.11.0 types-python-dateutil==2.8.10 types-orjson==3.6.2 -pre-commit==2.17.0 +pre-commit==2.18.1 # optional dependencies orjson==3.6.7 From a0faa998eadf568f22b368e387868463b2160486 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 10 Apr 2022 13:15:51 -0400 Subject: [PATCH 24/60] build(deps): bump doc8 from 0.11.0 to 0.11.1 (#780) Bumps [doc8](https://github.com/pycqa/doc8) from 0.11.0 to 0.11.1. - [Release notes](https://github.com/pycqa/doc8/releases) - [Commits](https://github.com/pycqa/doc8/compare/0.11.0...0.11.1) --- updated-dependencies: - dependency-name: doc8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 482d8fc83..88a309e85 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,7 +6,7 @@ codespell==2.1.0 jsonschema==4.4.0 coverage==6.3.2 -doc8==0.11.0 +doc8==0.11.1 types-python-dateutil==2.8.10 types-orjson==3.6.2 From a6c8927dfb1f1f92f60937b3022c499137416468 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Mon, 25 Apr 2022 22:40:05 -0400 Subject: [PATCH 25/60] Update CI to use Python 3.11.0-alpha.7 (#795) --- .github/workflows/continuous-integration.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 2437671d8..cf01d1417 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -32,13 +32,13 @@ jobs: experimental: - false include: - - python-version: "3.11.0-alpha.6" + - python-version: "3.11.0-alpha.7" os: ubuntu-latest experimental: true - - python-version: "3.11.0-alpha.6" + - python-version: "3.11.0-alpha.7" os: windows-latest experimental: true - - python-version: "3.11.0-alpha.6" + - python-version: "3.11.0-alpha.7" os: macos-latest experimental: true @@ -127,7 +127,7 @@ jobs: - "3.8" - "3.9" - "3.10" - - "3.11.0-alpha.6" + - "3.11.0-alpha.7" steps: - uses: actions/checkout@v3 From 876b60a8547d9307b564cfb372c1237b324c555b Mon Sep 17 00:00:00 2001 From: Tarashish Mishra Date: Sat, 30 Apr 2022 05:05:47 +0530 Subject: [PATCH 26/60] Deepcopy Collection properties on clone (#794) * Make sure that Collections deepcopy their properties on clone Implements `clone` method on Summaries as well. * Fix broken test * Fix mypy errors * Add changelog entry * fix: minor formatting on changelog * Use copy() method to make copies of list of strings deepcopy() is unnecessary on properties that are only list of strings Co-authored-by: Pete Gadomski --- CHANGELOG.md | 1 + pystac/catalog.py | 2 +- pystac/collection.py | 10 +++++----- pystac/summaries.py | 16 ++++++++++++++++ tests/extensions/test_scientific.py | 2 +- tests/test_collection.py | 13 +++++++++++++ tests/test_summaries.py | 11 +++++++++++ 7 files changed, 48 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76378e76b..125bf72f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - "How to create STAC catalogs" tutorial ([#775](https://github.com/stac-utils/pystac/pull/775)) - Add a `variables` argument, to accompany `dimensions`, for the `apply` method of stac objects extended with datacube ([#782](https://github.com/stac-utils/pystac/pull/782)) +- Deepcopy collection properties on clone. Implement `clone` method for `Summaries` ([#794](https://github.com/stac-utils/pystac/pull/794)) ## [v1.4.0] diff --git a/pystac/catalog.py b/pystac/catalog.py index 95f268df7..c9ab7114d 100644 --- a/pystac/catalog.py +++ b/pystac/catalog.py @@ -517,7 +517,7 @@ def clone(self) -> "Catalog": id=self.id, description=self.description, title=self.title, - stac_extensions=self.stac_extensions, + stac_extensions=self.stac_extensions.copy(), extra_fields=deepcopy(self.extra_fields), catalog_type=self.catalog_type, ) diff --git a/pystac/collection.py b/pystac/collection.py index af99a4320..069367711 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -562,13 +562,13 @@ def clone(self) -> "Collection": description=self.description, extent=self.extent.clone(), title=self.title, - stac_extensions=self.stac_extensions, - extra_fields=self.extra_fields, + stac_extensions=self.stac_extensions.copy(), + extra_fields=deepcopy(self.extra_fields), catalog_type=self.catalog_type, license=self.license, - keywords=self.keywords, - providers=self.providers, - summaries=self.summaries, + keywords=self.keywords.copy() if self.keywords is not None else None, + providers=deepcopy(self.providers), + summaries=self.summaries.clone(), ) clone._resolved_objects.cache(clone) diff --git a/pystac/summaries.py b/pystac/summaries.py index 9652b4b7b..f7850628c 100644 --- a/pystac/summaries.py +++ b/pystac/summaries.py @@ -1,3 +1,4 @@ +from copy import deepcopy import sys import numbers from enum import Enum @@ -287,6 +288,21 @@ def is_empty(self) -> bool: any(self.lists) or any(self.ranges) or any(self.schemas) or any(self.other) ) + def clone(self) -> "Summaries": + """Clones this object. + + Returns: + Summaries: The clone of this object + """ + summaries = Summaries( + summaries=deepcopy(self._summaries), maxcount=self.maxcount + ) + summaries.lists = deepcopy(self.lists) + summaries.other = deepcopy(self.other) + summaries.ranges = deepcopy(self.ranges) + summaries.schemas = deepcopy(self.schemas) + return summaries + def to_dict(self) -> Dict[str, Any]: return { **{k: v for k, v in self.lists.items() if len(v) < self.maxcount}, diff --git a/tests/extensions/test_scientific.py b/tests/extensions/test_scientific.py index 0d881d2f7..9edf3c3b3 100644 --- a/tests/extensions/test_scientific.py +++ b/tests/extensions/test_scientific.py @@ -452,7 +452,7 @@ def test_set_doi_summaries(self) -> None: sci_summaries = ScientificExtension.summaries(collection) sci_summaries.doi = [PUB2_DOI] - new_dois = ScientificExtension.summaries(self.collection).doi + new_dois = ScientificExtension.summaries(collection).doi assert new_dois is not None self.assertListEqual([PUB2_DOI], new_dois) diff --git a/tests/test_collection.py b/tests/test_collection.py index 2a307c38f..61e4dc415 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -80,6 +80,19 @@ def test_clone_uses_previous_catalog_type(self) -> None: clone = catalog.clone() self.assertEqual(clone.catalog_type, CatalogType.SELF_CONTAINED) + def test_clone_cant_mutate_original(self) -> None: + collection = TestCases.test_case_8() + assert collection.keywords is not None + self.assertListEqual(collection.keywords, ["disaster", "open"]) + clone = collection.clone() + clone.extra_fields["test"] = "extra" + self.assertNotIn("test", collection.extra_fields) + assert clone.keywords is not None + clone.keywords.append("clone") + self.assertListEqual(clone.keywords, ["disaster", "open", "clone"]) + self.assertListEqual(collection.keywords, ["disaster", "open"]) + self.assertNotEqual(id(collection.summaries), id(clone.summaries)) + def test_multiple_extents(self) -> None: cat1 = TestCases.test_case_1() country = cat1.get_child("country-1") diff --git a/tests/test_summaries.py b/tests/test_summaries.py index c542da203..adb06daad 100644 --- a/tests/test_summaries.py +++ b/tests/test_summaries.py @@ -59,6 +59,17 @@ def test_summary_not_empty(self) -> None: summaries = Summarizer().summarize(coll.get_all_items()) self.assertFalse(summaries.is_empty()) + def test_clone_summary(self) -> None: + coll = TestCases.test_case_5() + summaries = Summarizer().summarize(coll.get_all_items()) + summaries_dict = summaries.to_dict() + self.assertEqual(len(summaries_dict["eo:bands"]), 4) + self.assertEqual(len(summaries_dict["proj:epsg"]), 1) + clone = summaries.clone() + self.assertTrue(isinstance(clone, Summaries)) + clone_dict = clone.to_dict() + self.assertDictEqual(clone_dict, summaries_dict) + class RangeSummaryTest(unittest.TestCase): def setUp(self) -> None: From 78348674412f17275efdb815a469dfd69f57f24f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Apr 2022 19:42:43 -0400 Subject: [PATCH 27/60] build(deps): bump mypy from 0.942 to 0.950 (#798) Bumps [mypy](https://github.com/python/mypy) from 0.942 to 0.950. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.942...v0.950) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 88a309e85..826da5dc1 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -mypy==0.942 +mypy==0.950 flake8==4.0.1 black==22.3.0 From 8bae35afbfe562d4fbc82980bd695d6304e6a2fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Apr 2022 19:42:56 -0400 Subject: [PATCH 28/60] build(deps): bump ipython from 8.2.0 to 8.3.0 (#800) Bumps [ipython](https://github.com/ipython/ipython) from 8.2.0 to 8.3.0. - [Release notes](https://github.com/ipython/ipython/releases) - [Commits](https://github.com/ipython/ipython/compare/8.2.0...8.3.0) --- updated-dependencies: - dependency-name: ipython dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index bc2814d93..f37028a6d 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,4 +1,4 @@ -ipython==8.2.0 +ipython==8.3.0 Sphinx==4.5.0 sphinxcontrib-fulltoc==1.2.0 nbsphinx==0.8.8 From e5d010f94cf718a94995ea5883325faa820f50c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Apr 2022 19:43:14 -0400 Subject: [PATCH 29/60] build(deps): bump orjson from 3.6.7 to 3.6.8 (#791) Bumps [orjson](https://github.com/ijl/orjson) from 3.6.7 to 3.6.8. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.6.7...3.6.8) --- updated-dependencies: - dependency-name: orjson dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jon Duckworth --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 826da5dc1..7a73d78f8 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -14,4 +14,4 @@ types-orjson==3.6.2 pre-commit==2.18.1 # optional dependencies -orjson==3.6.7 +orjson==3.6.8 From 3a959742c448e6f6cfefb073e0321a22eb18722a Mon Sep 17 00:00:00 2001 From: Phil Varner Date: Wed, 4 May 2022 09:22:29 -0400 Subject: [PATCH 30/60] add Grid Extension support (#799) * add Grid Extension support * update changelog Co-authored-by: Jon Duckworth --- CHANGELOG.md | 1 + pystac/extensions/grid.py | 107 +++ setup.cfg | 7 + .../examples/1.0.0/example-sentinel2.json | 818 ++++++++++++++++++ tests/data-files/examples/example-info.csv | 1 + tests/data-files/grid/example-sentinel2.json | 818 ++++++++++++++++++ tests/extensions/test_grid.py | 122 +++ 7 files changed, 1874 insertions(+) create mode 100644 pystac/extensions/grid.py create mode 100644 tests/data-files/examples/1.0.0/example-sentinel2.json create mode 100644 tests/data-files/grid/example-sentinel2.json create mode 100644 tests/extensions/test_grid.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 125bf72f4..ab25b0aad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Updated Link to obtain stac_io from owner root ([#762](https://github.com/stac-utils/pystac/pull/762)) - Replace test.com with special-use domain name. ([#769](https://github.com/stac-utils/pystac/pull/769)) - Updated AssetDefinition to have create, apply methods ([#768](https://github.com/stac-utils/pystac/pull/768)) +- Add Grid Extension support ([#799](https://github.com/stac-utils/pystac/pull/799)) ### Removed diff --git a/pystac/extensions/grid.py b/pystac/extensions/grid.py new file mode 100644 index 000000000..928897ebc --- /dev/null +++ b/pystac/extensions/grid.py @@ -0,0 +1,107 @@ +"""Implements the :stac-ext:`Grid Extension `.""" + +import re +from typing import Any, Dict, Optional, Pattern, Set, Union + +import pystac +from pystac.extensions.base import ExtensionManagementMixin, PropertiesExtension +from pystac.extensions.hooks import ExtensionHooks + +SCHEMA_URI: str = "https://stac-extensions.github.io/grid/v1.0.0/schema.json" +PREFIX: str = "grid:" + +# Field names +CODE_PROP: str = PREFIX + "code" # required + +CODE_REGEX: str = r"[A-Z]+-[-_.A-Za-z0-9]+" +CODE_PATTERN: Pattern[str] = re.compile(CODE_REGEX) + + +def validated_code(v: str) -> str: + if not isinstance(v, str): + raise ValueError("Invalid Grid code: must be str") + if not CODE_PATTERN.fullmatch(v): + raise ValueError( + f"Invalid Grid code: {v}" f" does not match the regex {CODE_REGEX}" + ) + return v + + +class GridExtension( + PropertiesExtension, + ExtensionManagementMixin[Union[pystac.Item, pystac.Collection]], +): + """A concrete implementation of :class:`GridExtension` on an :class:`~pystac.Item` + that extends the properties of the Item to include properties defined in the + :stac-ext:`Grid Extension `. + + This class should generally not be instantiated directly. Instead, call + :meth:`GridExtension.ext` on an :class:`~pystac.Item` to extend it. + + .. code-block:: python + + >>> item: pystac.Item = ... + >>> proj_ext = GridExtension.ext(item) + """ + + item: pystac.Item + """The :class:`~pystac.Item` being extended.""" + + properties: Dict[str, Any] + """The :class:`~pystac.Item` properties, including extension properties.""" + + def __init__(self, item: pystac.Item): + self.item = item + self.properties = item.properties + + def __repr__(self) -> str: + return "".format(self.item.id) + + def apply(self, code: str) -> None: + """Applies Grid extension properties to the extended Item. + + Args: + code : REQUIRED. The code of the Item's grid location. + """ + self.code = validated_code(code) + + @property + def code(self) -> Optional[str]: + """Get or sets the latitude band of the datasource.""" + return self._get_property(CODE_PROP, str) + + @code.setter + def code(self, v: str) -> None: + self._set_property(CODE_PROP, validated_code(v), pop_if_none=False) + + @classmethod + def get_schema_uri(cls) -> str: + return SCHEMA_URI + + @classmethod + def ext(cls, obj: pystac.Item, add_if_missing: bool = False) -> "GridExtension": + """Extends the given STAC Object with properties from the :stac-ext:`Grid + Extension `. + + This extension can be applied to instances of :class:`~pystac.Item`. + + Raises: + + pystac.ExtensionTypeError : If an invalid object type is passed. + """ + if isinstance(obj, pystac.Item): + cls.validate_has_extension(obj, add_if_missing) + return GridExtension(obj) + else: + raise pystac.ExtensionTypeError( + f"Grid Extension does not apply to type '{type(obj).__name__}'" + ) + + +class GridExtensionHooks(ExtensionHooks): + schema_uri: str = SCHEMA_URI + prev_extension_ids: Set[str] = set() + stac_object_types = {pystac.STACObjectType.ITEM} + + +Grid_EXTENSION_HOOKS: ExtensionHooks = GridExtensionHooks() diff --git a/setup.cfg b/setup.cfg index ff51dc013..36013b9a7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,9 @@ [metadata] version = attr: pystac.version.__version__ + +[tool:pytest] +minversion = 6.0 +addopts = -ra -q +testpaths = + tests +asyncio_mode = auto \ No newline at end of file diff --git a/tests/data-files/examples/1.0.0/example-sentinel2.json b/tests/data-files/examples/1.0.0/example-sentinel2.json new file mode 100644 index 000000000..7d6bf667e --- /dev/null +++ b/tests/data-files/examples/1.0.0/example-sentinel2.json @@ -0,0 +1,818 @@ +{ + "type": "Feature", + "stac_version": "1.0.0", + "id": "S2A_MSIL1C_20210908T042701_R133_T46RER_20210908T070248", + "properties": { + "providers": [ + { + "name": "ESA", + "roles": [ + "producer", + "processor", + "licensor" + ], + "url": "https://earth.esa.int/web/guest/home" + } + ], + "platform": "sentinel-2a", + "constellation": "sentinel-2", + "instruments": [ + "msi" + ], + "eo:cloud_cover": 88.2972, + "sat:orbit_state": "descending", + "sat:relative_orbit": 133, + "proj:epsg": 32646, + "mgrs:utm_zone": 46, + "mgrs:latitude_band": "R", + "mgrs:grid_square": "ER", + "grid:code": "MGRS-46RER", + "view:sun_azimuth": 142.987598836457, + "view:sun_elevation": 63.5068357330561, + "sentinel2:product_uri": "S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE", + "sentinel2:generation_time": "2021-09-08T07:02:48.000000Z", + "sentinel2:processing_baseline": "03.01", + "sentinel2:product_type": "S2MSI1C", + "sentinel2:datatake_id": "GS2A_20210908T042701_032448_N03.01", + "sentinel2:datatake_type": "INS-NOBS", + "sentinel2:datastrip_id": "S2A_OPER_MSI_L1C_DS_VGS4_20210908T070248_S20210908T043714_N03.01", + "sentinel2:granule_id": "S2A_OPER_MSI_L1C_TL_VGS4_20210908T070248_A032448_T46RER_N03.01", + "sentinel2:mgrs_tile": "46RER", + "sentinel2:reflectance_conversion_factor": 0.983841990384341, + "sentinel2:degraded_msi_data_percentage": 0.0, + "datetime": "2021-09-08T04:27:01.024000Z" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 93.43341763893636, + 28.023673957735582 + ], + [ + 93.40196468575007, + 27.9143684592852 + ], + [ + 93.36091831507485, + 27.766687360630367 + ], + [ + 93.31695738599002, + 27.619769241040945 + ], + [ + 93.2783823780312, + 27.471306867624268 + ], + [ + 93.23722635567444, + 27.32359772615637 + ], + [ + 93.19755659090528, + 27.17555811144318 + ], + [ + 93.16090624762064, + 27.033537745066344 + ], + [ + 92.99979835697575, + 27.03417096113827 + ], + [ + 92.99979654001324, + 28.025435531419042 + ], + [ + 93.43341763893636, + 28.023673957735582 + ] + ] + ] + }, + "links": [ + { + "rel": "license", + "href": "https://sentinel.esa.int/documents/247904/690755/Sentinel_Data_Legal_Notice" + }, + { + "rel": "self", + "href": "/var/folders/3q/jbg6x0zx3194zq6_2jbwygjw0000gn/T/tmp9ewihdpa/S2A_MSIL1C_20210908T042701_R133_T46RER_20210908T070248.json", + "type": "application/json" + } + ], + "assets": { + "coastal": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B01.jp2", + "type": "image/jp2", + "title": "Coastal aerosol (band 1) - 60m", + "eo:bands": [ + { + "name": "coastal", + "common_name": "coastal", + "description": "Coastal aerosol (band 1)", + "center_wavelength": 0.443, + "full_width_half_max": 0.027 + } + ], + "gsd": 60, + "proj:shape": [ + 1830, + 1830 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 60.0, + 0.0, + 499980.0, + 0.0, + -60.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 60, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "blue": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B02.jp2", + "type": "image/jp2", + "title": "Blue (band 2) - 10m", + "eo:bands": [ + { + "name": "blue", + "common_name": "blue", + "description": "Blue (band 2)", + "center_wavelength": 0.49, + "full_width_half_max": 0.098 + } + ], + "gsd": 10, + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "green": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B03.jp2", + "type": "image/jp2", + "title": "Green (band 3) - 10m", + "eo:bands": [ + { + "name": "green", + "common_name": "green", + "description": "Green (band 3)", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + } + ], + "gsd": 10, + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "red": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B04.jp2", + "type": "image/jp2", + "title": "Red (band 4) - 10m", + "eo:bands": [ + { + "name": "red", + "common_name": "red", + "description": "Red (band 4)", + "center_wavelength": 0.665, + "full_width_half_max": 0.038 + } + ], + "gsd": 10, + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "rededge1": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B05.jp2", + "type": "image/jp2", + "title": "Red edge 1 (band 5) - 20m", + "eo:bands": [ + { + "name": "rededge1", + "common_name": "rededge", + "description": "Red edge 1 (band 5)", + "center_wavelength": 0.704, + "full_width_half_max": 0.019 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "rededge2": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B06.jp2", + "type": "image/jp2", + "title": "Red edge 2 (band 6) - 20m", + "eo:bands": [ + { + "name": "rededge2", + "common_name": "rededge", + "description": "Red edge 2 (band 6)", + "center_wavelength": 0.74, + "full_width_half_max": 0.018 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "rededge3": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B07.jp2", + "type": "image/jp2", + "title": "Red edge 3 (band 7) - 20m", + "eo:bands": [ + { + "name": "rededge3", + "common_name": "rededge", + "description": "Red edge 3 (band 7)", + "center_wavelength": 0.783, + "full_width_half_max": 0.028 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "nir": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B08.jp2", + "type": "image/jp2", + "title": "NIR 1 (band 8) - 10m", + "eo:bands": [ + { + "name": "nir", + "common_name": "nir", + "description": "NIR 1 (band 8)", + "center_wavelength": 0.842, + "full_width_half_max": 0.145 + } + ], + "gsd": 10, + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "nir08": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B8A.jp2", + "type": "image/jp2", + "title": "NIR 2 (band 8A) - 20m", + "eo:bands": [ + { + "name": "nir08", + "common_name": "nir08", + "description": "NIR 2 (band 8A)", + "center_wavelength": 0.865, + "full_width_half_max": 0.033 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "nir09": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B09.jp2", + "type": "image/jp2", + "title": "NIR 3 (band 9) - 60m", + "eo:bands": [ + { + "name": "nir09", + "common_name": "nir09", + "description": "NIR 3 (band 9)", + "center_wavelength": 0.945, + "full_width_half_max": 0.026 + } + ], + "gsd": 60, + "proj:shape": [ + 1830, + 1830 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 60.0, + 0.0, + 499980.0, + 0.0, + -60.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 60, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "cirrus": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B10.jp2", + "type": "image/jp2", + "title": "Cirrus (band 10) - 60m", + "eo:bands": [ + { + "name": "cirrus", + "common_name": "cirrus", + "description": "Cirrus (band 10)", + "center_wavelength": 1.3735, + "full_width_half_max": 0.075 + } + ], + "gsd": 60, + "proj:shape": [ + 1830, + 1830 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 60.0, + 0.0, + 499980.0, + 0.0, + -60.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 60, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "swir16": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B11.jp2", + "type": "image/jp2", + "title": "SWIR 1 (band 11) - 20m", + "eo:bands": [ + { + "name": "swir16", + "common_name": "swir16", + "description": "SWIR 1 (band 11)", + "center_wavelength": 1.61, + "full_width_half_max": 0.143 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "swir22": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B12.jp2", + "type": "image/jp2", + "title": "SWIR 2 (band 12) - 20m", + "eo:bands": [ + { + "name": "swir22", + "common_name": "swir22", + "description": "SWIR 2 (band 12)", + "center_wavelength": 2.19, + "full_width_half_max": 0.242 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "visual": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_TCI.jp2", + "type": "image/jp2", + "title": "True color image", + "eo:bands": [ + { + "name": "red", + "common_name": "red", + "description": "Red (band 4)", + "center_wavelength": 0.665, + "full_width_half_max": 0.038 + }, + { + "name": "green", + "common_name": "green", + "description": "Green (band 3)", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + }, + { + "name": "blue", + "common_name": "blue", + "description": "Blue (band 2)", + "center_wavelength": 0.49, + "full_width_half_max": 0.098 + } + ], + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "roles": [ + "visual" + ] + }, + "safe_manifest": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/manifest.safe", + "type": "application/xml", + "roles": [ + "metadata" + ] + }, + "product_metadata": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/MTD_MSIL1C.xml", + "type": "application/xml", + "roles": [ + "metadata" + ] + }, + "granule_metadata": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/MTD_TL.xml", + "type": "application/xml", + "roles": [ + "metadata" + ] + }, + "inspire_metadata": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/INSPIRE.xml", + "type": "application/xml", + "roles": [ + "metadata" + ] + }, + "datastrip_metadata": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/DATASTRIP/DS_VGS4_20210908T070248_S20210908T043714/MTD_DS.xml", + "type": "application/xml", + "roles": [ + "metadata" + ] + } + }, + "bbox": [ + 92.99979654001324, + 27.033537745066344, + 93.43341763893636, + 28.025435531419042 + ], + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.0.0/schema.json", + "https://stac-extensions.github.io/sat/v1.0.0/schema.json", + "https://stac-extensions.github.io/projection/v1.0.0/schema.json", + "https://stac-extensions.github.io/mgrs/v1.0.0/schema.json", + "https://stac-extensions.github.io/grid/v1.0.0/schema.json", + "https://stac-extensions.github.io/view/v1.0.0/schema.json" + ] +} \ No newline at end of file diff --git a/tests/data-files/examples/example-info.csv b/tests/data-files/examples/example-info.csv index 4e980338e..faf65b02a 100644 --- a/tests/data-files/examples/example-info.csv +++ b/tests/data-files/examples/example-info.csv @@ -119,6 +119,7 @@ "1.0.0/collection.json","Collection","1.0.0","https://stac-extensions.github.io/eo/v1.0.0/schema.json|https://stac-extensions.github.io/view/v1.0.0/schema.json" "1.0.0/collectionless-item.json","Feature","1.0.0","https://stac-extensions.github.io/eo/v1.0.0/schema.json|https://stac-extensions.github.io/view/v1.0.0/schema.json" "1.0.0/core-item.json","Feature","1.0.0","" +"1.0.0/example-sentinel2.json","Feature","1.0.0","https://stac-extensions.github.io/eo/v1.0.0/schema.json|https://stac-extensions.github.io/sat/v1.0.0/schema.json|https://stac-extensions.github.io/projection/v1.0.0/schema.json|https://stac-extensions.github.io/mgrs/v1.0.0/schema.json|https://stac-extensions.github.io/grid/v1.0.0/schema.json|https://stac-extensions.github.io/view/v1.0.0/schema.json" "1.0.0/extended-item.json","Feature","1.0.0","https://stac-extensions.github.io/eo/v1.0.0/schema.json|https://stac-extensions.github.io/projection/v1.0.0/schema.json|https://stac-extensions.github.io/scientific/v1.0.0/schema.json|https://stac-extensions.github.io/view/v1.0.0/schema.json|https://stac-extensions.github.io/remote-data/v1.0.0/schema.json" "1.0.0/extensions-collection/collection.json","Collection","1.0.0","" "1.0.0/extensions-collection/proj-example/proj-example.json","Feature","1.0.0","https://stac-extensions.github.io/eo/v1.0.0/schema.json|https://stac-extensions.github.io/projection/v1.0.0/schema.json" diff --git a/tests/data-files/grid/example-sentinel2.json b/tests/data-files/grid/example-sentinel2.json new file mode 100644 index 000000000..7d6bf667e --- /dev/null +++ b/tests/data-files/grid/example-sentinel2.json @@ -0,0 +1,818 @@ +{ + "type": "Feature", + "stac_version": "1.0.0", + "id": "S2A_MSIL1C_20210908T042701_R133_T46RER_20210908T070248", + "properties": { + "providers": [ + { + "name": "ESA", + "roles": [ + "producer", + "processor", + "licensor" + ], + "url": "https://earth.esa.int/web/guest/home" + } + ], + "platform": "sentinel-2a", + "constellation": "sentinel-2", + "instruments": [ + "msi" + ], + "eo:cloud_cover": 88.2972, + "sat:orbit_state": "descending", + "sat:relative_orbit": 133, + "proj:epsg": 32646, + "mgrs:utm_zone": 46, + "mgrs:latitude_band": "R", + "mgrs:grid_square": "ER", + "grid:code": "MGRS-46RER", + "view:sun_azimuth": 142.987598836457, + "view:sun_elevation": 63.5068357330561, + "sentinel2:product_uri": "S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE", + "sentinel2:generation_time": "2021-09-08T07:02:48.000000Z", + "sentinel2:processing_baseline": "03.01", + "sentinel2:product_type": "S2MSI1C", + "sentinel2:datatake_id": "GS2A_20210908T042701_032448_N03.01", + "sentinel2:datatake_type": "INS-NOBS", + "sentinel2:datastrip_id": "S2A_OPER_MSI_L1C_DS_VGS4_20210908T070248_S20210908T043714_N03.01", + "sentinel2:granule_id": "S2A_OPER_MSI_L1C_TL_VGS4_20210908T070248_A032448_T46RER_N03.01", + "sentinel2:mgrs_tile": "46RER", + "sentinel2:reflectance_conversion_factor": 0.983841990384341, + "sentinel2:degraded_msi_data_percentage": 0.0, + "datetime": "2021-09-08T04:27:01.024000Z" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 93.43341763893636, + 28.023673957735582 + ], + [ + 93.40196468575007, + 27.9143684592852 + ], + [ + 93.36091831507485, + 27.766687360630367 + ], + [ + 93.31695738599002, + 27.619769241040945 + ], + [ + 93.2783823780312, + 27.471306867624268 + ], + [ + 93.23722635567444, + 27.32359772615637 + ], + [ + 93.19755659090528, + 27.17555811144318 + ], + [ + 93.16090624762064, + 27.033537745066344 + ], + [ + 92.99979835697575, + 27.03417096113827 + ], + [ + 92.99979654001324, + 28.025435531419042 + ], + [ + 93.43341763893636, + 28.023673957735582 + ] + ] + ] + }, + "links": [ + { + "rel": "license", + "href": "https://sentinel.esa.int/documents/247904/690755/Sentinel_Data_Legal_Notice" + }, + { + "rel": "self", + "href": "/var/folders/3q/jbg6x0zx3194zq6_2jbwygjw0000gn/T/tmp9ewihdpa/S2A_MSIL1C_20210908T042701_R133_T46RER_20210908T070248.json", + "type": "application/json" + } + ], + "assets": { + "coastal": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B01.jp2", + "type": "image/jp2", + "title": "Coastal aerosol (band 1) - 60m", + "eo:bands": [ + { + "name": "coastal", + "common_name": "coastal", + "description": "Coastal aerosol (band 1)", + "center_wavelength": 0.443, + "full_width_half_max": 0.027 + } + ], + "gsd": 60, + "proj:shape": [ + 1830, + 1830 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 60.0, + 0.0, + 499980.0, + 0.0, + -60.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 60, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "blue": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B02.jp2", + "type": "image/jp2", + "title": "Blue (band 2) - 10m", + "eo:bands": [ + { + "name": "blue", + "common_name": "blue", + "description": "Blue (band 2)", + "center_wavelength": 0.49, + "full_width_half_max": 0.098 + } + ], + "gsd": 10, + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "green": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B03.jp2", + "type": "image/jp2", + "title": "Green (band 3) - 10m", + "eo:bands": [ + { + "name": "green", + "common_name": "green", + "description": "Green (band 3)", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + } + ], + "gsd": 10, + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "red": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B04.jp2", + "type": "image/jp2", + "title": "Red (band 4) - 10m", + "eo:bands": [ + { + "name": "red", + "common_name": "red", + "description": "Red (band 4)", + "center_wavelength": 0.665, + "full_width_half_max": 0.038 + } + ], + "gsd": 10, + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "rededge1": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B05.jp2", + "type": "image/jp2", + "title": "Red edge 1 (band 5) - 20m", + "eo:bands": [ + { + "name": "rededge1", + "common_name": "rededge", + "description": "Red edge 1 (band 5)", + "center_wavelength": 0.704, + "full_width_half_max": 0.019 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "rededge2": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B06.jp2", + "type": "image/jp2", + "title": "Red edge 2 (band 6) - 20m", + "eo:bands": [ + { + "name": "rededge2", + "common_name": "rededge", + "description": "Red edge 2 (band 6)", + "center_wavelength": 0.74, + "full_width_half_max": 0.018 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "rededge3": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B07.jp2", + "type": "image/jp2", + "title": "Red edge 3 (band 7) - 20m", + "eo:bands": [ + { + "name": "rededge3", + "common_name": "rededge", + "description": "Red edge 3 (band 7)", + "center_wavelength": 0.783, + "full_width_half_max": 0.028 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "nir": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B08.jp2", + "type": "image/jp2", + "title": "NIR 1 (band 8) - 10m", + "eo:bands": [ + { + "name": "nir", + "common_name": "nir", + "description": "NIR 1 (band 8)", + "center_wavelength": 0.842, + "full_width_half_max": 0.145 + } + ], + "gsd": 10, + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "nir08": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B8A.jp2", + "type": "image/jp2", + "title": "NIR 2 (band 8A) - 20m", + "eo:bands": [ + { + "name": "nir08", + "common_name": "nir08", + "description": "NIR 2 (band 8A)", + "center_wavelength": 0.865, + "full_width_half_max": 0.033 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "nir09": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B09.jp2", + "type": "image/jp2", + "title": "NIR 3 (band 9) - 60m", + "eo:bands": [ + { + "name": "nir09", + "common_name": "nir09", + "description": "NIR 3 (band 9)", + "center_wavelength": 0.945, + "full_width_half_max": 0.026 + } + ], + "gsd": 60, + "proj:shape": [ + 1830, + 1830 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 60.0, + 0.0, + 499980.0, + 0.0, + -60.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 60, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "cirrus": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B10.jp2", + "type": "image/jp2", + "title": "Cirrus (band 10) - 60m", + "eo:bands": [ + { + "name": "cirrus", + "common_name": "cirrus", + "description": "Cirrus (band 10)", + "center_wavelength": 1.3735, + "full_width_half_max": 0.075 + } + ], + "gsd": 60, + "proj:shape": [ + 1830, + 1830 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 60.0, + 0.0, + 499980.0, + 0.0, + -60.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 60, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "swir16": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B11.jp2", + "type": "image/jp2", + "title": "SWIR 1 (band 11) - 20m", + "eo:bands": [ + { + "name": "swir16", + "common_name": "swir16", + "description": "SWIR 1 (band 11)", + "center_wavelength": 1.61, + "full_width_half_max": 0.143 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "swir22": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B12.jp2", + "type": "image/jp2", + "title": "SWIR 2 (band 12) - 20m", + "eo:bands": [ + { + "name": "swir22", + "common_name": "swir22", + "description": "SWIR 2 (band 12)", + "center_wavelength": 2.19, + "full_width_half_max": 0.242 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "visual": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_TCI.jp2", + "type": "image/jp2", + "title": "True color image", + "eo:bands": [ + { + "name": "red", + "common_name": "red", + "description": "Red (band 4)", + "center_wavelength": 0.665, + "full_width_half_max": 0.038 + }, + { + "name": "green", + "common_name": "green", + "description": "Green (band 3)", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + }, + { + "name": "blue", + "common_name": "blue", + "description": "Blue (band 2)", + "center_wavelength": 0.49, + "full_width_half_max": 0.098 + } + ], + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "roles": [ + "visual" + ] + }, + "safe_manifest": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/manifest.safe", + "type": "application/xml", + "roles": [ + "metadata" + ] + }, + "product_metadata": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/MTD_MSIL1C.xml", + "type": "application/xml", + "roles": [ + "metadata" + ] + }, + "granule_metadata": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/MTD_TL.xml", + "type": "application/xml", + "roles": [ + "metadata" + ] + }, + "inspire_metadata": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/INSPIRE.xml", + "type": "application/xml", + "roles": [ + "metadata" + ] + }, + "datastrip_metadata": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/DATASTRIP/DS_VGS4_20210908T070248_S20210908T043714/MTD_DS.xml", + "type": "application/xml", + "roles": [ + "metadata" + ] + } + }, + "bbox": [ + 92.99979654001324, + 27.033537745066344, + 93.43341763893636, + 28.025435531419042 + ], + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.0.0/schema.json", + "https://stac-extensions.github.io/sat/v1.0.0/schema.json", + "https://stac-extensions.github.io/projection/v1.0.0/schema.json", + "https://stac-extensions.github.io/mgrs/v1.0.0/schema.json", + "https://stac-extensions.github.io/grid/v1.0.0/schema.json", + "https://stac-extensions.github.io/view/v1.0.0/schema.json" + ] +} \ No newline at end of file diff --git a/tests/extensions/test_grid.py b/tests/extensions/test_grid.py new file mode 100644 index 000000000..e2edb9610 --- /dev/null +++ b/tests/extensions/test_grid.py @@ -0,0 +1,122 @@ +"""Tests for pystac.extensions.grid.""" + +import datetime +from typing import Any, Dict +import unittest + +import pystac +from pystac import ExtensionTypeError +from pystac.extensions import grid +from pystac.extensions.grid import GridExtension +from tests.utils import TestCases + +code = "MGRS-4CFJ" + + +def make_item() -> pystac.Item: + """Create basic test items that are only slightly different.""" + asset_id = "an/asset" + start = datetime.datetime(2018, 1, 2) + item = pystac.Item( + id=asset_id, geometry=None, bbox=None, datetime=start, properties={} + ) + + GridExtension.add_to(item) + return item + + +class GridTest(unittest.TestCase): + def setUp(self) -> None: + super().setUp() + self.item = make_item() + self.sentinel_example_uri = TestCases.get_path( + "data-files/grid/example-sentinel2.json" + ) + + def test_stac_extensions(self) -> None: + self.assertTrue(GridExtension.has_extension(self.item)) + + def test_item_repr(self) -> None: + grid_item_ext = GridExtension.ext(self.item) + self.assertEqual( + f"", grid_item_ext.__repr__() + ) + + def test_attributes(self) -> None: + GridExtension.ext(self.item).apply(code) + self.assertEqual(code, GridExtension.ext(self.item).code) + self.item.validate() + + def test_invalid_code_value(self) -> None: + with self.assertRaises(ValueError): + GridExtension.ext(self.item).apply("not_a_valid_code") + + def test_modify(self) -> None: + GridExtension.ext(self.item).apply(code) + GridExtension.ext(self.item).apply(code + "a") + self.assertEqual(code + "a", GridExtension.ext(self.item).code) + self.item.validate() + + def test_from_dict(self) -> None: + d: Dict[str, Any] = { + "type": "Feature", + "stac_version": "1.0.0", + "id": "an/asset", + "properties": { + "grid:code": code, + "datetime": "2018-01-02T00:00:00Z", + }, + "geometry": None, + "links": [], + "assets": {}, + "stac_extensions": [GridExtension.get_schema_uri()], + } + item = pystac.Item.from_dict(d) + self.assertEqual(code, GridExtension.ext(item).code) + + def test_to_from_dict(self) -> None: + GridExtension.ext(self.item).apply(code) + d = self.item.to_dict() + self.assertEqual(code, d["properties"][grid.CODE_PROP]) + + item = pystac.Item.from_dict(d) + self.assertEqual(code, GridExtension.ext(item).code) + + def test_clear_code(self) -> None: + GridExtension.ext(self.item).apply(code) + + with self.assertRaises(ValueError): + GridExtension.ext(self.item).code = None + + def test_extension_not_implemented(self) -> None: + # Should raise exception if Item does not include extension URI + item = pystac.Item.from_file(self.sentinel_example_uri) + item.stac_extensions.remove(GridExtension.get_schema_uri()) + + with self.assertRaises(pystac.ExtensionNotImplemented): + _ = GridExtension.ext(item) + + # Should raise exception if owning Item does not include extension URI + item.properties["grid:code"] = None + + with self.assertRaises(pystac.ExtensionNotImplemented): + _ = GridExtension.ext(item) + + def test_item_ext_add_to(self) -> None: + item = pystac.Item.from_file(self.sentinel_example_uri) + item.stac_extensions.remove(GridExtension.get_schema_uri()) + self.assertNotIn(GridExtension.get_schema_uri(), item.stac_extensions) + + _ = GridExtension.ext(item, add_if_missing=True) + + self.assertIn(GridExtension.get_schema_uri(), item.stac_extensions) + + def test_should_raise_exception_when_passing_invalid_extension_object( + self, + ) -> None: + self.assertRaisesRegex( + ExtensionTypeError, + r"^Grid Extension does not apply to type 'object'$", + GridExtension.ext, + object(), + ) From 3dc78f8da3834f276cab28fceb3f96c19902ca73 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 May 2022 09:22:49 -0400 Subject: [PATCH 31/60] build(deps): bump types-python-dateutil from 2.8.10 to 2.8.14 (#797) Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.10 to 2.8.14. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-python-dateutil dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 7a73d78f8..d2b7a3764 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -8,7 +8,7 @@ jsonschema==4.4.0 coverage==6.3.2 doc8==0.11.1 -types-python-dateutil==2.8.10 +types-python-dateutil==2.8.14 types-orjson==3.6.2 pre-commit==2.18.1 From 98d25ad7c42363f7b687e4e3028b20f2a537e08f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 May 2022 11:37:13 -0400 Subject: [PATCH 32/60] build(deps): bump pre-commit from 2.18.1 to 2.19.0 (#802) Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.18.1 to 2.19.0. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v2.18.1...v2.19.0) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index d2b7a3764..18f8a4a3d 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -11,7 +11,7 @@ doc8==0.11.1 types-python-dateutil==2.8.14 types-orjson==3.6.2 -pre-commit==2.18.1 +pre-commit==2.19.0 # optional dependencies orjson==3.6.8 From 87534736a4318e0b1b3f5d619912425128034438 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 May 2022 11:37:53 -0400 Subject: [PATCH 33/60] build(deps): bump jsonschema from 4.4.0 to 4.5.1 (#803) Bumps [jsonschema](https://github.com/python-jsonschema/jsonschema) from 4.4.0 to 4.5.1. - [Release notes](https://github.com/python-jsonschema/jsonschema/releases) - [Changelog](https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst) - [Commits](https://github.com/python-jsonschema/jsonschema/compare/v4.4.0...v4.5.1) --- updated-dependencies: - dependency-name: jsonschema dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 18f8a4a3d..bcb7e11bd 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,7 +4,7 @@ black==22.3.0 codespell==2.1.0 -jsonschema==4.4.0 +jsonschema==4.5.1 coverage==6.3.2 doc8==0.11.1 From 008d83caa93e7f773833ba03454fe46ec9176353 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 May 2022 10:21:54 -0400 Subject: [PATCH 34/60] build(deps): bump types-python-dateutil from 2.8.14 to 2.8.15 (#804) Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.14 to 2.8.15. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-python-dateutil dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index bcb7e11bd..2738e5614 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -8,7 +8,7 @@ jsonschema==4.5.1 coverage==6.3.2 doc8==0.11.1 -types-python-dateutil==2.8.14 +types-python-dateutil==2.8.15 types-orjson==3.6.2 pre-commit==2.19.0 From 3526b651a9d0686542efa08b6dc83e55cbd194a4 Mon Sep 17 00:00:00 2001 From: Dahn Date: Fri, 13 May 2022 04:04:17 +0200 Subject: [PATCH 35/60] Optional HTML representations for Jupyter Notebook (#743) * Add rudimentary optional _repr_html_() Starting point is https://github.com/stac-utils/pystac/pull/573 Improvements - Jinja dependency is optional, otherwise __repr__ is used - Catalog shows children - get_items() is kept lazy - Minor improvements in the Jinja templates * Refactor Jinja templates, Assets show title - Use macros to simplify Jinja templates - Assets show title before expansion * All pre-commit checks pass * Jinja templates are distributed with the package Tested by building a wheel (python -m build) after a pip install build creating a new virtualenv installing from the built wheel in the new virtualenv installing jupyter notebooks and testing the html repr * Jinja is imported lazily at the first _repr_html_ call Previously, importing pystac would attempt to import jinja This means jinja would be imported even if the html repr was never used * REF: Separate out Catalog and Collection templates * Style: All list s have identical margins * Refactor: Collection template inherits from Catalog * Remove unused parameter * Add provider template * Catalog-derived templates use class name as title * Add PR link to changelog * WIP: HTML repr does not retrieve all children&items * Remove accidentally commited notebook * Formatting (black) * Refactor: Limit try/except scope * Mypy ignores optional dependency jinja2 jinja2 is used for optional html_repr for Jupyter Notebooks and should thus not be required * Moving unreleased PR from 1.4.0 to unreleased * ItemCollection implements _repr_html_ * ItemCollection only shows at most 10 items ItemCollection could theoretically have hundreds or thousands of items which shouldn't all be shown in the HTML representation at once Co-authored-by: Jon Duckworth Co-authored-by: Pete Gadomski --- CHANGELOG.md | 1 + mypy.ini | 3 + pystac/asset.py | 10 ++++ pystac/catalog.py | 10 ++++ pystac/collection.py | 10 ++++ pystac/html/Asset.jinja2 | 31 +++++++++++ pystac/html/Catalog.jinja2 | 30 ++++++++++ pystac/html/Collection.jinja2 | 20 +++++++ pystac/html/Item.jinja2 | 33 +++++++++++ pystac/html/ItemCollection.jinja2 | 31 +++++++++++ pystac/html/Link.jinja2 | 23 ++++++++ pystac/html/Macros.jinja2 | 93 +++++++++++++++++++++++++++++++ pystac/html/Provider.jinja2 | 12 ++++ pystac/html/__init__.py | 3 + pystac/html/jinja_env.py | 19 +++++++ pystac/item.py | 10 ++++ pystac/item_collection.py | 10 ++++ pystac/link.py | 10 ++++ pystac/provider.py | 10 ++++ setup.py | 2 +- 20 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 pystac/html/Asset.jinja2 create mode 100644 pystac/html/Catalog.jinja2 create mode 100644 pystac/html/Collection.jinja2 create mode 100644 pystac/html/Item.jinja2 create mode 100644 pystac/html/ItemCollection.jinja2 create mode 100644 pystac/html/Link.jinja2 create mode 100644 pystac/html/Macros.jinja2 create mode 100644 pystac/html/Provider.jinja2 create mode 100644 pystac/html/__init__.py create mode 100644 pystac/html/jinja_env.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ab25b0aad..c68d285dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Replace test.com with special-use domain name. ([#769](https://github.com/stac-utils/pystac/pull/769)) - Updated AssetDefinition to have create, apply methods ([#768](https://github.com/stac-utils/pystac/pull/768)) - Add Grid Extension support ([#799](https://github.com/stac-utils/pystac/pull/799)) +- Rich HTML representations for Jupyter Notebook display ([#743](https://github.com/stac-utils/pystac/pull/743)) ### Removed diff --git a/mypy.ini b/mypy.ini index ffdd91a23..90581decb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,6 +2,9 @@ show_error_codes = True strict = True +[mypy-jinja2.*] +ignore_missing_imports = True + [mypy-jsonschema.*] ignore_missing_imports = True diff --git a/pystac/asset.py b/pystac/asset.py index 2c9582a13..d027721f8 100644 --- a/pystac/asset.py +++ b/pystac/asset.py @@ -1,7 +1,9 @@ +from html import escape from copy import copy from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union from pystac import common_metadata +from pystac.html.jinja_env import get_jinja_env from pystac import utils if TYPE_CHECKING: @@ -156,6 +158,14 @@ def common_metadata(self) -> "CommonMetadata_Type": def __repr__(self) -> str: return "".format(self.href) + def _repr_html_(self) -> str: + jinja_env = get_jinja_env() + if jinja_env: + template = jinja_env.get_template("Asset.jinja2") + return str(template.render(asset=self)) + else: + return escape(repr(self)) + @classmethod def from_dict(cls, d: Dict[str, Any]) -> "Asset": """Constructs an Asset from a dict. diff --git a/pystac/catalog.py b/pystac/catalog.py index c9ab7114d..038032b43 100644 --- a/pystac/catalog.py +++ b/pystac/catalog.py @@ -1,6 +1,8 @@ import os +from html import escape from copy import deepcopy from pystac.errors import STACTypeError +from pystac.html.jinja_env import get_jinja_env from typing import ( Any, Callable, @@ -196,6 +198,14 @@ def __init__( def __repr__(self) -> str: return "".format(self.id) + def _repr_html_(self) -> str: + jinja_env = get_jinja_env() + if jinja_env: + template = jinja_env.get_template("Catalog.jinja2") + return str(template.render(catalog=self)) + else: + return escape(repr(self)) + def set_root(self, root: Optional["Catalog"]) -> None: STACObject.set_root(self, root) if root is not None: diff --git a/pystac/collection.py b/pystac/collection.py index 069367711..42d14ebfe 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -1,6 +1,8 @@ +from html import escape from copy import deepcopy from datetime import datetime from pystac.errors import STACTypeError +from pystac.html.jinja_env import get_jinja_env from typing import ( Any, Dict, @@ -525,6 +527,14 @@ def __init__( def __repr__(self) -> str: return "".format(self.id) + def _repr_html_(self) -> str: + jinja_env = get_jinja_env() + if jinja_env: + template = jinja_env.get_template("Collection.jinja2") + return str(template.render(catalog=self)) + else: + return escape(repr(self)) + def add_item( self, item: "Item_Type", diff --git a/pystac/html/Asset.jinja2 b/pystac/html/Asset.jinja2 new file mode 100644 index 000000000..f771db01a --- /dev/null +++ b/pystac/html/Asset.jinja2 @@ -0,0 +1,31 @@ +{% import 'Macros.jinja2' as macros %} + + diff --git a/pystac/html/Catalog.jinja2 b/pystac/html/Catalog.jinja2 new file mode 100644 index 000000000..b47125d7f --- /dev/null +++ b/pystac/html/Catalog.jinja2 @@ -0,0 +1,30 @@ +{% import 'Macros.jinja2' as macros %} + + \ No newline at end of file diff --git a/pystac/html/Collection.jinja2 b/pystac/html/Collection.jinja2 new file mode 100644 index 000000000..6bb12e664 --- /dev/null +++ b/pystac/html/Collection.jinja2 @@ -0,0 +1,20 @@ +{% extends 'Catalog.jinja2' %} +{% import 'Macros.jinja2' as macros %} + +{% block subclass_fields %} + {# Providers field #} + {% if catalog.providers %} + Providers: +
    + {% for provider in catalog.providers %} +
  • {{ provider._repr_html_() }}
  • + {% endfor %} +
+ + {% endif %} +{% endblock %} + +{% block subclass_details %} + {{ macros.assets(catalog) }} +{% endblock %} + diff --git a/pystac/html/Item.jinja2 b/pystac/html/Item.jinja2 new file mode 100644 index 000000000..3a556b63c --- /dev/null +++ b/pystac/html/Item.jinja2 @@ -0,0 +1,33 @@ +{% import 'Macros.jinja2' as macros %} + + \ No newline at end of file diff --git a/pystac/html/ItemCollection.jinja2 b/pystac/html/ItemCollection.jinja2 new file mode 100644 index 000000000..20835fe29 --- /dev/null +++ b/pystac/html/ItemCollection.jinja2 @@ -0,0 +1,31 @@ +{% import 'Macros.jinja2' as macros %} + + diff --git a/pystac/html/Link.jinja2 b/pystac/html/Link.jinja2 new file mode 100644 index 000000000..5a495264e --- /dev/null +++ b/pystac/html/Link.jinja2 @@ -0,0 +1,23 @@ +{% import 'Macros.jinja2' as macros %} + + \ No newline at end of file diff --git a/pystac/html/Macros.jinja2 b/pystac/html/Macros.jinja2 new file mode 100644 index 000000000..63c79f8f0 --- /dev/null +++ b/pystac/html/Macros.jinja2 @@ -0,0 +1,93 @@ +{% macro square(background_color='#E1E1E1', border_color='#9D9D9D') -%} +
+
+{%- endmacro %} + + +{% macro extra_fields(parent) -%} + {% if parent.extra_fields %} + {% for key, value in parent.extra_fields.items() %} + {{ key }}: {{ value }} + {% endfor %} + {% endif %} +{%- endmacro %} + + +{% macro links(parent) -%} + {% if parent.links|length > 0 %} +
+ +

Links

+
+ {% for link in parent.links %} + {{ link._repr_html_() }} + {% endfor %} +
+ {% endif %} +{%- endmacro %} + + +{% macro items(parent) -%} + {% if parent.get_items()|is_nonempty_generator %} +
+ +

Items

+
+ Only the first item shown + {% for item in parent.get_items()|first %} + {{ item._repr_html_() }} + {% endfor %} +
+ {% endif %} +{%- endmacro %} + + +{% macro children(parent) -%} + {% if parent.get_children()|is_nonempty_generator %} +
+ +

Children

+
+ Only the first child shown + {% for child in parent.get_children()|first %} + {{ child._repr_html_() }} + {% endfor %} +
+ {% endif %} +{%- endmacro %} + + +{% macro stac_extensions(parent) -%} + {% if parent.stac_extensions|length > 0 %} +
+ +

STAC Extensions

+
+ + {% for stac_extension in parent.stac_extensions %} + + {% endfor %} +
{{stac_extension}}
+
+ {% endif %} +{%- endmacro %} + + +{% macro assets(parent) -%} + {% if parent.assets|length > 0 %} +
+ +

Assets

+
+ {% for key, asset in parent.assets.items() %} + {{ asset._repr_html_() }} + {% endfor %} +
+ {% endif %} +{%- endmacro %} \ No newline at end of file diff --git a/pystac/html/Provider.jinja2 b/pystac/html/Provider.jinja2 new file mode 100644 index 000000000..ddca00662 --- /dev/null +++ b/pystac/html/Provider.jinja2 @@ -0,0 +1,12 @@ +{% import 'Macros.jinja2' as macros %} + +
+ {% if provider.url %} + {{ provider.name }} + {% else %} + {{ provider.name }} + {% endif %} + ({{ provider.roles|join(", ") }}){% if provider.description %}: {{ provider.description }} {% endif %} + {{ macros.extra_fields(provider) }} +
+ diff --git a/pystac/html/__init__.py b/pystac/html/__init__.py new file mode 100644 index 000000000..25947a849 --- /dev/null +++ b/pystac/html/__init__.py @@ -0,0 +1,3 @@ +from .jinja_env import get_jinja_env + +__all__ = ["get_jinja_env"] diff --git a/pystac/html/jinja_env.py b/pystac/html/jinja_env.py new file mode 100644 index 000000000..f2389789b --- /dev/null +++ b/pystac/html/jinja_env.py @@ -0,0 +1,19 @@ +from functools import lru_cache +from itertools import islice + + +@lru_cache() +def get_jinja_env(): # type: ignore + try: + from jinja2 import Environment, PackageLoader, select_autoescape + except ModuleNotFoundError: + return None + + environment = Environment( + loader=PackageLoader("pystac", "html"), autoescape=select_autoescape() + ) + + environment.filters["first"] = lambda x: islice(x, 1) + environment.filters["is_nonempty_generator"] = lambda x: next(x, None) is not None + + return environment diff --git a/pystac/item.py b/pystac/item.py index b9730acba..46a8948de 100644 --- a/pystac/item.py +++ b/pystac/item.py @@ -1,3 +1,4 @@ +from html import escape from copy import copy, deepcopy from datetime import datetime as Datetime from pystac.catalog import Catalog @@ -6,6 +7,7 @@ import dateutil.parser import pystac +from pystac.html.jinja_env import get_jinja_env from pystac import STACError, STACObjectType from pystac.asset import Asset from pystac.link import Link @@ -145,6 +147,14 @@ def __init__( def __repr__(self) -> str: return "".format(self.id) + def _repr_html_(self) -> str: + jinja_env = get_jinja_env() + if jinja_env: + template = jinja_env.get_template("Item.jinja2") + return str(template.render(item=self)) + else: + return escape(repr(self)) + def set_self_href(self, href: Optional[str]) -> None: """Sets the absolute HREF that is represented by the ``rel == 'self'`` :class:`~pystac.Link`. diff --git a/pystac/item_collection.py b/pystac/item_collection.py index de38ac34f..4ff373c16 100644 --- a/pystac/item_collection.py +++ b/pystac/item_collection.py @@ -1,8 +1,10 @@ from copy import deepcopy from pystac.errors import STACTypeError from typing import Any, Dict, Iterator, List, Optional, Collection, Iterable, Union +from html import escape import pystac +from pystac.html.jinja_env import get_jinja_env from pystac.utils import make_absolute_href, is_absolute_href from pystac.serialization.identify import identify_stac_object_type @@ -235,3 +237,11 @@ def is_item_collection(d: Dict[str, Any]) -> bool: identify_stac_object_type(feature) == pystac.STACObjectType.ITEM for feature in d.get("features", []) ) + + def _repr_html_(self) -> str: + jinja_env = get_jinja_env() + if jinja_env: + template = jinja_env.get_template("ItemCollection.jinja2") + return str(template.render(item_collection=self)) + else: + return escape(repr(self)) diff --git a/pystac/link.py b/pystac/link.py index 9035848b7..57b69b4da 100644 --- a/pystac/link.py +++ b/pystac/link.py @@ -1,8 +1,10 @@ import os from copy import copy +from html import escape from typing import Any, Dict, Optional, TYPE_CHECKING, Union import pystac +from pystac.html.jinja_env import get_jinja_env from pystac.utils import make_absolute_href, make_relative_href, is_absolute_href if TYPE_CHECKING: @@ -254,6 +256,14 @@ def __fspath__(self) -> str: def __repr__(self) -> str: return "".format(self.rel, self.target) + def _repr_html_(self) -> str: + jinja_env = get_jinja_env() + if jinja_env: + template = jinja_env.get_template("Link.jinja2") + return str(template.render(link=self)) + else: + return escape(repr(self)) + def resolve_stac_object(self, root: Optional["Catalog_Type"] = None) -> "Link": """Resolves a STAC object from the HREF of this link, if the link is not already resolved. diff --git a/pystac/provider.py b/pystac/provider.py index d03d59c28..0622433d0 100644 --- a/pystac/provider.py +++ b/pystac/provider.py @@ -1,5 +1,7 @@ from typing import Any, Dict, List, Optional +from html import escape +from pystac.html.jinja_env import get_jinja_env from pystac.utils import StringEnum @@ -70,6 +72,14 @@ def __eq__(self, o: object) -> bool: return NotImplemented return self.to_dict() == o.to_dict() + def _repr_html_(self) -> str: + jinja_env = get_jinja_env() + if jinja_env: + template = jinja_env.get_template("Provider.jinja2") + return str(template.render(provider=self)) + else: + return escape(repr(self)) + def to_dict(self) -> Dict[str, Any]: """Generate a dictionary representing the JSON of this Provider. diff --git a/setup.py b/setup.py index 1bc077b2e..ca4065801 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ author_email="stac@radiant.earth", url="https://github.com/stac-utils/pystac", packages=find_packages(exclude=["tests*"]), - package_data={"": ["py.typed"]}, + package_data={"": ["py.typed", "*.jinja2"]}, py_modules=[splitext(basename(path))[0] for path in glob("pystac/*.py")], python_requires=">=3.7", install_requires=[ From 1261e6058e8e9137d5c7c7f16e0d18e888a6d9cf Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Tue, 17 May 2022 19:56:06 -0400 Subject: [PATCH 36/60] Return None from Asset.get_absolute_href when owning Item self HREF is NOne (#808) * Return None from Asset.get_absolute_href for owner with no self HREF * Remove unused mypy config * Update docstring --- mypy.ini | 3 --- pystac/asset.py | 10 ++++++---- tests/test_item.py | 16 +++++++++++++++- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/mypy.ini b/mypy.ini index 90581decb..5fcd1391e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -10,6 +10,3 @@ ignore_missing_imports = True [mypy-setuptools.*] ignore_missing_imports = True - -[mypy-sphinx.util] -ignore_missing_imports = True diff --git a/pystac/asset.py b/pystac/asset.py index d027721f8..7402054c9 100644 --- a/pystac/asset.py +++ b/pystac/asset.py @@ -92,7 +92,8 @@ def get_absolute_href(self) -> Optional[str]: """Gets the absolute href for this asset, if possible. If this Asset has no associated Item, and the asset HREF is a relative path, - this method will return None. + this method will return ``None``. If the Item that owns the Asset has no + self HREF, this will also return ``None``. Returns: str: The absolute HREF of this asset, or None if an absolute HREF could not @@ -102,9 +103,10 @@ def get_absolute_href(self) -> Optional[str]: return self.href else: if self.owner is not None: - return utils.make_absolute_href(self.href, self.owner.get_self_href()) - else: - return None + item_self = self.owner.get_self_href() + if item_self is not None: + return utils.make_absolute_href(self.href, item_self) + return None def to_dict(self) -> Dict[str, Any]: """Generate a dictionary representing the JSON of this Asset. diff --git a/tests/test_item.py b/tests/test_item.py index 904ac0be6..7788dbc4e 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -72,14 +72,28 @@ def test_set_self_href_none_ignores_relative_asset_hrefs(self) -> None: self.assertFalse(is_absolute_href(asset.href)) def test_asset_absolute_href(self) -> None: + item_path = TestCases.get_path("data-files/item/sample-item.json") item_dict = self.get_example_item_dict() item = Item.from_dict(item_dict) + item.set_self_href(item_path) rel_asset = Asset("./data.geojson") rel_asset.set_owner(item) - expected_href = os.path.abspath("./data.geojson") + expected_href = os.path.abspath( + os.path.join(os.path.dirname(item_path), "./data.geojson") + ) actual_href = rel_asset.get_absolute_href() self.assertEqual(expected_href, actual_href) + def test_asset_absolute_href_no_item_self(self) -> None: + item_dict = self.get_example_item_dict() + item = Item.from_dict(item_dict) + assert item.get_self_href() is None + + rel_asset = Asset("./data.geojson") + rel_asset.set_owner(item) + actual_href = rel_asset.get_absolute_href() + self.assertEqual(None, actual_href) + def test_extra_fields(self) -> None: item = pystac.Item.from_file( TestCases.get_path("data-files/item/sample-item.json") From ec14691994e2983a8a2dd47a6dee8692fa918464 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 13:42:07 -0400 Subject: [PATCH 37/60] build(deps): bump mypy from 0.950 to 0.961 (#821) Bumps [mypy](https://github.com/python/mypy) from 0.950 to 0.961. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.950...v0.961) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 2738e5614..6963c73e2 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -mypy==0.950 +mypy==0.961 flake8==4.0.1 black==22.3.0 From cf489305e38a437cd43c499dffc789a43c9bd70e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 13:45:28 -0400 Subject: [PATCH 38/60] build(deps): bump jsonschema from 4.5.1 to 4.6.0 (#822) Bumps [jsonschema](https://github.com/python-jsonschema/jsonschema) from 4.5.1 to 4.6.0. - [Release notes](https://github.com/python-jsonschema/jsonschema/releases) - [Changelog](https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst) - [Commits](https://github.com/python-jsonschema/jsonschema/compare/v4.5.1...v4.6.0) --- updated-dependencies: - dependency-name: jsonschema dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 6963c73e2..7006764ed 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,7 +4,7 @@ black==22.3.0 codespell==2.1.0 -jsonschema==4.5.1 +jsonschema==4.6.0 coverage==6.3.2 doc8==0.11.1 From a743dfc8cb4cdb9312f322410d8a75943e29c6c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 13:46:56 -0400 Subject: [PATCH 39/60] build(deps): bump ipython from 8.3.0 to 8.4.0 (#817) Bumps [ipython](https://github.com/ipython/ipython) from 8.3.0 to 8.4.0. - [Release notes](https://github.com/ipython/ipython/releases) - [Commits](https://github.com/ipython/ipython/compare/8.3.0...8.4.0) --- updated-dependencies: - dependency-name: ipython dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index f37028a6d..76d9f8c82 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,4 +1,4 @@ -ipython==8.3.0 +ipython==8.4.0 Sphinx==4.5.0 sphinxcontrib-fulltoc==1.2.0 nbsphinx==0.8.8 From 235a8a682f05a2e9cfb31ea3cd8a1676e919bf65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 13:48:23 -0400 Subject: [PATCH 40/60] build(deps): bump types-python-dateutil from 2.8.15 to 2.8.17 (#815) Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.15 to 2.8.17. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-python-dateutil dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 7006764ed..fe1849bc1 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -8,7 +8,7 @@ jsonschema==4.6.0 coverage==6.3.2 doc8==0.11.1 -types-python-dateutil==2.8.15 +types-python-dateutil==2.8.17 types-orjson==3.6.2 pre-commit==2.19.0 From ab57143a0d8c4ddfc6de3f93ff149707690e53d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 13:48:45 -0400 Subject: [PATCH 41/60] build(deps): bump doc8 from 0.11.1 to 0.11.2 (#810) Bumps [doc8](https://github.com/pycqa/doc8) from 0.11.1 to 0.11.2. - [Release notes](https://github.com/pycqa/doc8/releases) - [Commits](https://github.com/pycqa/doc8/compare/0.11.1...0.11.2) --- updated-dependencies: - dependency-name: doc8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index fe1849bc1..2d9494afa 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -6,7 +6,7 @@ codespell==2.1.0 jsonschema==4.6.0 coverage==6.3.2 -doc8==0.11.1 +doc8==0.11.2 types-python-dateutil==2.8.17 types-orjson==3.6.2 From 8220b4c1262b24d4fd7a49d2c9c886f81b8a5176 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 13:50:26 -0400 Subject: [PATCH 42/60] build(deps): bump coverage from 6.3.2 to 6.4.1 (#818) Bumps [coverage](https://github.com/nedbat/coveragepy) from 6.3.2 to 6.4.1. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/6.3.2...6.4.1) --- updated-dependencies: - dependency-name: coverage dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jon Duckworth --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 2d9494afa..51e954da7 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -5,7 +5,7 @@ black==22.3.0 codespell==2.1.0 jsonschema==4.6.0 -coverage==6.3.2 +coverage==6.4.1 doc8==0.11.2 types-python-dateutil==2.8.17 From 72870808e2d794098b485ff6110055f8755e4d8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 13:52:16 -0400 Subject: [PATCH 43/60] build(deps): bump nbsphinx from 0.8.8 to 0.8.9 (#823) Bumps [nbsphinx](https://github.com/spatialaudio/nbsphinx) from 0.8.8 to 0.8.9. - [Release notes](https://github.com/spatialaudio/nbsphinx/releases) - [Changelog](https://github.com/spatialaudio/nbsphinx/blob/master/NEWS.rst) - [Commits](https://github.com/spatialaudio/nbsphinx/compare/0.8.8...0.8.9) --- updated-dependencies: - dependency-name: nbsphinx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 76d9f8c82..ec5b5d99e 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,7 +1,7 @@ ipython==8.4.0 Sphinx==4.5.0 sphinxcontrib-fulltoc==1.2.0 -nbsphinx==0.8.8 +nbsphinx==0.8.9 pydata-sphinx-theme==0.8.1 sphinx-panels==0.6.0 jinja2<4.0 From 3c65cd98c0e979d4bd3faae7b6ea254920b31339 Mon Sep 17 00:00:00 2001 From: Matthias Mohr Date: Tue, 7 Jun 2022 20:13:57 +0200 Subject: [PATCH 44/60] Add html media type (#816) * Add html media type * Update CHANGELOG.md Co-authored-by: Jon Duckworth --- CHANGELOG.md | 1 + pystac/media_type.py | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c68d285dc..7cb0f079c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - Enum MediaType entry for PDF documents ([#758](https://github.com/stac-utils/pystac/pull/758)) +- Enum MediaType entry for HTML documents ([#816](https://github.com/stac-utils/pystac/pull/816)) - Updated Link to obtain stac_io from owner root ([#762](https://github.com/stac-utils/pystac/pull/762)) - Replace test.com with special-use domain name. ([#769](https://github.com/stac-utils/pystac/pull/769)) - Updated AssetDefinition to have create, apply methods ([#768](https://github.com/stac-utils/pystac/pull/768)) diff --git a/pystac/media_type.py b/pystac/media_type.py index 33f4d316a..e6e7c9b37 100644 --- a/pystac/media_type.py +++ b/pystac/media_type.py @@ -10,6 +10,7 @@ class MediaType(StringEnum): GEOTIFF = "image/tiff; application=geotiff" HDF = "application/x-hdf" # Hierarchical Data Format versions 4 and earlier. HDF5 = "application/x-hdf5" # Hierarchical Data Format version 5 + HTML = "text/html" JPEG = "image/jpeg" JPEG2000 = "image/jp2" JSON = "application/json" From 322125be08feee4078cf99af71789c646f556b21 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 14:55:10 -0400 Subject: [PATCH 45/60] build(deps): bump orjson from 3.6.8 to 3.7.2 (#825) Bumps [orjson](https://github.com/ijl/orjson) from 3.6.8 to 3.7.2. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.6.8...3.7.2) --- updated-dependencies: - dependency-name: orjson dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jon Duckworth --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 51e954da7..49dd59995 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -14,4 +14,4 @@ types-orjson==3.6.2 pre-commit==2.19.0 # optional dependencies -orjson==3.6.8 +orjson==3.7.2 From a476084d44eb20f5af7691cdbab04c5df9256830 Mon Sep 17 00:00:00 2001 From: Preston Hartzell Date: Sun, 19 Jun 2022 14:59:36 -0400 Subject: [PATCH 46/60] update raster extension to v1.1.0 (#809) * adds raster extension v1.1.0 - update schema uri - align nodata type with new schema: change nodata type from float to union of float and a StringEnum, where the StringEnum contains "nan", "inf", and "-inf" - update tests * update CHANGELOG Co-authored-by: Jon Duckworth --- CHANGELOG.md | 2 ++ pystac/extensions/raster.py | 16 +++++++++----- .../raster/raster-planet-example.json | 2 +- .../raster/raster-sentinel2-example.json | 9 +++++++- tests/extensions/test_raster.py | 21 ++++++++++++++++--- 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cb0f079c..c1168b10a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ ### Changed +- Updated Raster Extension from v1.0.0 to v1.1.0 ([#809](https://github.com/stac-utils/pystac/pull/809)) + ### Fixed - "How to create STAC catalogs" tutorial ([#775](https://github.com/stac-utils/pystac/pull/775)) diff --git a/pystac/extensions/raster.py b/pystac/extensions/raster.py index 6119b61e7..0c7623c24 100644 --- a/pystac/extensions/raster.py +++ b/pystac/extensions/raster.py @@ -10,7 +10,7 @@ ) from pystac.utils import StringEnum, get_opt, get_required, map_opt -SCHEMA_URI = "https://stac-extensions.github.io/raster/v1.0.0/schema.json" +SCHEMA_URI = "https://stac-extensions.github.io/raster/v1.1.0/schema.json" BANDS_PROP = "raster:bands" @@ -39,6 +39,12 @@ class DataType(StringEnum): OTHER = "other" +class NoDataStrings(StringEnum): + INF = "inf" + NINF = "-inf" + NAN = "nan" + + class Statistics: """Represents statistics information attached to a band in the raster extension. @@ -350,7 +356,7 @@ def __init__(self, properties: Dict[str, Any]) -> None: def apply( self, - nodata: Optional[float] = None, + nodata: Optional[Union[float, NoDataStrings]] = None, sampling: Optional[Sampling] = None, data_type: Optional[DataType] = None, bits_per_sample: Optional[float] = None, @@ -400,7 +406,7 @@ def apply( @classmethod def create( cls, - nodata: Optional[float] = None, + nodata: Optional[Union[float, NoDataStrings]] = None, sampling: Optional[Sampling] = None, data_type: Optional[DataType] = None, bits_per_sample: Optional[float] = None, @@ -452,7 +458,7 @@ def create( return b @property - def nodata(self) -> Optional[float]: + def nodata(self) -> Optional[Union[float, NoDataStrings]]: """Get or sets the nodata pixel value Returns: @@ -461,7 +467,7 @@ def nodata(self) -> Optional[float]: return self.properties.get("nodata") @nodata.setter - def nodata(self, v: Optional[float]) -> None: + def nodata(self, v: Optional[Union[float, NoDataStrings]]) -> None: if v is not None: self.properties["nodata"] = v else: diff --git a/tests/data-files/raster/raster-planet-example.json b/tests/data-files/raster/raster-planet-example.json index b85aea962..66fb39d3c 100644 --- a/tests/data-files/raster/raster-planet-example.json +++ b/tests/data-files/raster/raster-planet-example.json @@ -1245,6 +1245,6 @@ "https://stac-extensions.github.io/projection/v1.0.0/schema.json", "https://stac-extensions.github.io/eo/v1.0.0/schema.json", "https://stac-extensions.github.io/processing/v1.0.0/schema.json", - "https://stac-extensions.github.io/raster/v1.0.0/schema.json" + "https://stac-extensions.github.io/raster/v1.1.0/schema.json" ] } \ No newline at end of file diff --git a/tests/data-files/raster/raster-sentinel2-example.json b/tests/data-files/raster/raster-sentinel2-example.json index dbaaef625..807270404 100644 --- a/tests/data-files/raster/raster-sentinel2-example.json +++ b/tests/data-files/raster/raster-sentinel2-example.json @@ -548,6 +548,13 @@ "full_width_half_max": 0.026 } ], + "raster:bands": [ + { + "data_type": "uint16", + "spatial_resolution": 60, + "nodata": "nan" + } + ], "proj:shape": [ 1830, 1830 @@ -711,7 +718,7 @@ "https://stac-extensions.github.io/eo/v1.0.0/schema.json", "https://stac-extensions.github.io/view/v1.0.0/schema.json", "https://stac-extensions.github.io/projection/v1.0.0/schema.json", - "https://stac-extensions.github.io/raster/v1.0.0/schema.json" + "https://stac-extensions.github.io/raster/v1.1.0/schema.json" ], "virtual:assets": { "SIR": { diff --git a/tests/extensions/test_raster.py b/tests/extensions/test_raster.py index 677929c6b..423bba66e 100644 --- a/tests/extensions/test_raster.py +++ b/tests/extensions/test_raster.py @@ -6,6 +6,7 @@ from pystac.utils import get_opt from pystac.extensions.raster import ( Histogram, + NoDataStrings, RasterExtension, RasterBand, Sampling, @@ -41,6 +42,7 @@ def test_validate_raster(self) -> None: def test_asset_bands(self) -> None: item = pystac.Item.from_file(self.PLANET_EXAMPLE_URI) + item2 = pystac.Item.from_file(self.SENTINEL2_EXAMPLE_URI) # Get data_asset = item.assets["data"] @@ -74,8 +76,12 @@ def test_asset_bands(self) -> None: asset_bands = RasterExtension.ext(index_asset).bands self.assertIs(None, asset_bands) + b09_asset = item2.assets["B09"] + b09_bands = RasterExtension.ext(b09_asset).bands + assert b09_bands is not None + self.assertEqual(b09_bands[0].nodata, "nan") + # Set - item2 = pystac.Item.from_file(self.SENTINEL2_EXAMPLE_URI) b2_asset = item2.assets["B02"] self.assertEqual( get_opt(get_opt(RasterExtension.ext(b2_asset).bands)[0].statistics).maximum, @@ -90,6 +96,9 @@ def test_asset_bands(self) -> None: get_opt(get_opt(new_b2_asset_bands)[0].statistics).maximum, 20567 ) + new_b2_asset_band0 = get_opt(new_b2_asset_bands)[0] + new_b2_asset_band0.nodata = NoDataStrings.INF + item2.validate() # Check adding a new asset @@ -127,7 +136,7 @@ def test_asset_bands(self) -> None: histogram=new_histograms[1], ), RasterBand.create( - nodata=3, + nodata=NoDataStrings.NINF, unit="test3", statistics=new_stats[2], histogram=new_histograms[2], @@ -148,6 +157,9 @@ def test_asset_bands(self) -> None: item.assets["test"].extra_fields["raster:bands"][1]["histogram"]["min"], 3848.354901960784, ) + self.assertEqual( + item.assets["test"].extra_fields["raster:bands"][2]["nodata"], "-inf" + ) for s in new_stats: s.minimum = None @@ -190,7 +202,7 @@ def test_asset_bands(self) -> None: histogram=new_histograms[1], ) new_bands[0].apply( - nodata=3, + nodata=NoDataStrings.NAN, unit="test3", statistics=new_stats[0], histogram=new_histograms[2], @@ -202,6 +214,9 @@ def test_asset_bands(self) -> None: ], 1, ) + self.assertEqual( + item.assets["test"].extra_fields["raster:bands"][0]["nodata"], "nan" + ) def test_extension_not_implemented(self) -> None: # Should raise exception if Item does not include extension URI From 0f308cdd0c29f290eb8a6eb08b4d4942f7180519 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 19 Jun 2022 15:00:24 -0400 Subject: [PATCH 47/60] build(deps): bump actions/setup-python from 3 to 4 (#828) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 3 to 4. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/continuous-integration.yml | 8 ++++---- .github/workflows/release.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index cf01d1417..3d95c1921 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -46,7 +46,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} cache: 'pip' @@ -81,7 +81,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python 3.8 - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: "3.8" cache: 'pip' @@ -133,7 +133,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} cache: 'pip' @@ -152,7 +152,7 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v4 with: python-version: "3.8" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 946250af3..841424785 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python 3.x - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: "3.x" From a6fffa59f4c6b6433858a8569b35647798593976 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Thu, 23 Jun 2022 11:29:46 -0400 Subject: [PATCH 48/60] Deepcopy Asset.extra_fields when cloning (#826) * Deepcopy Asset.extra_fields when cloning * Add CHANGELOG entry for #826 * Update docs and add test for Asset.roles --- CHANGELOG.md | 1 + pystac/asset.py | 6 +++--- tests/test_item.py | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1168b10a..c571200c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### Fixed +- Mutating `Asset.extra_fields` on a cloned `Asset` also mutated the original asset ([#826](https://github.com/stac-utils/pystac/pull/826)) - "How to create STAC catalogs" tutorial ([#775](https://github.com/stac-utils/pystac/pull/775)) - Add a `variables` argument, to accompany `dimensions`, for the `apply` method of stac objects extended with datacube ([#782](https://github.com/stac-utils/pystac/pull/782)) - Deepcopy collection properties on clone. Implement `clone` method for `Summaries` ([#794](https://github.com/stac-utils/pystac/pull/794)) diff --git a/pystac/asset.py b/pystac/asset.py index 7402054c9..d6ea612c2 100644 --- a/pystac/asset.py +++ b/pystac/asset.py @@ -1,5 +1,5 @@ from html import escape -from copy import copy +from copy import copy, deepcopy from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union from pystac import common_metadata @@ -136,7 +136,7 @@ def to_dict(self) -> Dict[str, Any]: return d def clone(self) -> "Asset": - """Clones this asset. + """Clones this asset. Makes a ``deepcopy`` of the :attr:`~pystac.Asset.extra_fields`. Returns: Asset: The clone of this asset. @@ -148,7 +148,7 @@ def clone(self) -> "Asset": description=self.description, media_type=self.media_type, roles=self.roles, - extra_fields=self.extra_fields, + extra_fields=deepcopy(self.extra_fields), ) @property diff --git a/tests/test_item.py b/tests/test_item.py index 7788dbc4e..895124636 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -280,6 +280,44 @@ def test_clone(self) -> None: self.assertIsInstance(cloned_item, self.BasicCustomItem) +class AssetTest(unittest.TestCase): + def setUp(self) -> None: + self.maxDiff = None + with open(TestCases.get_path("data-files/item/sample-item.json")) as src: + item_dict = json.load(src) + + self.asset_dict = item_dict["assets"]["analytic"] + + def example_asset(self) -> Asset: + return Asset.from_dict(self.asset_dict) + + def test_clone(self) -> None: + original_asset = self.example_asset() + cloned_asset = original_asset.clone() + + self.assertDictEqual(original_asset.to_dict(), self.asset_dict) + self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict) + + # Changes to original asset should not affect cloned Asset + original_asset.description = "Some new description" + self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict) + + original_asset.href = "/path/to/new/href" + self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict) + + original_asset.title = "New Title" + self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict) + + original_asset.roles = ["new role"] + self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict) + + original_asset.roles.append("new role") + self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict) + + original_asset.extra_fields["new_field"] = "new_value" + self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict) + + class AssetSubClassTest(unittest.TestCase): class CustomAsset(Asset): pass From afc12ccf5951a388f71d241637ab0ced4162f9e1 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Wed, 29 Jun 2022 12:28:01 -0400 Subject: [PATCH 49/60] Fix docs for StacIO.read/write_text (#835) * Fix docs for StacIO.read/write_text * Add CHANGELOG entry for #835 --- CHANGELOG.md | 1 + pystac/stac_io.py | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c571200c6..44d073bc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - "How to create STAC catalogs" tutorial ([#775](https://github.com/stac-utils/pystac/pull/775)) - Add a `variables` argument, to accompany `dimensions`, for the `apply` method of stac objects extended with datacube ([#782](https://github.com/stac-utils/pystac/pull/782)) - Deepcopy collection properties on clone. Implement `clone` method for `Summaries` ([#794](https://github.com/stac-utils/pystac/pull/794)) +- Docstrings for `StacIO.read_text` and `StacIO.write_text` now match the type annotations for the `source` argument. ([#835](https://github.com/stac-utils/pystac/pull/835)) ## [v1.4.0] diff --git a/pystac/stac_io.py b/pystac/stac_io.py index dd64e1930..6d98fffc7 100644 --- a/pystac/stac_io.py +++ b/pystac/stac_io.py @@ -42,11 +42,11 @@ class StacIO(ABC): def read_text(self, source: HREF, *args: Any, **kwargs: Any) -> str: """Read text from the given URI. - The source to read from can be specified as a string or a - :class:`~pystac.Link`. If it is a string, it must be a URI or local path from - which to read. Using a :class:`~pystac.Link` enables implementations to use - additional link information, such as paging information contained in the - extended links described in the `STAC API spec + The source to read from can be specified as a string or :class:`os.PathLike` + object (:class:`~pystac.Link` is a path-like object). If it is a string, it + must be a URI or local path from which to read. Using a :class:`~pystac.Link` + enables implementations to use additional link information, such as paging + information contained in the extended links described in the `STAC API spec `__. Args: @@ -71,10 +71,11 @@ def write_text( ) -> None: """Write the given text to a file at the given URI. - The destination to write to from can be specified as a string or a - :class:`~pystac.Link`. If it is a string, it must be a URI or local path from - which to read. Using a :class:`~pystac.Link` enables implementations to use - additional link information. + The destination to write to can be specified as a string or + :class:`os.PathLike` object (:class:`~pystac.Link` is a path-like object). If + it is a string, it must be a URI or local path from which to read. Using a + :class:`~pystac.Link` enables implementations to use additional link + information. Args: dest : The destination to write to. From 0c6074a4315af5d592613c7ce3b2e0d0ef6ed16d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jun 2022 12:28:55 -0400 Subject: [PATCH 50/60] build(deps): bump types-python-dateutil from 2.8.17 to 2.8.18 (#829) Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.17 to 2.8.18. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-python-dateutil dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 49dd59995..0747c6d75 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -8,7 +8,7 @@ jsonschema==4.6.0 coverage==6.4.1 doc8==0.11.2 -types-python-dateutil==2.8.17 +types-python-dateutil==2.8.18 types-orjson==3.6.2 pre-commit==2.19.0 From b7fbd6e55477c0ae23cb4e2d39385837110416d6 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Wed, 29 Jun 2022 18:17:53 -0400 Subject: [PATCH 51/60] Preserve Collection assets on clone (#834) * Allow assets to be passed to Item init * Allow assets to be passed to Collection init * Preserve Collection assets on clone * Add CHANGELOG entry for #834 * Match clone tests for Item and Collection --- CHANGELOG.md | 2 ++ pystac/collection.py | 18 +++++++++++++----- pystac/item.py | 19 +++++++++++-------- tests/test_collection.py | 19 +++++++++++++++++++ tests/test_item.py | 22 +++++++++++++++------- 5 files changed, 60 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44d073bc4..9a86d5b0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Updated AssetDefinition to have create, apply methods ([#768](https://github.com/stac-utils/pystac/pull/768)) - Add Grid Extension support ([#799](https://github.com/stac-utils/pystac/pull/799)) - Rich HTML representations for Jupyter Notebook display ([#743](https://github.com/stac-utils/pystac/pull/743)) +- Add `assets` argument to `Item` and `Collection` init methods to allow adding Assets during object initialization ([#834](https://github.com/stac-utils/pystac/pull/834)) ### Removed @@ -24,6 +25,7 @@ - "How to create STAC catalogs" tutorial ([#775](https://github.com/stac-utils/pystac/pull/775)) - Add a `variables` argument, to accompany `dimensions`, for the `apply` method of stac objects extended with datacube ([#782](https://github.com/stac-utils/pystac/pull/782)) - Deepcopy collection properties on clone. Implement `clone` method for `Summaries` ([#794](https://github.com/stac-utils/pystac/pull/794)) +- Collection assets are now preserved when using `Collection.clone` ([#834](https://github.com/stac-utils/pystac/pull/834)) - Docstrings for `StacIO.read_text` and `StacIO.write_text` now match the type annotations for the `source` argument. ([#835](https://github.com/stac-utils/pystac/pull/835)) ## [v1.4.0] diff --git a/pystac/collection.py b/pystac/collection.py index 42d14ebfe..187b524f7 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -1,6 +1,7 @@ from html import escape from copy import deepcopy from datetime import datetime + from pystac.errors import STACTypeError from pystac.html.jinja_env import get_jinja_env from typing import ( @@ -446,6 +447,9 @@ class Collection(Catalog): either a set of values or statistics such as a range. extra_fields : Extra fields that are part of the top-level JSON properties of the Collection. + assets : A dictionary mapping string keys to :class:`~pystac.Asset` objects. All + :class:`~pystac.Asset` values in the dictionary will have their + :attr:`~pystac.Asset.owner` attribute set to the created Collection. """ assets: Dict[str, Asset] @@ -504,6 +508,7 @@ def __init__( keywords: Optional[List[str]] = None, providers: Optional[List["Provider_Type"]] = None, summaries: Optional[Summaries] = None, + assets: Optional[Dict[str, Asset]] = None, ): super().__init__( id, @@ -523,6 +528,9 @@ def __init__( self.summaries = summaries or Summaries.empty() self.assets = {} + if assets is not None: + for k, asset in assets.items(): + self.add_asset(k, asset) def __repr__(self) -> str: return "".format(self.id) @@ -579,6 +587,7 @@ def clone(self) -> "Collection": keywords=self.keywords.copy() if self.keywords is not None else None, providers=deepcopy(self.providers), summaries=self.summaries.clone(), + assets={k: asset.clone() for k, asset in self.assets.items()}, ) clone._resolved_objects.cache(clone) @@ -631,7 +640,9 @@ def from_dict( if summaries is not None: summaries = Summaries(summaries) - assets: Optional[Dict[str, Any]] = d.get("assets", None) + assets: Optional[Dict[str, Any]] = { + k: Asset.from_dict(v) for k, v in d.get("assets", {}).items() + } links = d.pop("links") d.pop("stac_version") @@ -649,6 +660,7 @@ def from_dict( summaries=summaries, href=href, catalog_type=catalog_type, + assets=assets, ) for link in links: @@ -659,10 +671,6 @@ def from_dict( if link["rel"] != pystac.RelType.SELF or href is None: collection.add_link(Link.from_dict(link)) - if assets is not None: - for asset_key, asset_dict in assets.items(): - collection.add_asset(asset_key, Asset.from_dict(asset_dict)) - if root: collection.set_root(root) diff --git a/pystac/item.py b/pystac/item.py index 46a8948de..c21ffbe19 100644 --- a/pystac/item.py +++ b/pystac/item.py @@ -51,6 +51,9 @@ class Item(STACObject): belongs to. extra_fields : Extra fields that are part of the top-level JSON properties of the Item. + assets : A dictionary mapping string keys to :class:`~pystac.Asset` objects. All + :class:`~pystac.Asset` values in the dictionary will have their + :attr:`~pystac.Asset.owner` attribute set to the created Item. """ assets: Dict[str, Asset] @@ -107,6 +110,7 @@ def __init__( href: Optional[str] = None, collection: Optional[Union[str, Collection]] = None, extra_fields: Optional[Dict[str, Any]] = None, + assets: Optional[Dict[str, Asset]] = None, ): super().__init__(stac_extensions or []) @@ -144,6 +148,11 @@ def __init__( else: self.collection_id = collection + self.assets = {} + if assets is not None: + for k, asset in assets.items(): + self.add_asset(k, asset) + def __repr__(self) -> str: return "".format(self.id) @@ -359,13 +368,11 @@ def clone(self) -> "Item": properties=deepcopy(self.properties), stac_extensions=deepcopy(self.stac_extensions), collection=self.collection_id, + assets={k: asset.clone() for k, asset in self.assets.items()}, ) for link in self.links: clone.add_link(link.clone()) - for k, asset in self.assets.items(): - clone.add_asset(k, asset.clone()) - return clone def _object_links(self) -> List[Union[str, pystac.RelType]]: @@ -420,6 +427,7 @@ def from_dict( stac_extensions=stac_extensions, collection=collection_id, extra_fields=d, + assets={k: Asset.from_dict(v) for k, v in assets.items()}, ) has_self_link = False @@ -430,11 +438,6 @@ def from_dict( if not has_self_link and href is not None: item.add_link(Link.self_href(href)) - for k, v in assets.items(): - asset = Asset.from_dict(v) - asset.set_owner(item) - item.assets[k] = asset - if root: item.set_root(root) diff --git a/tests/test_collection.py b/tests/test_collection.py index 61e4dc415..82e78b583 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -271,6 +271,25 @@ def test_from_invalid_dict_raises_exception(self) -> None: with self.assertRaises(pystac.STACTypeError): _ = pystac.Collection.from_dict(catalog_dict) + def test_clone_preserves_assets(self) -> None: + path = TestCases.get_path("data-files/collections/with-assets.json") + original_collection = Collection.from_file(path) + assert len(original_collection.assets) > 0 + assert all( + asset.owner is original_collection + for asset in original_collection.assets.values() + ) + + cloned_collection = original_collection.clone() + + for key in original_collection.assets: + with self.subTest(f"Preserves {key} asset"): + self.assertIn(key, cloned_collection.assets) + cloned_asset = cloned_collection.assets.get(key) + if cloned_asset is not None: + with self.subTest(f"Sets owner for {key}"): + self.assertIs(cloned_asset.owner, cloned_collection) + class ExtentTest(unittest.TestCase): def setUp(self) -> None: diff --git a/tests/test_item.py b/tests/test_item.py index 895124636..4e6d8d6c3 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -221,15 +221,23 @@ def test_0_9_item_with_no_extensions_does_not_read_collection_data(self) -> None ) self.assertFalse(did_merge) - def test_clone_sets_asset_owner(self) -> None: + def test_clone_preserves_assets(self) -> None: cat = TestCases.test_case_2() - item = next(iter(cat.get_all_items())) - original_asset = list(item.assets.values())[0] - assert original_asset.owner is item + original_item = next(iter(cat.get_all_items())) + assert len(original_item.assets) > 0 + assert all( + asset.owner is original_item for asset in original_item.assets.values() + ) + + cloned_item = original_item.clone() - clone = item.clone() - clone_asset = list(clone.assets.values())[0] - self.assertIs(clone_asset.owner, clone) + for key in original_item.assets: + with self.subTest(f"Preserves {key} asset"): + self.assertIn(key, cloned_item.assets) + cloned_asset = cloned_item.assets.get(key) + if cloned_asset is not None: + with self.subTest(f"Sets owner for {key}"): + self.assertIs(cloned_asset.owner, cloned_item) def test_make_asset_href_relative_is_noop_on_relative_hrefs(self) -> None: cat = TestCases.test_case_2() From 57268362268d51ae59834aee3748ea830b44c445 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jun 2022 18:18:11 -0400 Subject: [PATCH 52/60] build(deps): bump black from 22.3.0 to 22.6.0 (#833) Bumps [black](https://github.com/psf/black) from 22.3.0 to 22.6.0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/22.3.0...22.6.0) --- updated-dependencies: - dependency-name: black dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 0747c6d75..442e98621 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,6 @@ mypy==0.961 flake8==4.0.1 -black==22.3.0 +black==22.6.0 codespell==2.1.0 From e88968b227312de86d8b340e7781d0d70598adcb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Jun 2022 08:59:56 -0400 Subject: [PATCH 53/60] build(deps): bump orjson from 3.7.2 to 3.7.5 (#832) Bumps [orjson](https://github.com/ijl/orjson) from 3.7.2 to 3.7.5. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.7.2...3.7.5) --- updated-dependencies: - dependency-name: orjson dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 442e98621..fd4809182 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -14,4 +14,4 @@ types-orjson==3.6.2 pre-commit==2.19.0 # optional dependencies -orjson==3.7.2 +orjson==3.7.5 From a3683fd8e26bea629ff86c863db1c9b8bd5bb874 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Jun 2022 20:16:15 -0400 Subject: [PATCH 54/60] build(deps): bump jsonschema from 4.6.0 to 4.6.1 (#836) Bumps [jsonschema](https://github.com/python-jsonschema/jsonschema) from 4.6.0 to 4.6.1. - [Release notes](https://github.com/python-jsonschema/jsonschema/releases) - [Changelog](https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst) - [Commits](https://github.com/python-jsonschema/jsonschema/compare/v4.6.0...v4.6.1) --- updated-dependencies: - dependency-name: jsonschema dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index fd4809182..e6dcbda40 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,7 +4,7 @@ black==22.6.0 codespell==2.1.0 -jsonschema==4.6.0 +jsonschema==4.6.1 coverage==6.4.1 doc8==0.11.2 From 5cd84099cd541b9b7458c3120c7cef3da010e603 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 17 Jul 2022 13:25:46 -0400 Subject: [PATCH 55/60] build(deps): bump orjson from 3.7.5 to 3.7.7 (#839) Bumps [orjson](https://github.com/ijl/orjson) from 3.7.5 to 3.7.7. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.7.5...3.7.7) --- updated-dependencies: - dependency-name: orjson dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index e6dcbda40..e41207848 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -14,4 +14,4 @@ types-orjson==3.6.2 pre-commit==2.19.0 # optional dependencies -orjson==3.7.5 +orjson==3.7.7 From 2e8bb2dfa11f91f016996cfb32c050ec0aae47e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 17 Jul 2022 13:28:34 -0400 Subject: [PATCH 56/60] build(deps): bump jsonschema from 4.6.1 to 4.7.2 (#844) Bumps [jsonschema](https://github.com/python-jsonschema/jsonschema) from 4.6.1 to 4.7.2. - [Release notes](https://github.com/python-jsonschema/jsonschema/releases) - [Changelog](https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst) - [Commits](https://github.com/python-jsonschema/jsonschema/compare/v4.6.1...v4.7.2) --- updated-dependencies: - dependency-name: jsonschema dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jon Duckworth --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index e41207848..7671f86b5 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,7 +4,7 @@ black==22.6.0 codespell==2.1.0 -jsonschema==4.6.1 +jsonschema==4.7.2 coverage==6.4.1 doc8==0.11.2 From e351afd4df34c83bd6de8f9082bb376ae5b48e06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 17 Jul 2022 13:32:54 -0400 Subject: [PATCH 57/60] build(deps): bump coverage from 6.4.1 to 6.4.2 (#847) Bumps [coverage](https://github.com/nedbat/coveragepy) from 6.4.1 to 6.4.2. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/6.4.1...6.4.2) --- updated-dependencies: - dependency-name: coverage dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jon Duckworth --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 7671f86b5..7813b7ea8 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -5,7 +5,7 @@ black==22.6.0 codespell==2.1.0 jsonschema==4.7.2 -coverage==6.4.1 +coverage==6.4.2 doc8==0.11.2 types-python-dateutil==2.8.18 From 8a03debf8b047f432bea76f3e4c406c74a6c4ee6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 17 Jul 2022 13:33:22 -0400 Subject: [PATCH 58/60] build(deps): bump pre-commit from 2.19.0 to 2.20.0 (#841) Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.19.0 to 2.20.0. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v2.19.0...v2.20.0) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 7813b7ea8..1b34d47ca 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -11,7 +11,7 @@ doc8==0.11.2 types-python-dateutil==2.8.18 types-orjson==3.6.2 -pre-commit==2.19.0 +pre-commit==2.20.0 # optional dependencies orjson==3.7.7 From 26305d539d2e44faa526c7034f4c7bf639392448 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Mon, 18 Jul 2022 11:20:23 -0400 Subject: [PATCH 59/60] Use isoparse instead of parse for datetimes (#848) * Use isoparse instead of parse for datetimes * Move tests to test_utils and remove comments * Add CHANGELOG entry for #848 * Avoid time.tzset calls on Windows --- CHANGELOG.md | 1 + pystac/collection.py | 7 +++---- pystac/item.py | 4 +--- pystac/utils.py | 2 +- tests/test_utils.py | 46 +++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 51 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a86d5b0a..5d1c14f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Deepcopy collection properties on clone. Implement `clone` method for `Summaries` ([#794](https://github.com/stac-utils/pystac/pull/794)) - Collection assets are now preserved when using `Collection.clone` ([#834](https://github.com/stac-utils/pystac/pull/834)) - Docstrings for `StacIO.read_text` and `StacIO.write_text` now match the type annotations for the `source` argument. ([#835](https://github.com/stac-utils/pystac/pull/835)) +- UTC timestamps now always have `tzutc` timezone even when system timezone is set to UTC. ([#848](https://github.com/stac-utils/pystac/pull/848)) ## [v1.4.0] diff --git a/pystac/collection.py b/pystac/collection.py index 187b524f7..893275fc8 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -17,7 +17,6 @@ cast, ) -import dateutil.parser from dateutil import tz import pystac @@ -27,7 +26,7 @@ from pystac.layout import HrefLayoutStrategy from pystac.link import Link from pystac.provider import Provider -from pystac.utils import datetime_to_str +from pystac.utils import datetime_to_str, str_to_datetime from pystac.serialization import ( identify_stac_object_type, identify_stac_object, @@ -253,9 +252,9 @@ def from_dict(d: Dict[str, Any]) -> "TemporalExtent": end = None if i[0]: - start = dateutil.parser.parse(i[0]) + start = str_to_datetime(i[0]) if i[1]: - end = dateutil.parser.parse(i[1]) + end = str_to_datetime(i[1]) parsed_intervals.append([start, end]) return TemporalExtent( diff --git a/pystac/item.py b/pystac/item.py index c21ffbe19..99cc5c1e4 100644 --- a/pystac/item.py +++ b/pystac/item.py @@ -4,8 +4,6 @@ from pystac.catalog import Catalog from typing import Any, Dict, List, Optional, Union, cast -import dateutil.parser - import pystac from pystac.html.jinja_env import get_jinja_env from pystac import STACError, STACObjectType @@ -411,7 +409,7 @@ def from_dict( datetime = properties.get("datetime") if datetime is not None: - datetime = dateutil.parser.parse(datetime) + datetime = str_to_datetime(datetime) links = d.pop("links") assets = d.pop("assets") diff --git a/pystac/utils.py b/pystac/utils.py index 8c1d4fd04..860e4ebba 100644 --- a/pystac/utils.py +++ b/pystac/utils.py @@ -330,7 +330,7 @@ def str_to_datetime(s: str) -> datetime: Args: s (str) : The string to convert to :class:`datetime.datetime`. """ - return dateutil.parser.parse(s) + return dateutil.parser.isoparse(s) def geometry_to_bbox(geometry: Dict[str, Any]) -> List[float]: diff --git a/tests/test_utils.py b/tests/test_utils.py index 453dc6fcd..ada187a21 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,13 +1,21 @@ +from typing import Optional import unittest import os import json import ntpath import sys +import time from datetime import datetime, timezone, timedelta +from dateutil import tz from pystac import utils -from pystac.utils import make_relative_href, make_absolute_href, is_absolute_href +from pystac.utils import ( + make_relative_href, + make_absolute_href, + is_absolute_href, + str_to_datetime, +) from tests.utils import TestCases @@ -273,6 +281,42 @@ def test_datetime_to_str(self) -> None: got = utils.datetime_to_str(dt) self.assertEqual(expected, got) + def test_str_to_datetime(self) -> None: + def _set_tzinfo(tz_str: Optional[str]) -> None: + if tz_str is None: + if "TZ" in os.environ: + del os.environ["TZ"] + else: + os.environ["TZ"] = tz_str + # time.tzset() only available for Unix/Linux + if hasattr(time, "tzset"): + time.tzset() + + utc_timestamp = "2015-06-27T10:25:31Z" + + prev_tz = os.environ.get("TZ") + + with self.subTest(tz=None): + _set_tzinfo(None) + utc_datetime = str_to_datetime(utc_timestamp) + self.assertIs(utc_datetime.tzinfo, tz.tzutc()) + self.assertIsNot(utc_datetime.tzinfo, tz.tzlocal()) + + with self.subTest(tz="UTC"): + _set_tzinfo("UTC") + utc_datetime = str_to_datetime(utc_timestamp) + self.assertIs(utc_datetime.tzinfo, tz.tzutc()) + self.assertIsNot(utc_datetime.tzinfo, tz.tzlocal()) + + with self.subTest(tz="US/Central"): + _set_tzinfo("US/Central") + utc_datetime = str_to_datetime(utc_timestamp) + self.assertIs(utc_datetime.tzinfo, tz.tzutc()) + self.assertIsNot(utc_datetime.tzinfo, tz.tzlocal()) + + if prev_tz is not None: + _set_tzinfo(prev_tz) + def test_geojson_bbox(self) -> None: # Use sample Geojson from https://en.wikipedia.org/wiki/GeoJSON with open( From 8efe00a6caa36618a3992e75e464ae601de351bc Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Fri, 22 Jul 2022 08:57:28 -0400 Subject: [PATCH 60/60] Changes to support 1.5.0 release (#849) --- CHANGELOG.md | 15 ++++++++++++--- pystac/version.py | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d1c14f34..db5e5d6ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ ### Added +### Removed + +### Changed + +### Fixed + +## [v1.5.0] + +### Added + - Enum MediaType entry for PDF documents ([#758](https://github.com/stac-utils/pystac/pull/758)) - Enum MediaType entry for HTML documents ([#816](https://github.com/stac-utils/pystac/pull/816)) - Updated Link to obtain stac_io from owner root ([#762](https://github.com/stac-utils/pystac/pull/762)) @@ -13,8 +23,6 @@ - Rich HTML representations for Jupyter Notebook display ([#743](https://github.com/stac-utils/pystac/pull/743)) - Add `assets` argument to `Item` and `Collection` init methods to allow adding Assets during object initialization ([#834](https://github.com/stac-utils/pystac/pull/834)) -### Removed - ### Changed - Updated Raster Extension from v1.0.0 to v1.1.0 ([#809](https://github.com/stac-utils/pystac/pull/809)) @@ -594,7 +602,8 @@ use `Band.create` Initial release. -[Unreleased]: +[Unreleased]: +[v1.5.0]: [v1.4.0]: [v1.3.0]: [v1.2.0]: diff --git a/pystac/version.py b/pystac/version.py index 32f4a026a..ba4486ac1 100644 --- a/pystac/version.py +++ b/pystac/version.py @@ -1,7 +1,7 @@ import os from typing import Optional -__version__ = "1.4.0" +__version__ = "1.5.0" """Library version"""