diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 8482db719..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ -**Describe the bug** -A clear and concise description of what the bug is. - -**To reproduce** -Steps to reproduce the behavior: - -> Ex. -> -> 1. Install pystac w/ dev requirements: `pip install -e . -r requirements-dev.txt` -> 2. Run `pytest` -> 3. See error - -Include OS, Python version, and PySTAC version. - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots and shell session dumps** -If applicable, add session dumps and/or screenshots to help explain your problem. - -> ex. `pre-commit run ruff > ruff.txt` - -**Additional context** -Add any other context about the problem here. - -**Issue Checklist** - -- [ ] OS, Python version, PySTAC version are included. -- [ ] Existing issues were reviewed to prevent duplicate submission. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 2e4bf5e1d..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,14 +0,0 @@ -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. Ex. I would like to use PySTAC to do [...] - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**PySTAC version** -Include your PySTAC version in case a similar feature already exists in a newer or pre-release version. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index dcfd50bb2..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,13 +0,0 @@ -version: 2 -updates: - - package-ecosystem: github-actions - directory: / - schedule: - interval: daily - commit-message: - prefix: build(deps) - - package-ecosystem: pip - directory: / - schedule: - interval: daily - versioning-strategy: increase-if-necessary diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 3fd45128a..000000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,13 +0,0 @@ -**Related Issue(s):** - -- # - -**Description:** - -**PR Checklist:** - -- [ ] Pre-commit hooks pass (run `pre-commit run --all-files`) -- [ ] Tests pass (run `pytest`) -- [ ] Documentation has been updated to reflect changes, if applicable -- [ ] This PR maintains or improves overall codebase code coverage. -- [ ] Changes are added to the [CHANGELOG](https://github.com/stac-utils/pystac/blob/main/CHANGELOG.md). See [the docs](https://pystac.readthedocs.io/en/latest/contributing.html#changelog) for information about adding to the changelog. diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..b4fd18636 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,70 @@ +name: CI + +on: + push: + branches: + - v2 + pull_request: + branches: + - v2 + +jobs: + test: + name: Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + os: + - ubuntu-latest + # - windows-latest + - macos-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Sync + run: uv sync + - name: Lint + run: scripts/lint + - name: Test + run: uv run pytest + - name: Test w/ validation extra + run: uv run --extra validation pytest + - name: Test w/ obstore extra + run: uv run --extra obstore pytest + build-docs: + name: Build docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + - name: Sync + run: uv sync + - name: Build + run: uv run mkdocs build + - name: Upload + id: deployment + uses: actions/upload-pages-artifact@v3 + with: + path: site/ + deploy-docs: + name: Deploy docs + if: github.ref == 'refs/heads/v2' + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build-docs + permissions: + pages: write + id-token: write + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml deleted file mode 100644 index dee1329ae..000000000 --- a/.github/workflows/continuous-integration.yml +++ /dev/null @@ -1,126 +0,0 @@ -name: CI - -on: - push: - branches: - - main - - "0.3" - - "0.4" - - "0.5" - - "1.0" - - "2.0" - pull_request: - merge_group: - -concurrency: - group: ${{ github.ref }} - cancel-in-progress: true - -jobs: - test: - name: test - runs-on: ${{ matrix.os }} - strategy: - matrix: - python-version: - - "3.10" - - "3.11" - - "3.12" - - "3.13" - os: - - ubuntu-latest - - windows-latest - - macos-latest - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Sync - run: uv sync --all-extras - - name: Lint - if: runner.os != 'Windows' - run: uv run pre-commit run --all-files - - name: Test on windows - if: runner.os == 'Windows' - shell: bash - env: - TMPDIR: 'D:\\a\\_temp' - run: uv run pytest tests - - name: Test - if: runner.os != 'Windows' - run: uv run pytest tests --block-network --record-mode=none - - coverage: - name: coverage - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 - - name: Install with dependencies - run: uv sync --all-extras - - name: Run coverage with orjson - run: uv run pytest tests --cov - - name: Uninstall orjson - run: uv pip uninstall orjson - - name: Run coverage without orjson, appending results - run: uv run pytest tests --cov --cov-append - - name: Prepare ./coverage.xml - # Ignore the configured fail-under to ensure we upload the coverage report. We - # will trigger a failure for coverage drops in a later job - run: uv run coverage xml --fail-under 0 - - name: Upload All coverage to Codecov - uses: codecov/codecov-action@v5 - if: ${{ env.GITHUB_REPOSITORY }} == 'stac-utils/pystac' - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage.xml - fail_ci_if_error: false - - name: Check for coverage drop - # This will use the configured fail-under, causing this job to fail if the - # coverage drops. - run: uv run coverage report - - without-orjson: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 - - name: Sync - run: uv sync - - name: Uninstall orjson - run: uv pip uninstall orjson - - name: Run tests - run: uv run pytest tests - - check-benchmarks: - # This checks to make sure any API changes haven't broken any of the - # benchmarks. It doesn't do any actual benchmarking, since (IMO) that's not - # appropriate for CI on Github actions. - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - - name: Sync - run: uv sync - - name: Set asv machine - run: uv run asv machine --yes - - name: Check benchmarks - run: uv run asv run -a repeat=1 -a rounds=1 HEAD - - docs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 - - name: Install pandoc - run: sudo apt-get install pandoc - - name: Sync - run: uv sync --group docs - - name: Check docs - run: uv run make -C docs html SPHINXOPTS="-W --keep-going -n" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index c9c8b4d11..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Release - -on: - release: - types: - - published - -jobs: - release: - name: release - runs-on: ubuntu-latest - environment: - name: pypi - url: https://pypi.org/p/pystac - permissions: - id-token: write - if: ${{ github.repository }} == 'stac-utils/pystac' - steps: - - uses: actions/checkout@v4 - - name: Set up Python 3.x - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - name: Install build - run: | - python -m pip install --upgrade pip - pip install build - - name: Build - run: python -m build - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 4ff8df29a..000000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Configuration file for pre-commit (https://pre-commit.com/). -# Please run `pre-commit run --all-files` when adding or changing entries. - -repos: - - repo: https://github.com/asottile/pyupgrade - rev: v3.18.0 - hooks: - - id: pyupgrade - args: - - "--py310-plus" - - - repo: local - hooks: - - id: ruff - name: ruff - entry: ruff check --force-exclude --fix --exit-non-zero-on-fix - language: system - types_or: [python, pyi, jupyter] - require_serial: true - - - id: ruff-format - name: ruff-format - entry: ruff format --force-exclude - language: system - stages: [pre-commit] - types_or: [python, pyi, jupyter] - require_serial: true - - - id: codespell - name: codespell - entry: codespell - language: system - stages: [pre-commit] - types_or: [jupyter, markdown, python, shell] - - - id: doc8 - name: doc8 - entry: doc8 - language: system - files: \.rst$ - require_serial: true - - - id: mypy - name: mypy - entry: mypy - args: [--no-incremental] - language: system - stages: [pre-commit] - types: [python] - require_serial: true diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 991e44fe0..9ef48dc06 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,7 +1,3 @@ -# .readthedocs.yaml -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - version: 2 build: @@ -9,18 +5,8 @@ build: tools: python: "3.10" commands: - # https://docs.readthedocs.io/en/stable/build-customization.html#install-dependencies-with-uv - # with adaptations to use workspaces+projects instead of `uv pip` - asdf plugin add uv - asdf install uv latest - asdf global uv latest - - uv sync --group docs - - uv run sphinx-build -T -b html -d docs/_build/doctrees -D language=en docs $READTHEDOCS_OUTPUT/html - -formats: - # Temporarily disabling PDF downloads due to problem with nbsphinx in LateX builds - # - pdf - - htmlzip - -sphinx: - fail_on_warning: false + - uv sync --all-extras + - uv run --all-extras mkdocs build -d $READTHEDOCS_OUTPUT/html diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index e8620dd31..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include pystac/py.typed pystac/html/*.jinja2 pystac/validation/jsonschemas/geojson/*.json pystac/validation/jsonschemas/stac-spec/v1.1.0/*.json pystac/static/*.json -exclude tests/* diff --git a/asv.conf.json b/asv.conf.json deleted file mode 100644 index 68bd9f138..000000000 --- a/asv.conf.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "version": 1, - "project": "pystac", - "project_url": "https://pystac.readthedocs.io/", - "repo": ".", - "branches": [ - "main" - ], - "dvcs": "git", - "environment_type": "virtualenv", - "show_commit_url": "http://github.com/stac-utils/pystac/commit/", - "matrix": { - "req": { - "orjson": [ - null, - "" - ] - } - }, - "benchmark_dir": "benchmarks", - "env_dir": ".asv/env", - "results_dir": ".asv/results", - "html_dir": ".asv/html", - "build_command": [ - "pip install build", - "python -m build", - "PIP_NO_BUILD_ISOLATION=false python -mpip wheel --no-deps --no-index -w {build_cache_dir} {build_dir}" - ] -} \ No newline at end of file diff --git a/benchmarks/_base.py b/benchmarks/_base.py deleted file mode 100644 index 02ca1fa9c..000000000 --- a/benchmarks/_base.py +++ /dev/null @@ -1,6 +0,0 @@ -class Bench: - # Repeat between 10-50 times up to a max time of 5s - repeat = (10, 50, 2.0) - - # Bump number of rounds to 4 - rounds = 4 diff --git a/benchmarks/_util.py b/benchmarks/_util.py deleted file mode 100644 index f2c4dcf96..000000000 --- a/benchmarks/_util.py +++ /dev/null @@ -1,16 +0,0 @@ -from __future__ import annotations - -import os -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - PathLike = os.PathLike[str] - - -def get_data_path(rel_path: str | PathLike) -> str: - """Gets the absolute path to a file based on a path relative to the - tests/data-files directory in this repo.""" - rel_path = os.fspath(rel_path) - return os.path.abspath( - os.path.join(os.path.dirname(__file__), "..", "tests", "data-files", rel_path) - ) diff --git a/benchmarks/catalog.py b/benchmarks/catalog.py deleted file mode 100644 index d7b8c71ab..000000000 --- a/benchmarks/catalog.py +++ /dev/null @@ -1,119 +0,0 @@ -import json -import os -import shutil -import tempfile -from datetime import datetime -from pathlib import Path -from tempfile import TemporaryDirectory - -from pystac import ( - Catalog, - Collection, - Extent, - Item, - SpatialExtent, - StacIO, - TemporalExtent, -) - -from ._base import Bench -from ._util import get_data_path - - -class CatalogBench(Bench): - def setup(self) -> None: - self.temp_dir = tempfile.mkdtemp() - - self.stac_io = StacIO.default() - - self.catalog_path = get_data_path("examples/1.0.0/catalog.json") - with open(self.catalog_path) as src: - self.catalog_dict = json.load(src) - self.catalog = Catalog.from_file(self.catalog_path) - - def teardown(self) -> None: - shutil.rmtree(self.temp_dir, ignore_errors=True) - - def time_catalog_from_file(self) -> None: - """Deserialize an Item from file""" - _ = Catalog.from_file(self.catalog_path) - - def time_catalog_from_dict(self) -> None: - """Deserialize an Item from dictionary.""" - _ = Catalog.from_dict(self.catalog_dict) - - def time_catalog_to_dict(self) -> None: - """Serialize an Item to a dictionary.""" - self.catalog.to_dict(include_self_link=True) - - def time_catalog_save(self) -> None: - """Serialize an Item to a JSON file.""" - self.catalog.save_object( - include_self_link=True, - dest_href=os.path.join(self.temp_dir, "time_catalog_save.json"), - stac_io=self.stac_io, - ) - - -class WalkCatalogBench(Bench): - def setup_cache(self) -> Catalog: - return make_large_catalog() - - def time_walk(self, catalog: Catalog) -> None: - for ( - _, - _, - _, - ) in catalog.walk(): - pass - - def peakmem_walk(self, catalog: Catalog) -> None: - for ( - _, - _, - _, - ) in catalog.walk(): - pass - - -class ReadCatalogBench(Bench): - def setup(self) -> None: - catalog = make_large_catalog() - self.temporary_directory = TemporaryDirectory() - self.path = str(Path(self.temporary_directory.name) / "catalog.json") - catalog.normalize_and_save(self.temporary_directory.name) - - def teardown(self) -> None: - shutil.rmtree(self.temporary_directory.name) - - def time_read_and_walk(self) -> None: - catalog = Catalog.from_file(self.path) - for _, _, _ in catalog.walk(): - pass - - -class WriteCatalogBench(Bench): - def setup(self) -> None: - self.catalog = make_large_catalog() - self.temporary_directory = TemporaryDirectory() - - def teardown(self) -> None: - shutil.rmtree(self.temporary_directory.name) - - def time_normalize_and_save(self) -> None: - self.catalog.normalize_and_save(self.temporary_directory.name) - - -def make_large_catalog() -> Catalog: - catalog = Catalog("an-id", "a description") - extent = Extent( - SpatialExtent([[-180.0, -90.0, 180.0, 90.0]]), - TemporalExtent([[datetime(2023, 1, 1), None]]), - ) - for i in range(0, 10): - collection = Collection(f"collection-{i}", f"Collection {i}", extent) - for j in range(0, 100): - item = Item(f"item-{i}-{j}", None, None, datetime.now(), {}) - collection.add_item(item) - catalog.add_child(collection) - return catalog diff --git a/benchmarks/collection.py b/benchmarks/collection.py deleted file mode 100644 index 3b1200b91..000000000 --- a/benchmarks/collection.py +++ /dev/null @@ -1,44 +0,0 @@ -import json -import os -import shutil -import tempfile - -from pystac import Collection, StacIO - -from ._base import Bench -from ._util import get_data_path - - -class CollectionBench(Bench): - def setup(self) -> None: - self.temp_dir = tempfile.mkdtemp() - - self.stac_io = StacIO.default() - - self.collection_path = get_data_path("examples/1.0.0/collection.json") - with open(self.collection_path) as src: - self.collection_dict = json.load(src) - self.collection = Collection.from_file(self.collection_path) - - def teardown(self) -> None: - shutil.rmtree(self.temp_dir, ignore_errors=True) - - def time_collection_from_file(self) -> None: - """Deserialize an Item from file""" - _ = Collection.from_file(self.collection_path) - - def time_collection_from_dict(self) -> None: - """Deserialize an Item from dictionary.""" - _ = Collection.from_dict(self.collection_dict) - - def time_collection_to_dict(self) -> None: - """Serialize an Item to a dictionary.""" - self.collection.to_dict(include_self_link=True) - - def time_collection_save(self) -> None: - """Serialize an Item to a JSON file.""" - self.collection.save_object( - include_self_link=True, - dest_href=os.path.join(self.temp_dir, "time_collection_save.json"), - stac_io=self.stac_io, - ) diff --git a/benchmarks/extensions/projection.py b/benchmarks/extensions/projection.py deleted file mode 100644 index 8de2e13ae..000000000 --- a/benchmarks/extensions/projection.py +++ /dev/null @@ -1,14 +0,0 @@ -from datetime import datetime - -from pystac import Item -from pystac.extensions.projection import ProjectionExtension - -from .._base import Bench - - -class ProjectionBench(Bench): - def setup(self) -> None: - self.item = Item("an-id", None, None, datetime.now(), {}) - - def time_add_projection_extension(self) -> None: - _ = ProjectionExtension.ext(self.item, add_if_missing=True) diff --git a/benchmarks/import_pystac.py b/benchmarks/import_pystac.py deleted file mode 100644 index 90f5858c7..000000000 --- a/benchmarks/import_pystac.py +++ /dev/null @@ -1,7 +0,0 @@ -class ImportPySTACBench: - repeat = 10 - - def timeraw_import_pystac(self) -> str: - return """ - import pystac - """ diff --git a/benchmarks/item.py b/benchmarks/item.py deleted file mode 100644 index 2e70dd1d8..000000000 --- a/benchmarks/item.py +++ /dev/null @@ -1,45 +0,0 @@ -import json -import os -import shutil -import tempfile - -from pystac import Item, StacIO - -from ._base import Bench -from ._util import get_data_path - - -class ItemBench(Bench): - def setup(self) -> None: - self.temp_dir = tempfile.mkdtemp() - - self.stac_io = StacIO.default() - - # using an item with many assets to better test deserialization timing - self.item_path = get_data_path("eo/eo-sentinel2-item.json") - with open(self.item_path) as src: - self.item_dict = json.load(src) - self.item = Item.from_file(self.item_path) - - def teardown(self) -> None: - shutil.rmtree(self.temp_dir, ignore_errors=True) - - def time_item_from_file(self) -> None: - """Deserialize an Item from file""" - _ = Item.from_file(self.item_path) - - def time_item_from_dict(self) -> None: - """Deserialize an Item from dictionary.""" - _ = Item.from_dict(self.item_dict) - - def time_item_to_dict(self) -> None: - """Serialize an Item to a dictionary.""" - self.item.to_dict(include_self_link=True) - - def time_item_save(self) -> None: - """Serialize an Item to a JSON file.""" - self.item.save_object( - include_self_link=True, - dest_href=os.path.join(self.temp_dir, "time_item_save.json"), - stac_io=self.stac_io, - ) diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index bfdc9877d..000000000 --- a/codecov.yml +++ /dev/null @@ -1,8 +0,0 @@ -coverage: - status: - project: - default: - informational: true - patch: - default: - informational: true diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index f9846e160..000000000 --- a/docs/Makefile +++ /dev/null @@ -1,22 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -livehtml: - sphinx-autobuild --watch ../pystac --host 0.0.0.0 ${SOURCEDIR} $(BUILDDIR)/html -d _build/doctrees - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/STAC-03.png b/docs/_static/STAC-03.png deleted file mode 100644 index 48783f210..000000000 Binary files a/docs/_static/STAC-03.png and /dev/null differ diff --git a/docs/api.rst b/docs/api.rst deleted file mode 100644 index 66d55e50a..000000000 --- a/docs/api.rst +++ /dev/null @@ -1,230 +0,0 @@ -API Reference -============= - -.. toctree:: - :hidden: - :maxdepth: 2 - :glob: - - api/pystac - api/* - -This API reference is auto-generated from the Python docstrings. The table of contents -on the left is organized by module. The sections below are organized based on concepts -and sections within the :stac-spec:`STAC Spec <>` and PySTAC itself. - -Base Structures & Classes -------------------------- - -These are the core Python classes representing entities within the STAC Spec. These -classes provide convenient methods for serializing and deserializing from JSON, -extracting properties, and creating relationships between entities. - -* :class:`pystac.Link`: Represents a :stac-spec:`Link Object - `. -* :class:`pystac.MediaType`: Provides common values used in the Link and Asset - ``"type"`` fields. -* :class:`pystac.RelType`: Provides common values used in the Link ``"rel"`` field. -* :class:`pystac.STACObject`: Base class implementing functionality common to - :class:`Catalog `, :class:`Collection ` and - :class:`Item `. - -Items ------ - -Representations of :stac-spec:`Items ` and related structures -like :stac-spec:`Asset Objects`. - -* :class:`pystac.Asset`: Represents an :stac-spec:`Asset Object - ` -* :class:`pystac.Item`: Represents an :stac-spec:`Item ` -* :class:`pystac.CommonMetadata`: A container for fields defined in the - :stac-spec:`Common Metadata ` section of the spec. - These fields are commonly found in STAC Item properties, but may be found elsewhere. - -Collections ------------ - -These are representations of :stac-spec:`Collections -` and related structures. - -* :class:`pystac.Collection`: Represents a :stac-spec:`Collection - `. -* :class:`pystac.Extent`: Represents an - :stac-spec:`Extent Object `, which - is composed of :class:`pystac.SpatialExtent` and :class:`pystac.TemporalExtent` - instances. -* :class:`pystac.Provider`: Represents a :stac-spec:`Provider Object - `. The - :class:`pystac.ProviderRole` enum provides common values used in the ``"roles"`` - field. -* :class:`pystac.Summaries`: Class for working with various types of - :stac-spec:`CollectionSummaries ` -* :class:`pystac.ItemCollection`: Represents a GeoJSON FeatureCollection in which all - Features are STAC Items. - -Catalogs --------- - -Representations of :stac-spec:`Catalogs ` and related -structures. - -* :class:`pystac.Catalog`: Represents a :stac-spec:`Catalog - `. -* :class:`pystac.CatalogType`: Enum representing the common types of Catalogs described - in the :stac-spec:`STAC Best Practices - ` - - -I/O ---- - -These classes are used to read and write files from disk or over the network, as well -as to serialize and deserialize STAC object to and from JSON. - -* :class:`pystac.StacIO`: Base class that can be inherited to provide custom I/O -* :class:`pystac.stac_io.DefaultStacIO`: The default :class:`pystac.StacIO` - implementation used throughout the library. - -Client ------- - -A convenience method for accessing `pystac-client `__ - -**Example:** - -.. code-block:: python - - from pystac.client import Client - - -Extensions ----------- - -PySTAC provides support for the following STAC Extensions: - -* :mod:`Datacube ` -* :mod:`Electro-Optical ` -* :mod:`File Info ` -* :mod:`Item Assets ` -* :mod:`MGRS ` -* :mod:`Point Cloud ` -* :mod:`Projection ` -* :mod:`Raster ` -* :mod:`SAR ` -* :mod:`Satellite ` -* :mod:`Scientific Citation ` -* :mod:`Table ` -* :mod:`Timestamps ` -* :mod:`Versioning Indicators ` -* :mod:`View Geometry ` -* :mod:`Xarray Assets ` - -The following classes are used internally to implement these extensions and may be used -to create custom implementations of STAC Extensions not supported by the library (see -:tutorial:`Adding New and Custom Extensions ` -for details): - -* :class:`pystac.extensions.base.SummariesExtension`: Base class for extending the - properties in :attr:`pystac.Collection.summaries` to include properties defined by a - STAC Extension. -* :class:`pystac.extensions.base.PropertiesExtension`: Abstract base class for - extending the properties of an :class:`~pystac.Item` to include properties defined - by a STAC Extension. -* :class:`pystac.extensions.base.ExtensionManagementMixin`: Abstract base class with - methods for adding and removing extensions from STAC Objects. -* :class:`pystac.extensions.hooks.ExtensionHooks`: Used to implement hooks when - extending a STAC Object. Primarily used to implement migrations from one extension - version to another. -* :class:`pystac.extensions.hooks.RegisteredExtensionHooks`: Used to register hooks - defined in :class:`~pystac.extensions.hooks.ExtensionHooks` instances to ensure they - are used in object deserialization. - - -Catalog Layout --------------- - -These classes are used to set the HREFs of a STAC according to some layout. -The templating functionality is also used when generating subcatalogs based on -a template. - -* :class:`pystac.layout.LayoutTemplate`: Represents a template that can be used for - deriving paths or other information based on properties of STAC objects supplied as a - template string. -* :class:`pystac.layout.BestPracticesLayoutStrategy`: Layout strategy that represents - the catalog layout described in the :stac-spec:`STAC Best Practices documentation - `. -* :class:`pystac.layout.APILayoutStrategy`: Layout strategy that represents - the catalog layout described in - the :stac-api-spec:`STAC API documentation `. -* :class:`pystac.layout.TemplateLayoutStrategy`: Layout strategy that can take strings - to be supplied to a :class:`~pystac.layout.LayoutTemplate` to derive paths. -* :class:`pystac.layout.CustomLayoutStrategy`: Layout strategy that allows users to - supply functions to dictate stac object paths. - -Errors ------- - -The following exceptions may be raised internally by the library. - -* :class:`pystac.STACError`: Generic STAC-related error -* :class:`pystac.STACTypeError`: Raised when a representation of a STAC entity is - encountered that is not correct for the context -* :class:`pystac.DuplicateObjectKeyError`: Raised when deserializing a JSON object - containing a duplicate key. -* :class:`pystac.ExtensionAlreadyExistsError`: Raised when deserializing a JSON object - containing a duplicate key. -* :class:`pystac.ExtensionTypeError`: Raised when an extension is used against an - object to which that the extension does not apply to. -* :class:`pystac.ExtensionNotImplemented`: Raised on an attempt to extend a STAC object - that does not implement the given extension. -* :class:`pystac.RequiredPropertyMissing`: Raised when a required value is expected to - be present but is missing or ``None``. -* :class:`pystac.STACValidationError`: Raised by validation calls if the STAC JSON is - invalid. -* :class:`pystac.TemplateError`: Raised when an error occurs while converting a - template string into data for :class:`~pystac.layout.LayoutTemplate`. - -Serialization -------------- - -The ``pystac.serialization`` sub-package contains tools used internally by PySTAC to -identify, serialize, and migrate STAC objects: - -* :mod:`pystac.serialization`: Tools for identifying and migrating STAC objects - - -Validation ----------- - -.. note:: - - The tools described here require that you install PySTAC with the ``validation`` - extra (see the documentation on :ref:`installing dependencies - ` for details). - -PySTAC includes a ``pystac.validation`` package for validating STAC objects, including -from PySTAC objects and directly from JSON. - -* :class:`pystac.validation.stac_validator.STACValidator`: Abstract base class defining - methods for validating STAC JSON. Implementations define methods for validating core - objects and extension. -* :class:`pystac.validation.stac_validator.JsonSchemaSTACValidator`: The default - :class:`~pystac.validation.stac_validator.STACValidator` implementation used by - PySTAC. Uses JSON schemas read from URIs provided by a - :class:`~pystac.validation.schema_uri_map.SchemaUriMap`, to validate STAC objects. -* :class:`pystac.validation.schema_uri_map.SchemaUriMap`: Defines methods for mapping - STAC versions, object types and extension ids to schema URIs. A default - implementation is included that uses known locations; however users can provide their - own schema URI maps in a - :class:`~pystac.validation.stac_validator.JsonSchemaSTACValidator` to modify the URIs - used. -* :class:`pystac.validation.schema_uri_map.DefaultSchemaUriMap`: The default - :class:`~pystac.validation.schema_uri_map.SchemaUriMap` used by PySTAC. - -Internal Classes ------------------------ - -These classes are used internally by PySTAC for caching. - -* :class:`pystac.cache.ResolvedObjectCache` diff --git a/docs/api/asset.md b/docs/api/asset.md new file mode 100644 index 000000000..66e1010c9 --- /dev/null +++ b/docs/api/asset.md @@ -0,0 +1,4 @@ +# Asset + +::: pystac.Asset +::: pystac.ItemAsset diff --git a/docs/api/asset.rst b/docs/api/asset.rst deleted file mode 100644 index 062108ead..000000000 --- a/docs/api/asset.rst +++ /dev/null @@ -1,12 +0,0 @@ -pystac.asset -============ - -.. autoclass:: pystac.asset.Asset - :members: - :undoc-members: - :noindex: - -.. automodule:: pystac.asset - :members: - :undoc-members: - :exclude-members: Asset diff --git a/docs/api/cache.rst b/docs/api/cache.rst deleted file mode 100644 index cc0d2f2ab..000000000 --- a/docs/api/cache.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.cache -============ - -.. automodule:: pystac.cache - :members: - :undoc-members: diff --git a/docs/api/catalog.md b/docs/api/catalog.md new file mode 100644 index 000000000..a8b20bdc9 --- /dev/null +++ b/docs/api/catalog.md @@ -0,0 +1,3 @@ +# Catalog + +::: pystac.Catalog diff --git a/docs/api/catalog.rst b/docs/api/catalog.rst deleted file mode 100644 index 99bfd1290..000000000 --- a/docs/api/catalog.rst +++ /dev/null @@ -1,12 +0,0 @@ -pystac.catalog -============== - -.. autoclass:: pystac.catalog.Catalog - :members: - :undoc-members: - :noindex: - -.. automodule:: pystac.catalog - :members: - :undoc-members: - :exclude-members: Catalog diff --git a/docs/api/collection.md b/docs/api/collection.md new file mode 100644 index 000000000..4afda31fe --- /dev/null +++ b/docs/api/collection.md @@ -0,0 +1,3 @@ +# Collection + +::: pystac.Collection diff --git a/docs/api/collection.rst b/docs/api/collection.rst deleted file mode 100644 index de435c23a..000000000 --- a/docs/api/collection.rst +++ /dev/null @@ -1,12 +0,0 @@ -pystac.collection -================= - -.. autoclass:: pystac.collection.Collection - :members: - :undoc-members: - :noindex: - -.. automodule:: pystac.collection - :members: - :undoc-members: - :exclude-members: Collection diff --git a/docs/api/common_metadata.rst b/docs/api/common_metadata.rst deleted file mode 100644 index f3eb46a57..000000000 --- a/docs/api/common_metadata.rst +++ /dev/null @@ -1,7 +0,0 @@ -pystac.common_metadata -====================== - -.. automodule:: pystac.common_metadata - :members: - :undoc-members: - :noindex: diff --git a/docs/api/constants.md b/docs/api/constants.md new file mode 100644 index 000000000..709b5f20b --- /dev/null +++ b/docs/api/constants.md @@ -0,0 +1,3 @@ +# Constants + +::: pystac.constants diff --git a/docs/api/container.md b/docs/api/container.md new file mode 100644 index 000000000..df2105162 --- /dev/null +++ b/docs/api/container.md @@ -0,0 +1,3 @@ +# Container + +::: pystac.Container diff --git a/docs/api/errors.rst b/docs/api/errors.rst deleted file mode 100644 index 62f1ea18e..000000000 --- a/docs/api/errors.rst +++ /dev/null @@ -1,7 +0,0 @@ -pystac.errors -============= - -.. automodule:: pystac.errors - :members: - :undoc-members: - :noindex: diff --git a/docs/api/extensions.rst b/docs/api/extensions.rst deleted file mode 100644 index 44cc8af32..000000000 --- a/docs/api/extensions.rst +++ /dev/null @@ -1,35 +0,0 @@ -pystac.extensions -================= - -.. toctree:: - :hidden: - :maxdepth: 2 - :glob: - - extensions/* - - -.. currentmodule:: pystac.extensions - -.. autosummary:: - - classification.ClassificationExtension - datacube.DatacubeExtension - eo.EOExtension - file.FileExtension - grid.GridExtension - item_assets.ItemAssetsExtension - mgrs.MgrsExtension - pointcloud.PointcloudExtension - projection.ProjectionExtension - raster.RasterExtension - render.RenderExtension - sar.SarExtension - sat.SatExtension - scientific.ScientificExtension - storage.StorageExtension - table.TableExtension - timestamps.TimestampsExtension - version.VersionExtension - view.ViewExtension - xarray_assets.XarrayAssetsExtension diff --git a/docs/api/extensions/base.rst b/docs/api/extensions/base.rst deleted file mode 100644 index 2085f3f94..000000000 --- a/docs/api/extensions/base.rst +++ /dev/null @@ -1,6 +0,0 @@ -pytac.extensions.base -===================== - -.. automodule:: pystac.extensions.base - :members: - :undoc-members: diff --git a/docs/api/extensions/classification.rst b/docs/api/extensions/classification.rst deleted file mode 100644 index 5b6e79aec..000000000 --- a/docs/api/extensions/classification.rst +++ /dev/null @@ -1,6 +0,0 @@ -pytac.extensions.classification -=============================== - -.. automodule:: pystac.extensions.classification - :members: - :undoc-members: diff --git a/docs/api/extensions/datacube.rst b/docs/api/extensions/datacube.rst deleted file mode 100644 index 24204dd26..000000000 --- a/docs/api/extensions/datacube.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.datacube -========================== - -.. automodule:: pystac.extensions.datacube - :members: - :undoc-members: diff --git a/docs/api/extensions/eo.rst b/docs/api/extensions/eo.rst deleted file mode 100644 index c46515d09..000000000 --- a/docs/api/extensions/eo.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.eo -==================== - -.. automodule:: pystac.extensions.eo - :members: - :undoc-members: diff --git a/docs/api/extensions/ext.rst b/docs/api/extensions/ext.rst deleted file mode 100644 index 23708b444..000000000 --- a/docs/api/extensions/ext.rst +++ /dev/null @@ -1,7 +0,0 @@ -pytac.extensions.ext -==================== - -.. automodule:: pystac.extensions.ext - :members: - :inherited-members: - :undoc-members: diff --git a/docs/api/extensions/file.rst b/docs/api/extensions/file.rst deleted file mode 100644 index 2e33794e1..000000000 --- a/docs/api/extensions/file.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.file -====================== - -.. automodule:: pystac.extensions.file - :members: - :undoc-members: diff --git a/docs/api/extensions/grid.rst b/docs/api/extensions/grid.rst deleted file mode 100644 index b8f31ca6e..000000000 --- a/docs/api/extensions/grid.rst +++ /dev/null @@ -1,6 +0,0 @@ -pytac.extensions.grid -===================== - -.. automodule:: pystac.extensions.grid - :members: - :undoc-members: diff --git a/docs/api/extensions/hooks.rst b/docs/api/extensions/hooks.rst deleted file mode 100644 index 949847bb4..000000000 --- a/docs/api/extensions/hooks.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.hooks -======================= - -.. automodule:: pystac.extensions.hooks - :members: - :undoc-members: diff --git a/docs/api/extensions/index.md b/docs/api/extensions/index.md new file mode 100644 index 000000000..d89c672d2 --- /dev/null +++ b/docs/api/extensions/index.md @@ -0,0 +1,3 @@ +# Extensions + +::: pystac.extensions diff --git a/docs/api/extensions/item_assets.rst b/docs/api/extensions/item_assets.rst deleted file mode 100644 index c46ce768d..000000000 --- a/docs/api/extensions/item_assets.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.item\_assets -============================== - -.. automodule:: pystac.extensions.item_assets - :members: - :undoc-members: diff --git a/docs/api/extensions/mgrs.rst b/docs/api/extensions/mgrs.rst deleted file mode 100644 index 5bcb6b389..000000000 --- a/docs/api/extensions/mgrs.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.mgrs -============================ - -.. automodule:: pystac.extensions.mgrs - :members: - :undoc-members: diff --git a/docs/api/extensions/pointcloud.rst b/docs/api/extensions/pointcloud.rst deleted file mode 100644 index 24dde5031..000000000 --- a/docs/api/extensions/pointcloud.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.pointcloud -============================ - -.. automodule:: pystac.extensions.pointcloud - :members: - :undoc-members: diff --git a/docs/api/extensions/projection.md b/docs/api/extensions/projection.md new file mode 100644 index 000000000..e3831f025 --- /dev/null +++ b/docs/api/extensions/projection.md @@ -0,0 +1,3 @@ +# Projection + +::: pystac.extensions.ProjectionExtension diff --git a/docs/api/extensions/projection.rst b/docs/api/extensions/projection.rst deleted file mode 100644 index 65f079f8d..000000000 --- a/docs/api/extensions/projection.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.projection -============================ - -.. automodule:: pystac.extensions.projection - :members: - :undoc-members: diff --git a/docs/api/extensions/raster.rst b/docs/api/extensions/raster.rst deleted file mode 100644 index 7f67d824d..000000000 --- a/docs/api/extensions/raster.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.raster -======================== - -.. automodule:: pystac.extensions.raster - :members: - :undoc-members: diff --git a/docs/api/extensions/render.rst b/docs/api/extensions/render.rst deleted file mode 100644 index b7b13ba1e..000000000 --- a/docs/api/extensions/render.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.render -======================== - -.. automodule:: pystac.extensions.render - :members: - :undoc-members: diff --git a/docs/api/extensions/sar.rst b/docs/api/extensions/sar.rst deleted file mode 100644 index 9f48cc5c8..000000000 --- a/docs/api/extensions/sar.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.sar -===================== - -.. automodule:: pystac.extensions.sar - :members: - :undoc-members: diff --git a/docs/api/extensions/sat.rst b/docs/api/extensions/sat.rst deleted file mode 100644 index 793a47862..000000000 --- a/docs/api/extensions/sat.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.sat -===================== - -.. automodule:: pystac.extensions.sat - :members: - :undoc-members: diff --git a/docs/api/extensions/scientific.rst b/docs/api/extensions/scientific.rst deleted file mode 100644 index 83d06361e..000000000 --- a/docs/api/extensions/scientific.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.scientific -============================ - -.. automodule:: pystac.extensions.scientific - :members: - :undoc-members: diff --git a/docs/api/extensions/storage.rst b/docs/api/extensions/storage.rst deleted file mode 100644 index 420e5dfb1..000000000 --- a/docs/api/extensions/storage.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.storage -============================ - -.. automodule:: pystac.extensions.storage - :members: - :undoc-members: diff --git a/docs/api/extensions/table.rst b/docs/api/extensions/table.rst deleted file mode 100644 index 80abd7dc1..000000000 --- a/docs/api/extensions/table.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.table -======================= - -.. automodule:: pystac.extensions.table - :members: - :undoc-members: diff --git a/docs/api/extensions/timestamps.rst b/docs/api/extensions/timestamps.rst deleted file mode 100644 index da5758255..000000000 --- a/docs/api/extensions/timestamps.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.timestamps -============================ - -.. automodule:: pystac.extensions.timestamps - :members: - :undoc-members: diff --git a/docs/api/extensions/version.rst b/docs/api/extensions/version.rst deleted file mode 100644 index 4c606ce75..000000000 --- a/docs/api/extensions/version.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.version -========================= - -.. automodule:: pystac.extensions.version - :members: - :undoc-members: diff --git a/docs/api/extensions/view.rst b/docs/api/extensions/view.rst deleted file mode 100644 index 1b395df28..000000000 --- a/docs/api/extensions/view.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.view -====================== - -.. automodule:: pystac.extensions.view - :members: - :undoc-members: diff --git a/docs/api/extensions/xarray_assets.rst b/docs/api/extensions/xarray_assets.rst deleted file mode 100644 index 494093068..000000000 --- a/docs/api/extensions/xarray_assets.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.extensions.xarray_assets -=============================== - -.. automodule:: pystac.extensions.xarray_assets - :members: - :undoc-members: diff --git a/docs/api/general.md b/docs/api/general.md new file mode 100644 index 000000000..857ea289c --- /dev/null +++ b/docs/api/general.md @@ -0,0 +1,11 @@ +# General + +::: pystac.get_stac_version +::: pystac.set_stac_version +::: pystac.read_file +::: pystac.read_dict +::: pystac.write_file +::: pystac.PySTACError +::: pystac.STACError +::: pystac.PySTACWarning +::: pystac.STACWarning diff --git a/docs/api/io.md b/docs/api/io.md new file mode 100644 index 000000000..cd7a669fd --- /dev/null +++ b/docs/api/io.md @@ -0,0 +1,4 @@ +# Input and output + +::: pystac.io +::: pystac.obstore diff --git a/docs/api/item.md b/docs/api/item.md new file mode 100644 index 000000000..ddeb00b81 --- /dev/null +++ b/docs/api/item.md @@ -0,0 +1,3 @@ +# Item + +::: pystac.Item diff --git a/docs/api/item.rst b/docs/api/item.rst deleted file mode 100644 index aedf84bcd..000000000 --- a/docs/api/item.rst +++ /dev/null @@ -1,12 +0,0 @@ -pystac.item -=========== - -.. autoclass:: pystac.item.Item - :members: - :undoc-members: - :noindex: - -.. automodule:: pystac.item - :members: - :undoc-members: - :exclude-members: Item diff --git a/docs/api/item_assets.rst b/docs/api/item_assets.rst deleted file mode 100644 index 501c56420..000000000 --- a/docs/api/item_assets.rst +++ /dev/null @@ -1,7 +0,0 @@ -pystac.item_assets -================== - -.. automodule:: pystac.item_assets - :members: - :undoc-members: - :noindex: diff --git a/docs/api/item_collection.rst b/docs/api/item_collection.rst deleted file mode 100644 index 9b01f7afa..000000000 --- a/docs/api/item_collection.rst +++ /dev/null @@ -1,12 +0,0 @@ -pystac.item_collection -====================== - -.. autoclass:: pystac.item_collection.ItemCollection - :members: - :undoc-members: - :noindex: - -.. automodule:: pystac.item_collection - :members: - :undoc-members: - :exclude-members: ItemCollection diff --git a/docs/api/layout.rst b/docs/api/layout.rst deleted file mode 100644 index 08853cf26..000000000 --- a/docs/api/layout.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.layout -============= - -.. automodule:: pystac.layout - :members: - :undoc-members: diff --git a/docs/api/link.rst b/docs/api/link.rst deleted file mode 100644 index 9756ef0b6..000000000 --- a/docs/api/link.rst +++ /dev/null @@ -1,12 +0,0 @@ -pystac.link -=========== - -.. autoclass:: pystac.link.Link - :members: - :undoc-members: - :noindex: - -.. automodule:: pystac.link - :members: - :undoc-members: - :exclude-members: Link diff --git a/docs/api/media_type.rst b/docs/api/media_type.rst deleted file mode 100644 index 174f42d0c..000000000 --- a/docs/api/media_type.rst +++ /dev/null @@ -1,7 +0,0 @@ -pystac.media_type -================= - -.. automodule:: pystac.media_type - :members: - :undoc-members: - :noindex: diff --git a/docs/api/provider.rst b/docs/api/provider.rst deleted file mode 100644 index b1f090e7d..000000000 --- a/docs/api/provider.rst +++ /dev/null @@ -1,7 +0,0 @@ -pystac.provider -=============== - -.. automodule:: pystac.provider - :members: - :undoc-members: - :noindex: diff --git a/docs/api/pystac.rst b/docs/api/pystac.rst deleted file mode 100644 index e16c4692d..000000000 --- a/docs/api/pystac.rst +++ /dev/null @@ -1,227 +0,0 @@ -pystac ------- - -.. automodule:: pystac - :members: read_file, write_file, read_dict, set_stac_version, get_stac_version - - .. autosummary:: - STACObject - Catalog - Collection - Extent - SpatialExtent - TemporalExtent - Provider - Summaries - Item - Asset - ItemAssetDefinition - CommonMetadata - ItemCollection - Link - StacIO - read_file - write_file - read_dict - set_stac_version - get_stac_version - - -STACObject ----------- - -.. autoclass:: pystac.STACObject - :members: - :inherited-members: - :undoc-members: - -.. autoclass:: pystac.STACObjectType - :members: - :undoc-members: - -Catalog -------- - -.. autoclass:: pystac.Catalog - :members: - :inherited-members: - :undoc-members: - -CatalogType ------------ - -.. autoclass:: pystac.CatalogType - :members: - :inherited-members: - :undoc-members: - -Collection ----------- - -.. autoclass:: pystac.Collection - :members: - :inherited-members: - :undoc-members: - -Extent ------- - -.. autoclass:: pystac.Extent - :members: - :undoc-members: - -SpatialExtent -------------- - -.. autoclass:: pystac.SpatialExtent - :members: - :undoc-members: - -TemporalExtent --------------- - -.. autoclass:: pystac.TemporalExtent - :members: - :undoc-members: - -ProviderRole ------------- - -.. autoclass:: pystac.ProviderRole - :members: - :undoc-members: - -Provider --------- - -.. autoclass:: pystac.Provider - :members: - :undoc-members: - -Summaries ---------- - -.. autoclass:: pystac.Summaries - :members: - :undoc-members: - -Item ----- - -.. autoclass:: pystac.Item - :members: - :inherited-members: - :undoc-members: - -Asset ------ - -.. autoclass:: pystac.Asset - :members: - :undoc-members: - -ItemAssetDefinition -------------------- - -.. autoclass:: pystac.ItemAssetDefinition - :members: - :undoc-members: - - -CommonMetadata --------------- - -.. autoclass:: pystac.CommonMetadata - :members: - :undoc-members: - -ItemCollection --------------- - -.. autoclass:: pystac.ItemCollection - :members: - :inherited-members: - :undoc-members: - -Link ----- - -.. autoclass:: pystac.Link - :members: - :inherited-members: - :undoc-members: - -MediaType ---------- - -.. autoclass:: pystac.MediaType - :members: - :undoc-members: - -RelType -------- - -.. autoclass:: pystac.RelType - :members: - :undoc-members: - -StacIO ------- - -.. autoclass:: pystac.StacIO - :members: - :undoc-members: - -Errors ------- - -STACError -~~~~~~~~~ - -.. autoclass:: pystac.STACError - -STACTypeError -~~~~~~~~~~~~~ - -.. autoclass:: pystac.STACTypeError - -DuplicateObjectKeyError -~~~~~~~~~~~~~~~~~~~~~~~ - -.. autoclass:: pystac.DuplicateObjectKeyError - -ExtensionAlreadyExistsError -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. autoclass:: pystac.ExtensionAlreadyExistsError - -ExtensionTypeError -~~~~~~~~~~~~~~~~~~ - -.. autoclass:: pystac.ExtensionTypeError - -ExtensionNotImplemented -~~~~~~~~~~~~~~~~~~~~~~~ - -.. autoclass:: pystac.ExtensionNotImplemented - -RequiredPropertyMissing -~~~~~~~~~~~~~~~~~~~~~~~ - -.. autoclass:: pystac.RequiredPropertyMissing - -STACValidationError -~~~~~~~~~~~~~~~~~~~ - -.. autoclass:: pystac.STACValidationError - -TemplateError -~~~~~~~~~~~~~ - -.. autoclass:: pystac.TemplateError - - -DeprecatedWarning -~~~~~~~~~~~~~~~~~ - -.. autoclass:: pystac.DeprecatedWarning diff --git a/docs/api/rel_type.rst b/docs/api/rel_type.rst deleted file mode 100644 index a13c99f86..000000000 --- a/docs/api/rel_type.rst +++ /dev/null @@ -1,7 +0,0 @@ -pystac.rel_type -=============== - -.. automodule:: pystac.rel_type - :members: - :undoc-members: - :noindex: diff --git a/docs/api/render.md b/docs/api/render.md new file mode 100644 index 000000000..496ff5f3e --- /dev/null +++ b/docs/api/render.md @@ -0,0 +1,3 @@ +# Render + +::: pystac.render diff --git a/docs/api/serialization.rst b/docs/api/serialization.rst deleted file mode 100644 index 4a21bea46..000000000 --- a/docs/api/serialization.rst +++ /dev/null @@ -1,12 +0,0 @@ -pystac.serialization -==================== - -.. toctree:: - :hidden: - :maxdepth: 2 - :glob: - - serialization/* - -.. automodule:: pystac.serialization - :members: diff --git a/docs/api/serialization/common_properties.rst b/docs/api/serialization/common_properties.rst deleted file mode 100644 index 5666ca965..000000000 --- a/docs/api/serialization/common_properties.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.serialization.common\_properties -======================================= - -.. automodule:: pystac.serialization.common_properties - :members: - :undoc-members: diff --git a/docs/api/serialization/identify.rst b/docs/api/serialization/identify.rst deleted file mode 100644 index a54e38f62..000000000 --- a/docs/api/serialization/identify.rst +++ /dev/null @@ -1,20 +0,0 @@ -pystac.serialization.identify -============================= - - -.. automodule:: pystac.serialization.identify - :members: STACVersionRange, identify_stac_object, identify_stac_object_type - :undoc-members: - :noindex: - - -.. autoclass:: pystac.serialization.identify.STACVersionRange - :members: - :undoc-members: - :noindex: - - -.. automodule:: pystac.serialization.identify - :members: - :undoc-members: - :exclude-members: STACVersionRange, identify_stac_object, identify_stac_object_type diff --git a/docs/api/serialization/migrate.rst b/docs/api/serialization/migrate.rst deleted file mode 100644 index df03f05ec..000000000 --- a/docs/api/serialization/migrate.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.serialization.migrate -============================ - -.. automodule:: pystac.serialization.migrate - :members: - :undoc-members: diff --git a/docs/api/stac_io.rst b/docs/api/stac_io.rst deleted file mode 100644 index 611d276f0..000000000 --- a/docs/api/stac_io.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.stac_io -============== - -.. automodule:: pystac.stac_io - :members: - :undoc-members: diff --git a/docs/api/stac_object.md b/docs/api/stac_object.md new file mode 100644 index 000000000..3eb3ba257 --- /dev/null +++ b/docs/api/stac_object.md @@ -0,0 +1,3 @@ +# STACObject + +::: pystac.STACObject diff --git a/docs/api/stac_object.rst b/docs/api/stac_object.rst deleted file mode 100644 index a5a37fcec..000000000 --- a/docs/api/stac_object.rst +++ /dev/null @@ -1,19 +0,0 @@ -pystac.stac_object -================== - -.. autoclass:: pystac.stac_object.STACObject - :members: - :undoc-members: - :noindex: - -.. autoclass:: pystac.stac_object.STACObjectType - :members: - :undoc-members: - :noindex: - -.. automodule:: pystac.stac_object - :members: - :undoc-members: - :exclude-members: STACObject, STACObjectType - -.. autoclass:: pystac.stac_object.S diff --git a/docs/api/summaries.rst b/docs/api/summaries.rst deleted file mode 100644 index e7a7191d8..000000000 --- a/docs/api/summaries.rst +++ /dev/null @@ -1,12 +0,0 @@ -pystac.summaries -================ - -.. autoclass:: pystac.summaries.Summaries - :members: - :undoc-members: - :noindex: - -.. automodule:: pystac.summaries - :members: - :undoc-members: - :exclude-members: Summaries diff --git a/docs/api/utils.rst b/docs/api/utils.rst deleted file mode 100644 index 83f148ccf..000000000 --- a/docs/api/utils.rst +++ /dev/null @@ -1,10 +0,0 @@ -pystac.utils -============ - -.. automodule:: pystac.utils - :members: - :undoc-members: - -.. autoclass:: pystac.utils.T - -.. autoclass:: pystac.utils.U diff --git a/docs/api/validate.md b/docs/api/validate.md new file mode 100644 index 000000000..6b4c81510 --- /dev/null +++ b/docs/api/validate.md @@ -0,0 +1,4 @@ +# Validate + +::: pystac.validate.base +::: pystac.validate.jsonschema diff --git a/docs/api/validation.rst b/docs/api/validation.rst deleted file mode 100644 index 67f0ff09b..000000000 --- a/docs/api/validation.rst +++ /dev/null @@ -1,13 +0,0 @@ -pystac.validation -================= - -.. toctree:: - :hidden: - :maxdepth: 2 - :glob: - - validation/* - -.. automodule:: pystac.validation - :members: - :undoc-members: diff --git a/docs/api/validation/local_validator.rst b/docs/api/validation/local_validator.rst deleted file mode 100644 index 09096e349..000000000 --- a/docs/api/validation/local_validator.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.validation.local\_validator -================================== - -.. automodule:: pystac.validation.local_validator - :members: - :undoc-members: diff --git a/docs/api/validation/schema_uri_map.rst b/docs/api/validation/schema_uri_map.rst deleted file mode 100644 index 231bd2f28..000000000 --- a/docs/api/validation/schema_uri_map.rst +++ /dev/null @@ -1,6 +0,0 @@ -pystac.validation.schema\_uri\_map -================================== - -.. automodule:: pystac.validation.schema_uri_map - :members: - :undoc-members: diff --git a/docs/api/validation/stac_validator.rst b/docs/api/validation/stac_validator.rst deleted file mode 100644 index 651a5c669..000000000 --- a/docs/api/validation/stac_validator.rst +++ /dev/null @@ -1,12 +0,0 @@ -pystac.validation.stac\_validator -================================= - -.. autoclass:: pystac.validation.stac_validator.JsonSchemaSTACValidator - :members: - :undoc-members: - :noindex: - -.. automodule:: pystac.validation.stac_validator - :members: - :undoc-members: - :exclude-members: JsonSchemaSTACValidator diff --git a/docs/api/version.rst b/docs/api/version.rst deleted file mode 100644 index ea856258f..000000000 --- a/docs/api/version.rst +++ /dev/null @@ -1,7 +0,0 @@ -pystac.version -============== - -.. automodule:: pystac.version - :members: - :undoc-members: - :noindex: diff --git a/docs/concepts.rst b/docs/concepts.rst deleted file mode 100644 index 2c320f1e1..000000000 --- a/docs/concepts.rst +++ /dev/null @@ -1,1004 +0,0 @@ -Concepts -######## - -This page will give an overview of some important concepts to understand when working -with PySTAC. If you want to check code examples, see the :ref:`tutorials`. - -.. _stac_version_support: - -STAC Spec Version Support -========================= - -The latest version of PySTAC supports STAC Spec |stac_version| and will automatically -update any catalogs to this version. To work with older versions of the STAC Spec, -please use an older version of PySTAC: - -================= ============== -STAC Spec Version PySTAC Version -================= ============== ->=1.0 Latest -0.9 0.4.* -0.8 0.3.* -<0.8 *Not supported* -================= ============== - -Reading STACs -============= - -PySTAC can read STAC data from JSON. Generally users read in the root catalog, and then -use the python objects to crawl through the data. Once you read in the root of the STAC, -you can work with the STAC in memory. - -.. code-block:: python - - from pystac import Catalog - - catalog = Catalog.from_file('/some/example/catalog.json') - - for root, catalogs, items in catalog.walk(): - # Do interesting things with the STAC data. - -To see how to hook into PySTAC for reading from alternate URIs such as cloud object -storage, see :ref:`using stac_io`. - -Writing STACs -============= - -While working with STACs in-memory don't require setting file paths, in order to save a -STAC, you'll need to give each STAC object a ``self`` link that describes the location -of where it should be saved to. Luckily, PySTAC makes it easy to create a STAC catalog -with a :stac-spec:`canonical layout ` and with the -links that follow the :stac-spec:`best practices `. You -simply call ``normalize_hrefs`` with the root directory of where the STAC will be saved, -and then call ``save`` with the type of catalog (described in the :ref:`catalog types` -section) that matches your use case. - -.. code-block:: python - - from pystac import (Catalog, CatalogType) - - catalog = Catalog.from_file('/some/example/catalog.json') - catalog.normalize_hrefs('/some/copy/') - catalog.save(catalog_type=CatalogType.SELF_CONTAINED) - - copycat = Catalog.from_file('/some/copy/catalog.json') - - -Normalizing HREFs ------------------ - -The ``normalize_hrefs`` call sets HREFs for all the links in the STAC according to the -Catalog, Collection and Items, all based off of the root URI that is passed in: - -.. code-block:: python - - catalog.normalize_hrefs('/some/location') - catalog.save(catalog_type=CatalogType.SELF_CONTAINED) - -This will lay out the HREFs of the STAC according to the :stac-spec:`best practices -document `. - -Layouts -~~~~~~~ - -PySTAC provides a few different strategies for laying out the HREFs of a STAC. -To use them you can pass in a strategy when instantiating a catalog or when -calling `normalize_hrefs`. - -Using templates -''''''''''''''' - -You can utilize template strings to determine the file paths of HREFs set on Catalogs, -Collection or Items. These templates use python format strings, which can name -the property or attribute of the item you want to use for replacing the template -variable. For example: - -.. code-block:: python - - from pystac.layout import TemplateLayoutStrategy - - strategy = TemplateLayoutStrategy(item_template="${collection}/${year}/${month}") - catalog.normalize_hrefs('/some/location', strategy=strategy) - catalog.save(catalog_type=CatalogType.SELF_CONTAINED) - -The above code will save items in subfolders based on the collection ID, year and month -of it's datetime (or start_datetime if a date range is defined and no datetime is -defined). Note that the forward slash (``/``) should be used as path separator in the -template string regardless of the system path separator (thus both in POSIX-compliant -and Windows environments). - -You can use dot notation to specify attributes of objects or keys in dictionaries for -template variables. PySTAC will look at the object, it's ``properties`` and its -``extra_fields`` for property names or dictionary keys. Some special cases, like -``year``, ``month``, ``day`` and ``date`` exist for datetime on Items, as well as -``collection`` for Item's Collection's ID. - -See the documentation on :class:`~pystac.layout.LayoutTemplate` for more documentation -on how layout templates work. - -Using custom functions -'''''''''''''''''''''' - -If you want to build your own strategy, you can subclass ``HrefLayoutStrategy`` or use -:class:`~pystac.layout.CustomLayoutStrategy` to provide functions that work with -Catalogs, Collections or Items. Similar to the templating strategy, you can provide a -fallback strategy (which defaults to -:class:`~pystac.layout.BestPracticesLayoutStrategy`) for any stac object type that you -don't supply a function for. - - -Set a default catalog layout strategy -''''''''''''''''''''''''''''''''''''' - -Instead of fixing the HREFs of child objects retrospectively using `normalize_hrefs`, -you can also define a default strategy for a catalog. When instantiating a catalog, -pass in a custom strategy and base href. Consequently, the HREFs of all child -objects and items added to the catalog tree will be set correctly using that strategy. - - -.. code-block:: python - - from pystac import Catalog, Collection, Item - - catalog = Catalog(..., - href="/some/location/catalog.json", - strategy=custom_strategy) - collection = Collection(...) - item = Item(...) - catalog.add_child(collection) - collection.add_item(item) - catalog.save() - - -.. _catalog types: - -Catalog Types -------------- - -The STAC :stac-spec:`best practices document ` lays out different -catalog types, and how their links should be formatted. A brief description is below, -but check out the document for the official take on these types: - -The catalog types will also dictate the asset HREF formats. Asset HREFs in any catalog -type can be relative or absolute may be absolute depending on their location; see the -section on :ref:`rel vs abs asset` below. - - -Self-Contained Catalogs -~~~~~~~~~~~~~~~~~~~~~~~ - -A self-contained catalog (indicated by ``catalog_type=CatalogType.SELF_CONTAINED``) -applies to STACs that do not have a long term location, and can be moved around. These -STACs are useful for copying data to and from locations, without having to change any -link metadata. - -A self-contained catalog has two important properties: - -- It contains only relative links -- It contains **no** self links. - -For a catalog that is the most easy to copy around, it's recommended that item assets -use relative links, and reside in the same directory as the item's STAC metadata file. - -Relative Published Catalogs -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A relative published catalog (indicated by -``catalog_type=CatalogType.RELATIVE_PUBLISHED``) is one that is tied at it's root to a -specific location, but otherwise contains relative links. This is designed so that a -self-contained catalog can be 'published' online by just adding one field (the self -link) to its root catalog. - -A relative published catalog has the following properties: - -- It contains **only one** self link: the root of the catalog contains a (necessarily - absolute) link to it's published location. -- All other objects in the STAC contain relative links, and no self links. - - -Absolute Published Catalogs -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An absolute published catalog (indicated by -``catalog_type=CatalogType.ABSOLUTE_PUBLISHED``) uses absolute links for everything. It -is preferable where possible, since it allows for the easiest provenance tracking out of -all the catalog types. - -An absolute published catalog has the following properties: - -- Each STAC object contains only absolute links. -- Each STAC object has a self link. - -It is not recommended to have relative asset HREFs in an absolute published catalog. - - -Relative vs Absolute HREFs --------------------------- - -HREFs inside a STAC for either links or assets can be relative or absolute. - -Relative vs Absolute Link HREFs -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Absolute links point to their file locations in a fully described way. Relative links -are relative to the linking object's file location. For example, if a catalog at -``/some/location/catalog.json`` has a link to an item that has an HREF set to -``item-id/item-id.json``, then that link should resolve to the absolute path -``/some/location/item-id/item-id.json``. - -Links are set as absolute or relative HREFs at save time, as determine by the root -catalog's catalog_type :attr:`~pystac.Catalog.catalog_type`. This means that, even if -the stored HREF of the link is absolute, if the root -``catalog_type=CatalogType.RELATIVE_PUBLISHED`` or -``catalog_type=CatalogType.SELF_CONTAINED`` and subsequent serializing of the any links -in the catalog will produce a relative link, based on the self link of the parent -object. - -You can make all the links of a catalog relative or absolute by setting the -:func:`~pystac.Catalog.catalog_type` field then resaving the entire catalog. - -.. _rel vs abs asset: - -Relative vs Absolute Asset HREFs -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Asset HREFs can also be relative or absolute. If an asset HREF is relative, then it is -relative to the Item's metadata file. For example, if the item at -``/some/location/item-id/item-id.json`` had an asset with an HREF of ``./image.tif``, -then the fully resolved path for that image would be -``/some/location/item-id/image.tif`` - -You can make all the asset HREFs of a catalog relative or absolute using the -:func:`Catalog.make_all_asset_hrefs_relative -` and -:func:`Catalog.make_all_asset_hrefs_absolute -` methods. Note that these will not move -any files around, and if the file location does not share a common parent with the -asset's item's self HREF, then the asset HREF will remain absolute as no relative path -is possible. - -Including a ``self`` link -------------------------- - -Every stac object has a :func:`~pystac.STACObject.save_object` method, that takes as an -argument whether or not to include the object's self link. As noted in the section on -:ref:`catalog types`, a self link is necessarily absolute; if an object only contains -relative links, then it cannot contain the self link. PySTAC uses self links as a way of -tracking the object's file location, either what it was read from or it's pending save -location, so each object can have a self link even if you don't ever want that self link -written (e.g. if you are working with self-contained catalogs). - -.. _using stac_io: - -I/O in PySTAC -============= - -The :class:`pystac.StacIO` class defines fundamental methods for I/O -operations within PySTAC, including serialization and deserialization to and from -JSON files and conversion to and from Python dictionaries. This is an abstract class -and should not be instantiated directly. However, PySTAC provides a -:class:`pystac.stac_io.DefaultStacIO` class with minimal implementations of these -methods. This default implementation provides support for reading and writing files -from the local filesystem as well as HTTP URIs (using ``urllib``). This class is -created automatically by all of the object-specific I/O methods (e.g. -:meth:`pystac.Catalog.from_file`), so most users will not need to instantiate this -class themselves. - -If you are dealing with a STAC catalog with URIs that require authentication. -It is possible provide auth headers (or any other customer headers) to the -:class:`pystac.stac_io.DefaultStacIO`. - -.. code-block:: python - - from pystac import Catalog - from pystac import StacIO - - stac_io = StacIO.default() - stac_io.headers = {"Authorization": ""} - - catalog = Catalog.from_file("", stac_io=stac_io) - -You can double check that requests PySTAC is making by adjusting logging level so -that you see all API calls. - -.. code-block:: python - - import logging - - logging.basicConfig() - logger = logging.getLogger('pystac') - logger.setLevel(logging.DEBUG) - -If you require more custom logic for I/O operations or would like to use a -3rd-party library for I/O operations (e.g. ``requests``), -you can create a sub-class of :class:`pystac.StacIO` -(or :class:`pystac.stac_io.DefaultStacIO`) and customize the methods as -you see fit. You can then pass instances of this custom sub-class into the ``stac_io`` -argument of most object-specific I/O methods. You can also use -:meth:`pystac.StacIO.set_default` in your client's ``__init__.py`` file to make this -sub-class the default :class:`pystac.StacIO` implementation throughout the library. - -For example, the following code examples will allow -for reading from AWS's S3 cloud object storage using `boto3 -`__ -or Azure Blob Storage using the `Azure SDK for Python -`__: - -.. tab-set:: - .. tab-item:: AWS S3 - - .. code-block:: python - - from urllib.parse import urlparse - import boto3 - from pystac import Link - from pystac.stac_io import DefaultStacIO, StacIO - from typing import Union, Any - - class CustomStacIO(DefaultStacIO): - def __init__(self): - self.s3 = boto3.resource("s3") - super().__init__() - - def read_text( - self, source: Union[str, Link], *args: Any, **kwargs: Any - ) -> str: - parsed = urlparse(source) - if parsed.scheme == "s3": - bucket = parsed.netloc - key = parsed.path[1:] - - obj = self.s3.Object(bucket, key) - return obj.get()["Body"].read().decode("utf-8") - else: - return super().read_text(source, *args, **kwargs) - - def write_text( - self, dest: Union[str, Link], txt: str, *args: Any, **kwargs: Any - ) -> None: - parsed = urlparse(dest) - if parsed.scheme == "s3": - bucket = parsed.netloc - key = parsed.path[1:] - self.s3.Object(bucket, key).put(Body=txt, ContentEncoding="utf-8") - else: - super().write_text(dest, txt, *args, **kwargs) - - StacIO.set_default(CustomStacIO) - - .. tab-item:: Azure Blob Storage - - .. code-block:: python - - import os - import re - from typing import Any, Dict, Optional, Tuple, Union - from urllib.parse import urlparse - - from azure.core.credentials import ( - AzureNamedKeyCredential, - AzureSasCredential, - TokenCredential, - ) - from azure.storage.blob import BlobClient, ContentSettings - from pystac import Link - from pystac.stac_io import DefaultStacIO - - BLOB_HTTPS_URI_PATTERN = r"https:\/\/(.+?)\.blob\.core\.windows\.net" - - AzureCredentialType = Union[ - str, - Dict[str, str], - AzureNamedKeyCredential, - AzureSasCredential, - TokenCredential, - ] - - - class BlobStacIO(DefaultStacIO): - """A custom StacIO class for reading and writing STAC objects - from/to Azure Blob storage. - """ - - conn_str: Optional[str] = os.getenv("AZURE_STORAGE_CONNECTION_STRING") - account_url: Optional[str] = None - credential: Optional[AzureCredentialType] = None - overwrite: bool = True - - def _is_blob_uri(self, href: str) -> bool: - """Check if href matches Blob URI pattern.""" - if re.search( - re.compile(BLOB_HTTPS_URI_PATTERN), href - ) is not None or href.startswith("abfs://"): - return True - else: - return False - - def _parse_blob_uri(self, uri: str) -> Tuple[str, str]: - """Parse the container and blob name from a Blob URI. - - Parameters - ---------- - uri - An Azure Blob URI. - - Returns - ------- - The container and blob names. - """ - if uri.startswith("abfs://"): - path = uri.replace("abfs://", "/") - else: - path = urlparse(uri).path - - parts = path.split("/") - container = parts[1] - blob = "/".join(parts[2:]) - return container, blob - - def _get_blob_client(self, uri: str) -> BlobClient: - """Instantiate a `BlobClient` given a container and blob. - - Parameters - ---------- - uri - An Azure Blob URI. - - Returns - ------- - A `BlobClient` for interacting with `blob` in `container`. - """ - container, blob = self._parse_blob_uri(uri) - - if self.conn_str: - return BlobClient.from_connection_string( - self.conn_str, - container_name=container, - blob_name=blob, - ) - elif self.account_url: - return BlobClient( - account_url=self.account_url, - container_name=container, - blob_name=blob, - credential=self.credential, - ) - else: - raise ValueError( - "Must set conn_str or account_url (and credential if required)" - ) - - def read_text(self, source: Union[str, Link], *args: Any, **kwargs: Any) -> str: - if isinstance(source, Link): - source = source.href - if self._is_blob_uri(source): - blob_client = self._get_blob_client(source) - obj = blob_client.download_blob().readall().decode() - return obj - else: - return super().read_text(source, *args, **kwargs) - - def write_text( - self, dest: Union[str, Link], txt: str, *args: Any, **kwargs: Any - ) -> None: - """Write STAC Objects to Blob storage. Note: overwrites by default.""" - if isinstance(dest, Link): - dest = dest.href - if self._is_blob_uri(dest): - blob_client = self._get_blob_client(dest) - blob_client.upload_blob( - txt, - overwrite=self.overwrite, - content_settings=ContentSettings(content_type="application/json"), - ) - else: - super().write_text(dest, txt, *args, **kwargs) - - - # set Blob storage connection string - BlobStacIO.conn_str = "my-storage-connection-string" - - # OR set Blob account URL, credential - BlobStacIO.account_url = "https://myblobstorageaccount.blob.core.windows.net" - BlobStacIO.credential = AzureSasCredential("my-sas-token") - - # modify overwrite behavior - BlobStacIO.overwrite = False - - # set BlobStacIO as default StacIO - StacIO.set_default(BlobStacIO) - -If you only need to customize read operations you can inherit from -:class:`~pystac.stac_io.DefaultStacIO` and only overwrite the read method. For example, -to take advantage of connection pooling using a `requests.Session -`__: - -.. code-block:: python - - from urllib.parse import urlparse - import requests - from pystac.stac_io import DefaultStacIO, StacIO - from typing import Union, Any - - class ConnectionPoolingIO(DefaultStacIO): - def __init__(self): - self.session = requests.Session() - - def read_text( - self, source: Union[str, Link], *args: Any, **kwargs: Any - ) -> str: - parsed = urlparse(uri) - if parsed.scheme.startswith("http"): - return self.session.get(uri).text - else: - return super().read_text(source, *args, **kwargs) - - StacIO.set_default(ConnectionPoolingIO) - - -.. _validation_concepts: - -Validation -========== - -PySTAC includes validation functionality that allows users to validate PySTAC objects as -well JSON-encoded STAC objects from STAC versions `0.8.0` and later. - -Enabling validation -------------------- - -To enable the validation feature you'll need to have installed PySTAC with the optional -dependency via: - -.. code-block:: bash - - > pip install pystac[validation] - -This installs the ``jsonschema`` package which is used with the default validator. If -you define your own validation class as described below, you are not required to have -this extra dependency. - -Validating PySTAC objects -------------------------- - -You can validate any :class:`~pystac.Catalog`, :class:`~pystac.Collection` or -:class:`~pystac.Item` by calling the :meth:`~pystac.STACObject.validate` method: - -.. code-block:: python - - item.validate() - -This validates against the latest set of JSON schemas (which are included with the -PySTAC package) or older versions (which are hosted at https://schemas.stacspec.org). -This validation includes any extensions that the object extends (these are always -accessed remotely based on their URIs). - -If there are validation errors, a :class:`~pystac.STACValidationError` -is raised. - -You can also call :meth:`~pystac.Catalog.validate_all` on a Catalog or Collection to -recursively walk through a catalog and validate all objects within it. - -.. code-block:: python - - catalog.validate_all() - -Validating STAC JSON --------------------- - -You can validate STAC JSON represented as a ``dict`` using the -:func:`pystac.validation.validate_dict` method: - -.. code-block:: python - - import json - from pystac.validation import validate_dict - - with open('/path/to/item.json') as f: - js = json.load(f) - validate_dict(js) - -You can also recursively validate all of the catalogs, collections and items across STAC -versions using the :func:`pystac.validation.validate_all` method: - -.. code-block:: python - - import json - from pystac.validation import validate_all - - with open('/path/to/catalog.json') as f: - js = json.load(f) - validate_all(js) - -Using your own validator ------------------------- - -By default PySTAC uses the :class:`~pystac.validation.JsonSchemaSTACValidator` -implementation for validation. Users can define their own implementations of -:class:`~pystac.validation.stac_validator.STACValidator` and register it with pystac -using :func:`pystac.validation.set_validator`. - -The :class:`~pystac.validation.JsonSchemaSTACValidator` takes a -:class:`~pystac.validation.schema_uri_map.SchemaUriMap`, which by default uses the -:class:`~pystac.validation.schema_uri_map.DefaultSchemaUriMap`. If desirable, users can -create their own implementation of -:class:`~pystac.validation.schema_uri_map.SchemaUriMap` and register -a new instance of :class:`~pystac.validation.JsonSchemaSTACValidator` using that schema -map with :func:`pystac.validation.set_validator`. - -Extensions -========== - -From the documentation on :stac-spec:`STAC Spec Extensions `: - - Extensions to the core STAC specification provide additional JSON fields that can be - used to better describe the data. Most tend to be about describing a particular - domain or type of data, but some imply functionality. - -This library makes an effort to support all extensions that are part of the -`stac-extensions GitHub org -`__, and -we are committed to supporting all STAC Extensions at the "Candidate" maturity level or -above (see the `Extension Maturity -`__ documentation for details). - -Accessing Extension Functionality ---------------------------------- - -Extension functionality is encapsulated in classes that are specific to the STAC -Extension (e.g. Electro-Optical, Projection, etc.) and STAC Object -(:class:`~pystac.Collection`, :class:`pystac.Item`, or :class:`pystac.Asset`). All -classes that extend these objects inherit from -:class:`pystac.extensions.base.PropertiesExtension`, and you can use the -``ext`` accessor on the object to access the extension fields. - -For instance, if you have an item that implements the :stac-ext:`Electro-Optical -Extension `, you can access the fields associated with that extension using -:meth:`Item.ext `: - -.. code-block:: python - - import pystac - - item = pystac.Item.from_file("tests/data-files/eo/eo-landsat-example.json") - - # As long as the Item implements the EO Extension you can access all the - # EO properties directly - bands = item.ext.eo.bands - cloud_cover = item.ext.eo.cloud_cover - ... - -.. note:: ``ext`` will raise an :exc:`~pystac.ExtensionNotImplemented` - exception if the object does not implement that extension (e.g. if the extension - URI is not in that object's :attr:`~pystac.STACObject.stac_extensions` list). See - the `Adding an Extension`_ section below for details on adding an extension to an - object. - -If you don't want to raise an error you can use -:meth:`Item.ext.has ` -to first check if the extension is implemented on your pystac object: - -.. code-block:: python - - if item.ext.has("eo"): - bands = item.ext.eo.bands - -See the documentation for each extension implementation for details on the supported -properties and other functionality. - -Extensions have access to the properties of the object. *This attribute is a reference -to the properties of the* :class:`~pystac.Collection`, :class:`~pystac.Item` *or* -:class:`~pystac.Asset` *being extended and can therefore mutate those properties.* -For instance: - -.. code-block:: python - - item = pystac.Item.from_file("tests/data-files/eo/eo-landsat-example.json") - print(item.properties["eo:cloud_cover"]) - # 78 - - print(item.ext.eo.cloud_cover) - # 78 - - item.ext.eo.cloud_cover = 45 - print(item.properties["eo:cloud_cover"]) - # 45 - -There is also a -:attr:`~pystac.extensions.base.PropertiesExtension.additional_read_properties` attribute -that, if present, gives read-only access to properties of any objects that own the -extended object. For instance, an extended :class:`pystac.Asset` instance would have -read access to the properties of the :class:`pystac.Item` that owns it (if there is -one). If a property exists in both additional_read_properties and properties, the value -in additional_read_properties will take precedence. - - -An ``apply`` method is available on extended objects. This allows you to pass in -property values pertaining to the extension. Properties that are required by the -extension will be required arguments to the ``apply`` method. Optional properties will -have a default value of ``None``: - -.. code-block:: python - - # Can also omit cloud_cover entirely... - item.ext.eo.apply(0.5, bands, cloud_cover=None) - - -Adding an Extension -------------------- - -You can add an extension to a STAC object that does not already implement that extension -using the :meth:`Item.ext.add ` method. -The :meth:`Item.ext.add ` method adds the correct -schema URI to the :attr:`~pystac.Item.stac_extensions` list for the STAC object. - -.. code-block:: python - - # Load a basic item without any extensions - item = pystac.Item.from_file("tests/data-files/item/sample-item.json") - print(item.stac_extensions) - # [] - - # Add the Electro-Optical extension - item.ext.add("eo") - print(item.stac_extensions) - # ['https://stac-extensions.github.io/eo/v1.1.0/schema.json'] - -Extended Summaries ------------------- - -Extension classes like :class:`~pystac.extensions.projection.ProjectionExtension` may -also provide a ``summaries`` static method that can be used to extend the Collection -summaries. This method returns a class inheriting from -:class:`pystac.extensions.base.SummariesExtension` that provides tools for summarizing -the properties defined by that extension. These classes also hold a reference to the -Collection's :class:`pystac.Summaries` instance in the ``summaries`` attribute. - - -.. code-block:: python - - import pystac - from pystac.extensions.projection import ProjectionExtension - - # Load a collection that does not implement the Projection extension - collection = pystac.Collection.from_file( - "tests/data-files/examples/1.0.0/collection.json" - ) - - # Add Projection extension summaries to the collection - proj = ProjectionExtension.summaries(collection, add_if_missing=True) - print(collection.stac_extensions) - # [ - # ...., - # 'https://stac-extensions.github.io/projection/v1.1.0/schema.json' - # ] - - # Set the values for various extension fields - proj.epsg = [4326] - collection_as_dict = collection.to_dict() - collection_as_dict["summaries"]["proj:epsg"] - # [4326] - - -Item Asset properties -===================== - -Properties that apply to Items can be found in two places: the Item's properties or in -any of an Item's Assets. If the property is on an Asset, it applies only to that specific -asset. For example, gsd defined for an Item represents the best Ground Sample Distance -(resolution) for the data within the Item. However, some assets may be lower resolution -and thus have a higher gsd. In that case, the `gsd` can be found on the Asset. - -See the STAC documentation on :stac-spec:`Additional Fields for Assets -` and the relevant :stac-spec:`Best -Practices ` for more -information. - -The implementation of this feature in PySTAC uses the method described here and is -consistent across Item and ItemExtensions. The bare property names represent values for -the Item only, but for each property where it is possible to set on both the Item or the -Asset there is a ``get_`` and ``set_`` methods that optionally take an Asset. For the -``get_`` methods, if the property is found on the Asset, the Asset's value is used; -otherwise the Item's value will be used. For the ``set_`` method, if an Asset is passed -in the value will be applied to the Asset and not the Item. - -For example, if we have an Item with a ``gsd`` of 10 with three bands, and only asset -"band3" having a ``gsd`` of 20, the ``get_gsd`` method will behave in the following way: - - .. code-block:: python - - assert item.common_metadata.gsd == 10 - assert item.common_metadata.get_gsd() == 10 - assert item.common_metadata.get_gsd(item.asset['band1']) == 10 - assert item.common_metadata.get_gsd(item.asset['band3']) == 20 - -Similarly, if we set the asset at 'band2' to have a ``gsd`` of 30, it will only affect -that asset: - - .. code-block:: python - - item.common_metadata.set_gsd(30, item.assets['band2'] - assert item.common_metadata.gsd == 10 - assert item.common_metadata.get_gsd(item.asset['band2']) == 30 - -Manipulating STACs -================== - -PySTAC is designed to allow for STACs to be manipulated in-memory. This includes -:ref:`copy stacs`, walking over all objects in a STAC and mutating their properties, or -using collection-style `map` methods for mapping over items. - - -Walking over a STAC -------------------- - -You can walk through all sub-catalogs and items of a catalog with a method inspired -by the Python Standard Library `os.walk() -`_ method: :func:`Catalog.walk() -`: - -.. code-block:: python - - for root, subcats, items in catalog.walk(): - # Root represents a catalog currently being walked in the tree - root.title = '{} has been walked!'.format(root.id) - - # subcats represents any catalogs or collections owned by root - for cat in subcats: - cat.title = 'About to be walked!' - - # items represent all items that are contained by root - for item in items: - item.title = '{} - owned by {}'.format(item.id, root.id) - -Mapping over Items ------------------- - -The :func:`Catalog.map_items ` method is useful for -into smaller chunks (e.g. tiling out large image items). -item, you can return multiple items, in case you are generating new objects, or splitting items -manipulating items in a STAC. This will create a full copy of the STAC, so will leave -the original catalog unmodified. In the method that manipulates and returns the modified - -.. code-block:: python - - def modify_item_title(item): - item.title = 'Some new title' - return item - - def duplicate_item(item): - duplicated_item = item.clone() - duplicated_item.id += "-duplicated" - return [item, duplicated_item] - - - c = catalog.map_items(modify_item_title) - c = c.map_items(duplicate_item) - new_catalog = c - -.. _copy stacs: - -Copying STACs in-memory ------------------------ - -The in-memory copying of STACs to create new ones is crucial to correct manipulations -and mutations of STAC data. The :func:`STACObject.full_copy -` mechanism handles this in a way that ties the elements of -the copies STAC together correctly. This includes situations where there might be cycles -in the graph of connected objects of the STAC (which otherwise would be `a tree -`_). - -Resolving STAC objects -====================== - -PySTAC tries to only "resolve" STAC Objects - that is, load the metadata contained by -STAC files pointed to by links into Python objects in-memory - when necessary. It also -ensures that two links that point to the same object resolve to the same in-memory -object. - -Lazy resolution of STAC objects -------------------------------- - -Links are read only when they need to be. For instance, when you load a catalog using -:func:`Catalog.from_file `, the catalog and all of its links -are read into a :class:`~pystac.Catalog` instance. If you iterate through -:attr:`Catalog.links `, you'll see the :attr:`~pystac.Link.target` -of the :class:`~pystac.Link` will refer to a string - that is the HREF of the link. -However, if you call :func:`Catalog.get_items `, for instance, -you'll get back the actual :class:`~pystac.Item` instances that are referred to by each -item link in the Catalog. That's because at the time you call ``get_items``, PySTAC is -"resolving" the links for any link that represents an item in the catalog. - -The resolution mechanism is accomplished through :func:`Link.resolve_stac_object -`. Though this method is used extensively internally to -PySTAC, ideally this is completely transparent to users of PySTAC, and you won't have to -worry about how and when links get resolved. However, one important aspect to understand -is how object resolution caching happens. - -Resolution Caching ------------------- - -The root :class:`~pystac.Catalog` instance of a STAC (the Catalog which is linked to by -every associated object's ``root`` link) contains a cache of resolved objects. This -cache points to in-memory instances of :class:`~pystac.STACObject` s that have already -been resolved through PySTAC crawling links associated with that root catalog. The cache -works off of the stac object's ID, which is why **it is necessary for every STAC object -in the catalog to have a unique identifier, which is unique across the entire STAC**. - -When a link is being resolved from a STACObject that has it's root set, that root is -passed into the :func:`Link.resolve_stac_object ` call. -That root's :class:`~pystac.cache.ResolvedObjectCache` will be used to -ensure that if the link is pointing to an object that has already been resolved, then -that link will point to the same, single instance in the cache. This ensures working -with STAC objects in memory doesn't create a situation where multiple copies of the same -STAC objects are created from different links, manipulated, and written over each other. - -Working with STAC JSON -====================== - -The ``pystac.serialization`` package has some functionality around working directly with -STAC JSON objects, without utilizing PySTAC object types. This is used internally by -PySTAC, but might also be useful to users working directly with JSON (e.g. on -validation). - - -Identifying STAC objects from JSON ----------------------------------- - -Users can identify STAC information, including the object type, version and extensions, -from JSON. The main method for this is -:func:`~pystac.serialization.identify_stac_object`, which returns an object that -contains the object type, the range of versions this object is valid for (according to -PySTAC's best guess), the common extensions implemented by this object, and any custom -extensions (represented by URIs to JSON Schemas). - -.. code-block:: python - - from pystac.serialization import identify_stac_object - - json_dict = ... - - info = identify_stac_object(json_dict) - - # The object type - info.object_type - - # The version range - info.version_range - - # The common extensions - info.common_extensions - - # The custom Extensions - info.custom_extensions - -Merging common properties -------------------------- - -For pre-1.0.0 STAC, The :func:`~pystac.serialization.merge_common_properties` will take -a JSON dict that represents an item, and if it is associated with a collection, merge in -the collection's properties. You can pass in a dict that contains previously read -collections that caches collections by the HREF of the collection link and/or the -collection ID, which can help avoid multiple reads of -collection links. - -Note that this feature was dropped in STAC 1.0.0-beta.1 - -Geo interface -============= - -:class:`~pystac.Item` implements ``__geo_interface__``, a de-facto standard for -describing geospatial objects in Python: -https://gist.github.com/sgillies/2217756. Many packages can automatically use -objects that implement this protocol, e.g. `shapely -`_: - -.. code-block:: python - - >>> from pystac import Item - >>> from shapely.geometry import mapping, shape - >>> item = Item.from_file("data-files/item/sample-item.json") - >>> print(shape(item)) - POLYGON ((-122.308150179 37.488035566, -122.597502109 37.538869539, - -122.576687533 37.613537207, -122.2880486 37.562818007, -122.308150179 - 37.488035566)) diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index d6a3a667d..000000000 --- a/docs/conf.py +++ /dev/null @@ -1,258 +0,0 @@ -# -# Configuration file for the Sphinx documentation builder. -# -# This file does only contain a selection of the most common options. For a -# full list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import os -import subprocess -import sys -from typing import Any - -sys.path.insert(0, os.path.abspath(".")) -sys.path.insert(0, os.path.abspath("../")) -from pystac.version import STACVersion, __version__ # noqa:E402 - -git_branch = ( - subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]) - .decode("utf-8") - .strip() -) - -# -- Project information ----------------------------------------------------- - -project = "pystac" -copyright = "2019, Azavea" -author = "stac-utils" - -# The short X.Y version -version = __version__ -# The full version, including alpha/beta/rc tags -release = __version__ - - -# -- General configuration --------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - "sphinx_design", - "sphinx.ext.autodoc", - "sphinx.ext.autosummary", - "sphinx.ext.viewcode", - "sphinx.ext.intersphinx", - "sphinx.ext.napoleon", - "sphinx.ext.githubpages", - "sphinx.ext.extlinks", - "nbsphinx", -] - -extlinks = { - "tutorial": ( - "https://github.com/stac-utils/pystac/" "tree/{}/docs/tutorials/%s".format( - git_branch - ), - "%s tutorial", - ), - "stac-spec": ( - "https://github.com/radiantearth/stac-spec/tree/" "v{}/%s".format( - STACVersion.DEFAULT_STAC_VERSION - ), - "%s path", - ), - "stac-api-spec": ( - "https://github.com/radiantearth/stac-api-spec/tree/" "v{}/%s".format( - STACVersion.DEFAULT_STAC_API_VERSION - ), - "%s path", - ), - "stac-ext": ("https://github.com/stac-extensions/%s", "%s extension"), -} - -# Add any paths that contain templates here, relative to this directory. -# templates_path = ["_templates"] - -# Static CSS files -# html_css_files = ["custom.css"] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = ".rst" - -# The master toctree document. -master_doc = "index" - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = "en" - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**.ipynb_checkpoints"] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = None - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = "pydata_sphinx_theme" - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -html_theme_options = { - "icon_links": [ - { - "name": "GitHub", - "url": "https://github.com/stac-utils/pystac", - "icon": "fab fa-github-square", - }, - { - "name": "Gitter", - "url": "https://gitter.im/SpatioTemporal-Asset-Catalog/" - "python?utm_source=share-link&utm_medium=link&utm_campaign=share-link", - "icon": "fab fa-gitter", - }, - ], - "external_links": [ - {"name": "STAC Spec", "url": "https://github.com/radiantearth/stac-spec"} - ], - "header_links_before_dropdown": 7, - "navigation_with_keys": False, - # "navbar_end": ["navbar-icon-links.html", "search-field.html"] -} - -html_logo = "_static/STAC-03.png" - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# The default sidebars (for documents that don't match any pattern) are -# defined by theme itself. Builtin themes are using these templates by -# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', -# 'searchbox.html']``. -# -html_sidebars: dict[str, list[str]] = {"index": []} - - -# -- Options for HTMLHelp output --------------------------------------------- - -# Output file base name for HTML help builder. -htmlhelp_basename = "pystacdoc" - - -# -- Options for LaTeX output ------------------------------------------------ - -latex_elements: dict[str, Any] = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, "pystac.tex", "pystac Documentation", "stac-utils", "manual"), -] - - -# -- Options for manual page output ------------------------------------------ - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [(master_doc, "pystac", "pystac Documentation", [author], 1)] - - -# -- Options for Texinfo output ---------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - master_doc, - "pystac", - "pystac Documentation", - author, - "pystac", - "Python library for SpatioTemporal Asset Catalogs (STAC).", - "Miscellaneous", - ), -] - - -# -- Options for Epub output ------------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = project - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -# -# epub_identifier = '' - -# A unique identification for the text. -# -# epub_uid = '' - -# A list of files that should not be packed into the epub file. -epub_exclude_files = ["search.html"] - - -# -- Extension configuration ------------------------------------------------- - -intersphinx_mapping = { - "python": ("https://docs.python.org/3", None), - "dateutil": ("https://dateutil.readthedocs.io/en/stable", None), - "urllib3": ("https://urllib3.readthedocs.io/en/stable", None), -} - -# -- Substutition variables - -rst_epilog = f".. |stac_version| replace:: {STACVersion.DEFAULT_STAC_VERSION}" - -nitpick_ignore = [ - ("py:class", "Datetime"), - ("py:class", "L"), - ("py:class", "pystac.summaries.T"), - ("py:class", "HREF"), # this one partially works - ("py:class", "jsonschema.validators.Draft7Validator"), -] diff --git a/docs/contributing.rst b/docs/contributing.rst deleted file mode 100644 index af2d32610..000000000 --- a/docs/contributing.rst +++ /dev/null @@ -1,166 +0,0 @@ -Contributing -============ - -A list of issues and ongoing work is available on the PySTAC `issues page -`_. If you want to contribute code, the best -way is to coordinate with the core developers via an issue or pull request conversation. - -Development installation -^^^^^^^^^^^^^^^^^^^^^^^^ -Fork PySTAC into your GitHub account. Then, clone the repo and install it locally with -`uv ` as follows: - -.. code-block:: bash - - git clone git@github.com:your_user_name/pystac.git - cd pystac - uv sync - source .venv/bin/activate - -Testing -^^^^^^^ - -PySTAC runs tests using `pytest `_. You can -find unit tests in the ``tests/`` directory. - -To run the tests: - -.. code-block:: bash - - $ pytest - -To run the tests and generate the coverage report: - -.. code-block:: bash - - $ pytest -v -s --cov pystac --cov-report term-missing - -To view the coverage report, you can run -`coverage report` (to view the report in the terminal) or `coverage html` (to generate -an HTML report that can be opened in a browser). - -The PySTAC tests use `vcrpy `_ to mock API calls -with "pre-recorded" API responses. This often comes up when testing validation. - -When adding new tests that require pulling remote files use the ``@pytest.mark.vcr`` -decorator. Record the new responses and commit them to the repository. - -.. code-block:: bash - - $ pytest -v -s --record-mode new_episodes - $ git add - $ git commit -a -m 'new test episodes' - -Code quality checks -^^^^^^^^^^^^^^^^^^^ - -tl;dr: Run ``pre-commit install --overwrite`` to perform checks when committing, and -``pytest`` to run all tests. - -PySTAC uses - -- `ruff `_ for Python code linting -- `codespell `_ to check code for common misspellings -- `doc8 `__ for style checking on RST files in the docs -- `mypy `_ for Python type annotation checks - -Run all of these with ``pre-commit run --all-files`` or a single one using -``pre-commit run --all-files ID``, where ``ID`` is one of the command names above. For -example, to lint all the Python code, run ``pre-commit run --all-files ruff``. - -You can also install a Git pre-commit hook which will run the relevant linters and -formatters on any staged code when committing. This will be much faster than running on -all files, which is usually [#]_ only required when changing the pre-commit version or -configuration. Once installed you can bypass this check by adding the ``--no-verify`` -flag to Git commit commands, as in ``git commit --no-verify``. - -.. [#] In rare cases changes to one file might invalidate an unchanged file, such as - when modifying the return type of a function used in another file. - -Documentation -^^^^^^^^^^^^^ - -All new features or changes should include API documentation, in the form of -docstrings. Additionally, if you are updating an extension version, check to -see if that extension is used in the ``examples/`` STAC objects at the top level -of the repository. If so, update the extension version, then re-run -``docs/quickstart.ipynb`` to include the new extension versions in the notebook -cell output. - -Benchmarks -^^^^^^^^^^ - -PySTAC uses `asv `_ for benchmarking. Benchmarks are -defined in the ``./benchmarks`` directory. Due to the inherent uncertainty in -the environment of Github workflow runners, benchmarks are not executed in CI. -If your changes may affect performance, use the provided script to run the -benchmark suite locally. You'll need to install the benchmark dependencies -first. This script will compare your current ``HEAD`` with the **main** branch -and report any improvements or regressions. - -.. code-block:: bash - - asv continuous --split -e --interleave-rounds --factor 1.25 main HEAD - -The benchmark suite takes a while to run, and will report any significant -changes to standard output. For example, here's a benchmark comparison between -v1.0.0 and v1.6.1 (from `@gadomski's `_ computer):: - - before after ratio - [eee06027] [579c071b] - - - 533±20μs 416±10μs 0.78 collection.CollectionBench.time_collection_from_file [gadomski/virtualenv-py3.10-orjson] - - 329±8μs 235±10μs 0.72 collection.CollectionBench.time_collection_from_dict [gadomski/virtualenv-py3.10-orjson] - - 332±10μs 231±4μs 0.70 collection.CollectionBench.time_collection_from_dict [gadomski/virtualenv-py3.10] - - 174±4μs 106±2μs 0.61 item.ItemBench.time_item_from_dict [gadomski/virtualenv-py3.10] - - 174±4μs 106±2μs 0.61 item.ItemBench.time_item_from_dict [gadomski/virtualenv-py3.10-orjson] - before after ratio - [eee06027] [579c071b] - - + 87.1±3μs 124±5μs 1.42 catalog.CatalogBench.time_catalog_from_dict [gadomski/virtualenv-py3.10] - + 87.1±4μs 122±5μs 1.40 catalog.CatalogBench.time_catalog_from_dict [gadomski/virtualenv-py3.10-orjson] - -When developing new benchmarks, you can run a shortened version of the benchmark suite: - -.. code-block:: bash - - asv dev - - -CHANGELOG -^^^^^^^^^ - -PySTAC maintains a `changelog `_ -to track changes between releases. All PRs should make a changelog entry unless -the change is trivial (e.g. fixing typos) or is entirely invisible to users who may -be upgrading versions (e.g. an improvement to the CI system). - -For changelog entries, please link to the PR of that change. This needs to happen in a -few steps: - -- Make a PR to PySTAC with your changes -- Record the link to the PR -- Push an additional commit to your branch with the changelog entry with the link to the - PR. - -For more information on changelogs and how to write a good entry, see `keep a changelog -`_. - - -Style -^^^^^ - -In an effort to maintain a consistent codebase, PySTAC conforms to the following rules: - -.. code-block:: python - - # DO - from datetime import datetime - - # DON't - import datetime - import datetime as dt - -The exception to this rule is when ``datetime`` is only imported for type checking and -using the class directly interferes with another variable name. In this case, in the -TYPE_CHECKING block you should do ``from datetime import datetime as Datetime``. diff --git a/docs/example-catalog/catalog.json b/docs/example-catalog/catalog.json deleted file mode 100644 index 285d412b6..000000000 --- a/docs/example-catalog/catalog.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "type": "Catalog", - "stac_version": "1.1.0", - "stac_extensions": [], - "id": "landsat-stac-collection-catalog", - "title": "STAC for Landsat data", - "description": "STAC for Landsat data", - "links": [ - { - "href": "./catalog.json", - "rel": "self" - }, - { - "href": "./catalog.json", - "rel": "root" - }, - { - "href": "./landsat-8-l1/collection.json", - "rel": "child" - } - ] -} \ No newline at end of file diff --git a/docs/example-catalog/landsat-8-l1/2018-05/LC80150322018141LGN00.json b/docs/example-catalog/landsat-8-l1/2018-05/LC80150322018141LGN00.json deleted file mode 100644 index bf6a8d4d5..000000000 --- a/docs/example-catalog/landsat-8-l1/2018-05/LC80150322018141LGN00.json +++ /dev/null @@ -1,276 +0,0 @@ -{ - "type": "Feature", - "id": "LC80150322018141LGN00", - "stac_version" : "1.0.0", - "stac_extensions" : [ - "https://stac-extensions.github.io/eo/v1.1.0/schema.json", - "https://stac-extensions.github.io/view/v1.0.0/schema.json", - "https://stac-extensions.github.io/projection/v1.1.0/schema.json" - ], - "bbox": [ - -77.88298, - 39.23073, - -75.07535, - 41.41022 - ], - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - -77.28911976020206, - 41.40912394323429 - ], - [ - -75.07576783500748, - 40.97162247589133 - ], - [ - -75.66872631473827, - 39.23210949585851 - ], - [ - -77.87946700654118, - 39.67679918442899 - ], - [ - -77.28911976020206, - 41.40912394323429 - ] - ] - ] - }, - "collection": "landsat-8-l1", - "properties": { - "collection": "landsat-8-l1", - "datetime": "2018-05-21T15:44:59Z", - "view:sun_azimuth": 134.8082647, - "view:sun_elevation": 64.00406717, - "eo:cloud_cover": 4, - "instruments": ["OLI_TIRS"], - "view:off_nadir": 0, - "platform": "landsat-8", - "gsd": 30, - "proj:epsg": 32618, - "proj:transform": [258885.0, 30.0, 0.0, 4584315.0, 0.0, -30.0], - "proj:geometry": { - "type": "Polygon", - "coordinates": [ - [ - [258885.0, 4346085.0], - [258885.0, 4584315.0], - [493515.0, 4584315.0], - [493515.0, 4346085.0], - [258885.0, 4346085.0] - ] - ] - }, - "proj:shape": [7821, 7941] - }, - "assets": { - "index": { - "type": "text/html", - "title": "HTML index page", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/index.html", - "roles": [] - }, - "thumbnail": { - "title": "Thumbnail image", - "type": "image/jpeg", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_thumb_large.jpg", - "roles" : [ - "thumbnail" - ] - }, - "B1": { - "type": "image/tiff", - "title": "Band 1 (coastal)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B1.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B1", - "full_width_half_max" : 0.02, - "center_wavelength" : 0.44, - "common_name" : "coastal" - } - ] - }, - "B2": { - "type": "image/tiff", - "title": "Band 2 (blue)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B2.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B2", - "full_width_half_max" : 0.06, - "center_wavelength" : 0.48, - "common_name" : "blue" - } - ] - }, - "B3": { - "type": "image/tiff", - "title": "Band 3 (green)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B3.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B3", - "full_width_half_max" : 0.06, - "center_wavelength" : 0.56, - "common_name" : "green" - } - ] - }, - "B4": { - "type": "image/tiff", - "title": "Band 4 (red)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B4.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B4", - "full_width_half_max" : 0.04, - "center_wavelength" : 0.65, - "common_name" : "red" - } - ] - }, - "B5": { - "type": "image/tiff", - "title": "Band 5 (nir)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B5.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B5", - "full_width_half_max" : 0.03, - "center_wavelength" : 0.86, - "common_name" : "nir" - } - ] - }, - "B6": { - "type": "image/tiff", - "title": "Band 6 (swir16)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B6.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B6", - "full_width_half_max" : 0.08, - "center_wavelength" : 1.6, - "common_name" : "swir16" - } - ] - }, - "B7": { - "type": "image/tiff", - "title": "Band 7 (swir22)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B7.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B7", - "full_width_half_max" : 0.22, - "center_wavelength" : 2.2, - "common_name" : "swir22" - } - ] - }, - "B8": { - "type": "image/tiff", - "title": "Band 8 (pan)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B8.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B8", - "full_width_half_max" : 0.18, - "center_wavelength" : 0.59, - "common_name" : "pan" - } - ] - }, - "B9": { - "type": "image/tiff", - "title": "Band 9 (cirrus)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B9.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B9", - "full_width_half_max" : 0.02, - "center_wavelength" : 1.37, - "common_name" : "cirrus" - } - ] - }, - "B10": { - "type": "image/tiff", - "title": "Band 10 (lwir)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B10.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B10", - "full_width_half_max" : 0.8, - "center_wavelength" : 10.9, - "common_name" : "lwir11" - } - ] - }, - "B11": { - "type": "image/tiff", - "title": "Band 11 (lwir)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B11.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B11", - "full_width_half_max" : 1, - "center_wavelength" : 12, - "common_name" : "lwir2" - } - ] - }, - "ANG": { - "title": "Angle coefficients file", - "type": "text/plain", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_ANG.txt", - "roles": [] - }, - "MTL": { - "title": "original metadata file", - "type": "text/plain", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_MTL.txt", - "roles": [] - }, - "BQA": { - "title": "Band quality data", - "type": "image/tiff", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_BQA.TIF", - "roles": [] - } - }, - "links": [ - { - "rel": "self", - "href": "./LC80150322018141LGN00.json" - }, - { - "rel": "parent", - "href": "../collection.json" - }, - { - "rel": "collection", - "href": "../collection.json" - }, - { - "rel": "root", - "href": "../../catalog.json" - } - ] -} diff --git a/docs/example-catalog/landsat-8-l1/2018-06/LC80140332018166LGN00.json b/docs/example-catalog/landsat-8-l1/2018-06/LC80140332018166LGN00.json deleted file mode 100644 index e4eecc3c5..000000000 --- a/docs/example-catalog/landsat-8-l1/2018-06/LC80140332018166LGN00.json +++ /dev/null @@ -1,276 +0,0 @@ -{ - "type": "Feature", - "id": "LC80140332018166LGN00", - "stac_version" : "1.0.0", - "stac_extensions" : [ - "https://stac-extensions.github.io/eo/v1.1.0/schema.json", - "https://stac-extensions.github.io/view/v1.0.0/schema.json", - "https://stac-extensions.github.io/projection/v1.1.0/schema.json" - ], - "bbox": [ - -76.66703, - 37.82561, - -73.94861, - 39.95958 - ], - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - -76.12180471942207, - 39.95810181489563 - ], - [ - -73.94910518227414, - 39.55117185146004 - ], - [ - -74.49564725552679, - 37.826064511480496 - ], - [ - -76.66550404911956, - 38.240699151776084 - ], - [ - -76.12180471942207, - 39.95810181489563 - ] - ] - ] - }, - "collection": "landsat-8-l1", - "properties": { - "collection": "landsat-8-l1", - "datetime": "2018-06-15T15:39:09Z", - "view:sun_azimuth": 125.59055137, - "view:sun_elevation": 66.54485226, - "eo:cloud_cover": 22, - "instruments": ["OLI_TIRS"], - "view:off_nadir": 0, - "platform": "landsat-8", - "gsd": 30, - "proj:epsg": 32618, - "proj:transform": [357585.0, 30.0, 0.0, 4423815.0, 0.0, -30.0], - "proj:geometry": { - "type": "Polygon", - "coordinates": [ - [ - [357585.0, 4187685.0], - [357585.0, 4423815.0], - [589815.0, 4423815.0], - [589815.0, 4187685.0], - [357585.0, 4187685.0] - ] - ] - }, - "proj:shape": [7741, 7871] - }, - "assets": { - "index": { - "type": "text/html", - "title": "HTML index page", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/index.html", - "roles": [] - }, - "thumbnail": { - "title": "Thumbnail image", - "type": "image/jpeg", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_thumb_large.jpg", - "roles" : [ - "thumbnail" - ] - }, - "B1": { - "type": "image/tiff", - "title": "Band 1 (coastal)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B1.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B1", - "full_width_half_max" : 0.02, - "center_wavelength" : 0.44, - "common_name" : "coastal" - } - ] - }, - "B2": { - "type": "image/tiff", - "title": "Band 2 (blue)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B2.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B2", - "full_width_half_max" : 0.06, - "center_wavelength" : 0.48, - "common_name" : "blue" - } - ] - }, - "B3": { - "type": "image/tiff", - "title": "Band 3 (green)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B3.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B3", - "full_width_half_max" : 0.06, - "center_wavelength" : 0.56, - "common_name" : "green" - } - ] - }, - "B4": { - "type": "image/tiff", - "title": "Band 4 (red)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B4.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B4", - "full_width_half_max" : 0.04, - "center_wavelength" : 0.65, - "common_name" : "red" - } - ] - }, - "B5": { - "type": "image/tiff", - "title": "Band 5 (nir)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B5.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B5", - "full_width_half_max" : 0.03, - "center_wavelength" : 0.86, - "common_name" : "nir" - } - ] - }, - "B6": { - "type": "image/tiff", - "title": "Band 6 (swir16)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B6.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B6", - "full_width_half_max" : 0.08, - "center_wavelength" : 1.6, - "common_name" : "swir16" - } - ] - }, - "B7": { - "type": "image/tiff", - "title": "Band 7 (swir22)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B7.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B7", - "full_width_half_max" : 0.22, - "center_wavelength" : 2.2, - "common_name" : "swir22" - } - ] - }, - "B8": { - "type": "image/tiff", - "title": "Band 8 (pan)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B8.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B8", - "full_width_half_max" : 0.18, - "center_wavelength" : 0.59, - "common_name" : "pan" - } - ] - }, - "B9": { - "type": "image/tiff", - "title": "Band 9 (cirrus)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B9.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B9", - "full_width_half_max" : 0.02, - "center_wavelength" : 1.37, - "common_name" : "cirrus" - } - ] - }, - "B10": { - "type": "image/tiff", - "title": "Band 10 (lwir)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B10.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B10", - "full_width_half_max" : 0.8, - "center_wavelength" : 10.9, - "common_name" : "lwir11" - } - ] - }, - "B11": { - "type": "image/tiff", - "title": "Band 11 (lwir)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B11.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B11", - "full_width_half_max" : 1, - "center_wavelength" : 12, - "common_name" : "lwir2" - } - ] - }, - "ANG": { - "title": "Angle coefficients file", - "type": "text/plain", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_ANG.txt", - "roles": [] - }, - "MTL": { - "title": "original metadata file", - "type": "text/plain", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_MTL.txt", - "roles": [] - }, - "BQA": { - "title": "Band quality data", - "type": "image/tiff", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_BQA.TIF", - "roles": [] - } - }, - "links": [ - { - "rel": "self", - "href": "./LC80140332018166LGN00.json" - }, - { - "rel": "parent", - "href": "../collection.json" - }, - { - "rel": "collection", - "href": "../collection.json" - }, - { - "rel": "root", - "href": "../../catalog.json" - } - ] -} diff --git a/docs/example-catalog/landsat-8-l1/2018-06/LC80300332018166LGN00.json b/docs/example-catalog/landsat-8-l1/2018-06/LC80300332018166LGN00.json deleted file mode 100644 index 4f62ac898..000000000 --- a/docs/example-catalog/landsat-8-l1/2018-06/LC80300332018166LGN00.json +++ /dev/null @@ -1,276 +0,0 @@ -{ - "type": "Feature", - "stac_version" : "1.0.0", - "stac_extensions" : [ - "https://stac-extensions.github.io/eo/v1.1.0/schema.json", - "https://stac-extensions.github.io/view/v1.0.0/schema.json", - "https://stac-extensions.github.io/projection/v1.1.0/schema.json" - ], - "id": "LC80300332018166LGN00", - "bbox": [ - -101.40793, - 37.81084, - -98.6721, - 39.97469 - ], - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - -100.84368079413701, - 39.97210491033466 - ], - [ - -98.67492641719046, - 39.54833037653145 - ], - [ - -99.23946071016417, - 37.81370881408165 - ], - [ - -101.40560438472555, - 38.24476872678675 - ], - [ - -100.84368079413701, - 39.97210491033466 - ] - ] - ] - }, - "collection": "landsat-8-l1", - "properties": { - "collection": "landsat-8-l1", - "datetime": "2018-06-15T17:18:03Z", - "view:sun_azimuth": 125.5799919, - "view:sun_elevation": 66.54407242, - "eo:cloud_cover": 0, - "instruments": ["OLI_TIRS"], - "view:off_nadir": 0, - "platform": "landsat-8", - "gsd": 30, - "proj:epsg": 32614, - "proj:transform": [294285.0, 30.0, 0.0, 4425015.0, 0.0, -30], - "proj:geometry": { - "type": "Polygon", - "coordinates": [ - [ - [294285.0, 4187385.0], - [294285.0, 4425015.0], - [528015.0, 4425015.0], - [528015.0, 4187385.0], - [294285.0, 4187385.0] - ] - ] - }, - "proj:shape": [7791, 7921] - }, - "assets": { - "index": { - "type": "text/html", - "title": "HTML index page", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/index.html", - "roles" : [] - }, - "thumbnail": { - "title": "Thumbnail image", - "type": "image/jpeg", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_thumb_large.jpg", - "roles" : [ - "thumbnail" - ] - }, - "B1": { - "type": "image/tiff", - "title": "Band 1 (coastal)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B1.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B1", - "full_width_half_max" : 0.02, - "center_wavelength" : 0.44, - "common_name" : "coastal" - } - ] - }, - "B2": { - "type": "image/tiff", - "title": "Band 2 (blue)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B2.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B2", - "full_width_half_max" : 0.06, - "center_wavelength" : 0.48, - "common_name" : "blue" - } - ] - }, - "B3": { - "type": "image/tiff", - "title": "Band 3 (green)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B3.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B3", - "full_width_half_max" : 0.06, - "center_wavelength" : 0.56, - "common_name" : "green" - } - ] - }, - "B4": { - "type": "image/tiff", - "title": "Band 4 (red)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B4.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B4", - "full_width_half_max" : 0.04, - "center_wavelength" : 0.65, - "common_name" : "red" - } - ] - }, - "B5": { - "type": "image/tiff", - "title": "Band 5 (nir)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B5.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B5", - "full_width_half_max" : 0.03, - "center_wavelength" : 0.86, - "common_name" : "nir" - } - ] - }, - "B6": { - "type": "image/tiff", - "title": "Band 6 (swir16)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B6.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B6", - "full_width_half_max" : 0.08, - "center_wavelength" : 1.6, - "common_name" : "swir16" - } - ] - }, - "B7": { - "type": "image/tiff", - "title": "Band 7 (swir22)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B7.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B7", - "full_width_half_max" : 0.22, - "center_wavelength" : 2.2, - "common_name" : "swir22" - } - ] - }, - "B8": { - "type": "image/tiff", - "title": "Band 8 (pan)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B8.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B8", - "full_width_half_max" : 0.18, - "center_wavelength" : 0.59, - "common_name" : "pan" - } - ] - }, - "B9": { - "type": "image/tiff", - "title": "Band 9 (cirrus)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B9.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B9", - "full_width_half_max" : 0.02, - "center_wavelength" : 1.37, - "common_name" : "cirrus" - } - ] - }, - "B10": { - "type": "image/tiff", - "title": "Band 10 (lwir)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B10.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B10", - "full_width_half_max" : 0.8, - "center_wavelength" : 10.9, - "common_name" : "lwir11" - } - ] - }, - "B11": { - "type": "image/tiff", - "title": "Band 11 (lwir)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B11.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B11", - "full_width_half_max" : 1, - "center_wavelength" : 12, - "common_name" : "lwir2" - } - ] - }, - "ANG": { - "title": "Angle coefficients file", - "type": "text/plain", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_ANG.txt", - "roles": [] - }, - "MTL": { - "title": "original metadata file", - "type": "text/plain", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_MTL.txt", - "roles": [] - }, - "BQA": { - "title": "Band quality data", - "type": "image/tiff", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_BQA.TIF", - "roles": [] - } - }, - "links": [ - { - "rel": "self", - "href": "./LC80300332018166LGN00.json" - }, - { - "rel": "parent", - "href": "../collection.json" - }, - { - "rel": "collection", - "href": "../collection.json" - }, - { - "rel": "root", - "href": "../../catalog.json" - } - ] -} diff --git a/docs/example-catalog/landsat-8-l1/2018-07/LC80150332018189LGN00.json b/docs/example-catalog/landsat-8-l1/2018-07/LC80150332018189LGN00.json deleted file mode 100644 index b74e84eef..000000000 --- a/docs/example-catalog/landsat-8-l1/2018-07/LC80150332018189LGN00.json +++ /dev/null @@ -1,275 +0,0 @@ -{ - "type": "Feature", - "id": "LC80150332018189LGN00", - "stac_version" : "1.0.0", - "stac_extensions" : [ - "https://stac-extensions.github.io/eo/v1.1.0/schema.json", - "https://stac-extensions.github.io/view/v1.0.0/schema.json", - "https://stac-extensions.github.io/projection/v1.1.0/schema.json" - ], - "bbox": [ - -78.25028, - 37.79719, - -75.48983, - 39.98757 - ], - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - -77.66532657556414, - 39.987421383364385 - ], - [ - -75.49021499188945, - 39.54442448711656 - ], - [ - -76.07747288135147, - 37.799167045362736 - ], - [ - -78.25025639728777, - 38.24897728816149 - ], - [ - -77.66532657556414, - 39.987421383364385 - ] - ] - ] - }, - "collection": "landsat-8-l1", - "properties": { - "collection": "landsat-8-l1", - "datetime": "2018-07-08T15:45:34Z", - "view:sun_azimuth": 125.31095515, - "view:sun_elevation": 65.2014335, - "eo:cloud_cover": 0, - "instruments": ["OLI_TIRS"], - "view:off_nadir": 0, - "platform": "landsat-8", - "gsd": 30, - "proj:epsg": 32618, - "proj:transform": [222285.0, 30.0, 0.0, 4426515.0, 0.0, -30.0], - "proj:geometry": { - "type": "Polygon", - "coordinates": [ - [ - [222285.0, 4187985.0], - [222285.0, 4426515.0], - [456915.0, 4426515.0], - [456915.0, 4187985.0], - [222285.0,4187985.0]] - ] - }, - "proj:shape": [7821, 7951] - }, - "assets": { - "index": { - "type": "text/html", - "title": "HTML index page", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/index.html", - "roles": [] - }, - "thumbnail": { - "title": "Thumbnail image", - "type": "image/jpeg", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_thumb_large.jpg", - "roles" : [ - "thumbnail" - ] - }, - "B1": { - "type": "image/tiff", - "title": "Band 1 (coastal)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B1.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B1", - "full_width_half_max" : 0.02, - "center_wavelength" : 0.44, - "common_name" : "coastal" - } - ] - }, - "B2": { - "type": "image/tiff", - "title": "Band 2 (blue)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B2.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B2", - "full_width_half_max" : 0.06, - "center_wavelength" : 0.48, - "common_name" : "blue" - } - ] - }, - "B3": { - "type": "image/tiff", - "title": "Band 3 (green)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B3.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B3", - "full_width_half_max" : 0.06, - "center_wavelength" : 0.56, - "common_name" : "green" - } - ] - }, - "B4": { - "type": "image/tiff", - "title": "Band 4 (red)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B4.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B4", - "full_width_half_max" : 0.04, - "center_wavelength" : 0.65, - "common_name" : "red" - } - ] - }, - "B5": { - "type": "image/tiff", - "title": "Band 5 (nir)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B5.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B5", - "full_width_half_max" : 0.03, - "center_wavelength" : 0.86, - "common_name" : "nir" - } - ] - }, - "B6": { - "type": "image/tiff", - "title": "Band 6 (swir16)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B6.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B6", - "full_width_half_max" : 0.08, - "center_wavelength" : 1.6, - "common_name" : "swir16" - } - ] - }, - "B7": { - "type": "image/tiff", - "title": "Band 7 (swir22)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B7.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B7", - "full_width_half_max" : 0.22, - "center_wavelength" : 2.2, - "common_name" : "swir22" - } - ] - }, - "B8": { - "type": "image/tiff", - "title": "Band 8 (pan)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B8.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B8", - "full_width_half_max" : 0.18, - "center_wavelength" : 0.59, - "common_name" : "pan" - } - ] - }, - "B9": { - "type": "image/tiff", - "title": "Band 9 (cirrus)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B9.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B9", - "full_width_half_max" : 0.02, - "center_wavelength" : 1.37, - "common_name" : "cirrus" - } - ] - }, - "B10": { - "type": "image/tiff", - "title": "Band 10 (lwir)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B10.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B10", - "full_width_half_max" : 0.8, - "center_wavelength" : 10.9, - "common_name" : "lwir11" - } - ] - }, - "B11": { - "type": "image/tiff", - "title": "Band 11 (lwir)", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B11.TIF", - "roles": [], - "eo:bands": [ - { - "name" : "B11", - "full_width_half_max" : 1, - "center_wavelength" : 12, - "common_name" : "lwir2" - } - ] - }, - "ANG": { - "title": "Angle coefficients file", - "type": "text/plain", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_ANG.txt", - "roles": [] - }, - "MTL": { - "title": "original metadata file", - "type": "text/plain", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_MTL.txt", - "roles": [] - }, - "BQA": { - "title": "Band quality data", - "type": "image/tiff", - "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_BQA.TIF", - "roles": [] - } - }, - "links": [ - { - "rel": "self", - "href": "./LC80150332018189LGN00.json" - }, - { - "rel": "parent", - "href": "../collection.json" - }, - { - "rel": "collection", - "href": "../collection.json" - }, - { - "rel": "root", - "href": "../../catalog.json" - } - ] -} diff --git a/docs/example-catalog/landsat-8-l1/collection.json b/docs/example-catalog/landsat-8-l1/collection.json deleted file mode 100644 index 173a33e4e..000000000 --- a/docs/example-catalog/landsat-8-l1/collection.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "type": "Collection", - "stac_version" : "1.0.0", - "stac_extensions" : [ - "eo", - "view", - "https://example.com/stac/landsat-extension/1.0/schema.json" - ], - "id" : "landsat-8-l1", - "title" : "Landsat 8 L1", - "description" : "Landsat 8 imagery radiometrically calibrated and orthorectified using ground points and Digital Elevation Model (DEM) data to correct relief displacement.", - "keywords" : [ - "landsat", - "earth observation", - "usgs" - ], - "license" : "proprietary", - "providers" : [ - { - "name" : "Development Seed", - "roles" : [ - "processor" - ], - "url" : "https://github.com/sat-utils/sat-api" - } - ], - "extent" : { - "spatial" : { - "bbox" : [ - [ - -180.0, - -90.0, - 180.0, - 90.0 - ] - ] - }, - "temporal" : { - "interval" : [ - [ - "2018-05-21T15:44:59Z", - "2018-07-08T15:45:34Z" - ] - ] - } - }, - "summaries": {}, - "properties" : { - "collection" : "landsat-8-l1", - "instruments" : ["OLI_TIRS"], - "view:sun_azimuth" : 149.01607154, - "eo:bands" : [ - { - "name" : "B1", - "full_width_half_max" : 0.02, - "center_wavelength" : 0.44, - "common_name" : "coastal" - }, - { - "name" : "B2", - "full_width_half_max" : 0.06, - "center_wavelength" : 0.48, - "common_name" : "blue" - }, - { - "name" : "B3", - "full_width_half_max" : 0.06, - "center_wavelength" : 0.56, - "common_name" : "green" - }, - { - "name" : "B4", - "full_width_half_max" : 0.04, - "center_wavelength" : 0.65, - "common_name" : "red" - }, - { - "name" : "B5", - "full_width_half_max" : 0.03, - "center_wavelength" : 0.86, - "common_name" : "nir" - }, - { - "name" : "B6", - "full_width_half_max" : 0.08, - "center_wavelength" : 1.6, - "common_name" : "swir16" - }, - { - "name" : "B7", - "full_width_half_max" : 0.22, - "center_wavelength" : 2.2, - "common_name" : "swir22" - }, - { - "name" : "B8", - "full_width_half_max" : 0.18, - "center_wavelength" : 0.59, - "common_name" : "pan" - }, - { - "name" : "B9", - "full_width_half_max" : 0.02, - "center_wavelength" : 1.37, - "common_name" : "cirrus" - }, - { - "name" : "B10", - "full_width_half_max" : 0.8, - "center_wavelength" : 10.9, - "common_name" : "lwir11" - }, - { - "name" : "B11", - "full_width_half_max" : 1, - "center_wavelength" : 12, - "common_name" : "lwir2" - } - ], - "view:off_nadir" : 0, - "view:azimuth" : 0, - "platform" : "landsat-8", - "gsd" : 15, - "view:sun_elevation" : 59.214247 - }, - "links" : [ - { - "href" : "../catalog.json", - "rel" : "root" - }, - { - "href" : "../catalog.json", - "rel" : "parent" - }, - { - "href" : "./collection.json", - "rel" : "self" - }, - { - "href" : "./2018-06/LC80140332018166LGN00.json", - "rel" : "item" - }, - { - "href" : "./2018-05/LC80150322018141LGN00.json", - "rel" : "item" - }, - { - "href" : "./2018-07/LC80150332018189LGN00.json", - "rel" : "item" - }, - { - "href" : "./2018-06/LC80300332018166LGN00.json", - "rel" : "item" - } - ] -} diff --git a/docs/img b/docs/img new file mode 120000 index 000000000..6ffc6ca9f --- /dev/null +++ b/docs/img @@ -0,0 +1 @@ +../img \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..5d10fb5c3 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,69 @@ +# PySTAC + +!!! warning + + These docs are for the work-in-progress v2 of PySTAC. + For the current PySTAC v1 docs, see . + + Our work plan for v2 goes like this: + + 1. Rebuild the core data structures (`Item`, `Catalog`, `Collection`, etc) from scratch, with new tests + 2. Slowly re-add the old tests to the `tests/v1` one at a time, to make sure that we're breaking as little as possible + 3. If we intentionally break a test (e.g. by relaxing a check on inputs) we'll mark it `xfail` and copy it to test the new expected behavior + + This will take a while. + Watch to track our progress. + We'll sometimes use pull requests, but sometimes not. + +**PySTAC** is a Python library for reading and writing [SpatioTemporal Asset Catalog (STAC)](https://stacspec.org) metadata. +To install: + + +```shell +python -m pip install pystac +``` + +## Creating + +STAC has three data structure: `Item`, `Catalog`, and `Collection`. +Each can be created with sensible defaults: + +```python +from pystac import Item, Catalog, Collection + +item = Item("an-item-id") +catalog = Item("a-catalog-id", "A catalog description") +collection = Item("a-collection-id", "A collection description") +``` + +## Reading + +Reading STAC from the local filesystem is supported out-of-the-box: + +```python +item = pystac.read_file("item.json") +``` + +To read from remote locations, including HTTP(S) and blob storage, we use [obstore](https://developmentseed.org/obstore/). +Install with that optional dependency: + +```shell +python -m pip install 'pystac[obstore]' +``` + +Then: + +```python +from pystac.obstore import ObstoreReader +reader = ObstoreReader() # provide any configuration values here, e.g. ObstoreReader(aws_region="us-east-1") +item = reader.read_file("s3://bucket/item.json") +``` + +!!! todo + + Add more examples, and maybe put them in a notebook so we can execute them. + +## Supported versions + +PySTAC v2.0 supports STAC v1.0 and STAC v1.1. +For pre-STAC v1.0 versions, use `pystac<2`. diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index e44fb74fa..000000000 --- a/docs/index.rst +++ /dev/null @@ -1,57 +0,0 @@ -PySTAC Documentation -#################### - -PySTAC is a library for working with `SpatioTemporal Asset Catalogs (STAC) -`_ in `Python 3 `_. Some nice features -of PySTAC are: - -* Reading and writing STAC version 1.0. Future versions will read older versions of - STAC, but always write the latest supported version. See :ref:`stac_version_support` - for details. -* In-memory manipulations of STAC catalogs. -* Extend the I/O of STAC metadata to provide support for other platforms (e.g. cloud - providers). -* Easy, efficient crawling of STAC catalogs. STAC objects are only read in when needed. -* Easily write "absolute published", "relative published" and "self-contained" catalogs - as :stac-spec:`described in the best practices documentation - `. - -.. grid:: 1 2 2 2 - :gutter: 2 - - .. grid-item-card:: Get Started - - * :doc:`installation`: Instructions for installing the basic package as well as - extras. - * :doc:`quickstart`: Jupyter notebook tutorial on using PySTAC for reading & - writing STAC catalogs. - - .. grid-item-card:: Go Deeper - - * :doc:`concepts`: Overview of how various concepts and structures from the STAC - Specification are implemented within PySTAC. - * :doc:`tutorials`: In-depth tutorials on using PySTAC for a number of different - applications. - * :doc:`api`: Detailed API documentation of PySTAC classes, methods, and functions. - -Related Projects -================ - -* `pystac-client `__: A Python client for - working with STAC Catalogs and APIs. -* `stactools `__: A command line tool and - library for working with STAC. -* `sat-stac `__: A Python 3 library for reading - and working with existing Spatio-Temporal Asset Catalogs (STAC). *Much of PySTAC - builds on the code and concepts of* ``sat-stac``. - -.. toctree:: - :maxdepth: 2 - :hidden: - - installation - quickstart - concepts - api - tutorials - contributing diff --git a/docs/installation.rst b/docs/installation.rst deleted file mode 100644 index 1f87bab4a..000000000 --- a/docs/installation.rst +++ /dev/null @@ -1,121 +0,0 @@ -Installation -############ - -Install from PyPi (recommended) -=============================== - -.. code-block:: bash - - pip install pystac - -Install from conda-forge -======================== - -.. code-block:: bash - - conda install -c conda-forge pystac - -Install from source -=================== - -.. code-block:: bash - - pip install git+https://github.com/stac-utils/pystac.git - -.. _installation_dependencies: - -Dependencies -============ - -PySTAC requires Python >= 3.10. This project follows the recommendations of -`NEP-29 `__ in deprecating support -for Python versions. This means that users can expect support for Python 3.10 to be -removed from the ``main`` branch after Apr 04, 2025 and therefore from the next release -after that date. - -As a foundational component of the Python STAC ecosystem used in a number of downstream -libraries, PySTAC aims to minimize its dependencies. As a result, the only dependency -for the basic PySTAC library is `python-dateutil -`__. - -PySTAC also has the following extras, which can be optionally installed to provide -additional functionality: - -* ``validation`` - - Installs the additional `jsonschema - `__ dependency. When this - dependency is installed, the :ref:`validation methods ` may be - used to validate STAC objects against the appropriate JSON schemas. - - To install: - - .. code-block:: bash - - pip install pystac[validation] - -* ``orjson`` - - Installs the additional `orjson `__ dependency. When - this dependency is installed, `orjson` will be used as the default JSON - serialization/deserialization for all operations in PySTAC. - - To install: - - .. code-block:: bash - - pip install pystac[orjson] - -* ``urllib3`` - - Installs the additional `urllib3 `__ dependency. - For now, this is only used in :py:class:`pystac.stac_io.RetryStacIO`, but it - may be used more extensively in the future. - - To install: - - .. code-block:: bash - - pip install pystac[urllib3] - -* ``jinja2`` - - Installs the additional `jinja2 `__ dependency. - When this dependency is installed, jupyter notebooks display pretty representations - of PySTAC objects - - To install: - - .. code-block:: bash - - pip install pystac[jinja2] - -Versions -======== - -To install a version of PySTAC that works with a specific versions of the STAC -specification, install the matching version of PySTAC from the following table. - -.. list-table:: - :widths: 50 50 - :header-rows: 1 - - * - PySTAC - - STAC - * - 1.x - - 1.0.x - * - 0.5.x - - 1.0.0-beta.* - * - 0.4.x - - 0.9.x - * - 0.3.x - - 0.8.x - -For instance, to work with STAC v0.9.x: - - .. code-block:: bash - - pip install pystac==0.4.0 - - -STAC spec versions below 0.8 are not supported by PySTAC. diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 451f805a5..000000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 000000000..2058d21a6 --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block announce %} +

+ 👷 These are work-in-progress docs for PySTAC v2.0, which has not been released. + For the current PySTAC v1 docs, see https://pystac.readthedocs.io. +

+{% endblock %} \ No newline at end of file diff --git a/docs/overrides/stylesheets/extra.css b/docs/overrides/stylesheets/extra.css new file mode 100644 index 000000000..eb430eb28 --- /dev/null +++ b/docs/overrides/stylesheets/extra.css @@ -0,0 +1,4 @@ +:root, +[data-md-color-scheme="default"] { + --md-primary-fg-color: #4cb1ac; +} \ No newline at end of file diff --git a/docs/pystac-v2.0.md b/docs/pystac-v2.0.md new file mode 100644 index 000000000..ed7f92514 --- /dev/null +++ b/docs/pystac-v2.0.md @@ -0,0 +1,22 @@ +# PySTAC v2.0 + +**PySTAC v2.0** is a ground-up re-write of **PySTAC**. +Our high-level design goals are a form of [Postel's law](https://en.wikipedia.org/wiki/Robustness_principle): + +- Help people make the best STAC possible +- Help people interact with existing (even poorly constructed or invalid) STAC as easily as possible + +To do so, we have some specific implementation strategies. + +- **Keep the core data structure APIs basically the same**: People are used to the basic methods on `Item`, `Catalog`, `Collection`, etc. + We shouldn't change the external APIs unless there's a good reason to. +- **Relax core data structure initializers to accept almost anything**, with warnings if something's being changed to make it valid +- **Stay low-dependency by default**: This keeps our maintenance burden lower, at the cost of having to hand-roll more stuff ourselves. +- **Replace "implementation" APIs**: `Item`, `Catalog`, etc are the "what" of **pystac**. + Things like `StacIO` are the "how". + We should create replacement structures for any of these "how" interfaces that we want to dramatically change, rather than try to "fix" the existing ones. + Backwards compatibility will be _much_ more difficult for these "how" interfaces, so we shouldn't even try. + _If possible_ we should re-write the existing "how" structure (e.g. `StacIO`) to use the new API, but this should be a lower-priority objective. +- **Do fewer things at once**: One of the biggest design problems of PySTAC v1.0 (in this author's opinion) was that many functions tried to be "helpful" by doing a lot of things at once. + When possible, we should simplify methods to do just one thing, and provide intuitive patterns for doing complex operations using multiple method calls. + Top-level functions can be used to "synthesize" complex operations, e.g. `pystac.read_file`. diff --git a/docs/quickstart.ipynb b/docs/quickstart.ipynb deleted file mode 100644 index 5645f4b83..000000000 --- a/docs/quickstart.ipynb +++ /dev/null @@ -1,2158 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Quickstart\n", - "\n", - "This notebook is a quick introduction to using PySTAC for reading an existing STAC catalog. For more in-depth examples check out the other tutorials.\n", - "\n", - "## Dependencies\n", - "\n", - "- PySTAC" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Reading a Catalog\n", - "\n", - "[A STAC Catalog](https://github.com/radiantearth/stac-spec/tree/master/catalog-spec) is used to group other STAC objects like Items, Collections, or even other Catalogs.\n", - "\n", - "We will be using a small example catalog adapted from the [example Landsat Collection](https://github.com/geotrellis/geotrellis-server/tree/977bad7a64c409341479c281c8c72222008861fd/stac-example/catalog/landsat-stac-collection) in the [GeoTrellis](https://geotrellis.io) repository. All STAC Items and Collections can be found in the [docs/example-catalog](https://github.com/stac-utils/pystac/tree/main/docs/example-catalog) directory of this repo; all Assets are hosted in the Landsat S3 bucket.\n", - "\n", - "First, we import the PySTAC classes we will be working with." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import shutil\n", - "import tempfile\n", - "from pathlib import Path\n", - "\n", - "from pystac import Catalog, get_stac_version" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Next, we read the example catalog and print some basic metadata." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ID: landsat-stac-collection-catalog\n", - "Title: STAC for Landsat data\n", - "Description: STAC for Landsat data\n" - ] - } - ], - "source": [ - "root_catalog = Catalog.from_file(\"./example-catalog/catalog.json\")\n", - "print(f\"ID: {root_catalog.id}\")\n", - "print(f\"Title: {root_catalog.title or 'N/A'}\")\n", - "print(f\"Description: {root_catalog.description or 'N/A'}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "*Note that we do not print the \"stac_version\" here. PySTAC automatically updates any Catalogs to the most recent supported STAC version and will automatically write this to the JSON object during serialization.*\n", - "\n", - "Let's confirm the latest STAC Spec version supported by PySTAC." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "1.0.0\n" - ] - } - ], - "source": [ - "print(get_stac_version())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Crawling Child Catalogs/Collections\n", - "\n", - "[STAC Collections](https://github.com/radiantearth/stac-spec/tree/master/collection-spec) are used to group related Items and provide aggregate or summary metadata for those Items.\n", - "\n", - "STAC Catalogs may have many nested layers of Catalogs or Collections within the top-level collection. Our example catalog has one Collection within the main Catalog at [landsat-8-l1/collection.json](./example-catalog/landsat-8-l1/collection.json). We can list the Collections in a given Catalog using the [Catalog.get_collections](https://pystac.readthedocs.io/en/latest/api.html#pystac.Catalog.get_collections) method. This method returns an iterable of PySTAC [Collection](https://pystac.readthedocs.io/en/latest/api.html#collection) instances, which we will turn into a `list`." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Number of collections: 1\n", - "Collections IDs:\n", - "- landsat-8-l1\n" - ] - } - ], - "source": [ - "collections = list(root_catalog.get_collections())\n", - "\n", - "print(f\"Number of collections: {len(collections)}\")\n", - "print(\"Collections IDs:\")\n", - "for collection in collections:\n", - " print(f\"- {collection.id}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's grab that Collection as a PySTAC [Collection](https://pystac.readthedocs.io/en/latest/api.html#collection) instance using the [Catalog.get_child method](https://pystac.readthedocs.io/en/latest/api.html#pystac.Catalog.get_child) so we can look at it in more detail. This method gets a child Catalog or Collection by ID, so we'll use the Collection ID that we printed above. Since this method returns `None` if no child exists with the given ID, we'll check to make sure we actually got the `Collection`." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "collection = root_catalog.get_child(\"landsat-8-l1\")\n", - "assert collection is not None" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Crawling Items\n", - "\n", - "[STAC Items](https://github.com/radiantearth/stac-spec/tree/master/item-spec) are the fundamental building blocks of a STAC Catalog. Each Item represents a single spatiotemporal resource (e.g. a satellite scene).\n", - "\n", - "Both Catalogs and Collections may have Items associated with them. Let's crawl our catalog, starting at the root, to see what Items we have. The [Catalog.get_items method](https://pystac.readthedocs.io/en/latest/api.html#pystac.Catalog.get_items) provides a convenient way of recursively listing all Items associated with a Catalog and all of its sub-Catalogs by including the `recursive=True` option." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Number of items: 4\n", - "- LC80140332018166LGN00\n", - "- LC80150322018141LGN00\n", - "- LC80150332018189LGN00\n", - "- LC80300332018166LGN00\n" - ] - } - ], - "source": [ - "items = list(root_catalog.get_items(recursive=True))\n", - "\n", - "print(f\"Number of items: {len(items)}\")\n", - "for item in items:\n", - " print(f\"- {item.id}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "These IDs are not very descriptive; in the next section, we will take a look at how we can access the rich metadata associated with each Item." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Item Metadata\n", - "\n", - "Items can have *a lot* of metadata. This can be a bit overwhelming at first, but break the metadata fields down into a few categories:\n", - "\n", - "- Core Item Metadata\n", - "- Common Metadata\n", - "- STAC Extensions\n", - "\n", - "We will walk through each of these metadata categories in the following sections. \n", - "\n", - "First, let's grab one of the Items using the [Catalog.get_items method](https://pystac.readthedocs.io/en/latest/api.html#pystac.Catalog.get_items). We will use `recursive=True` to recursively crawl all child Catalogs and/or Collections to find the Item." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "item = next(root_catalog.get_items(\"LC80140332018166LGN00\", recursive=True))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Core Item Metadata\n", - "\n", - "The core Item metadata fields include spatiotemporal information and the ID of the collection to which the Item belongs. These fields are all at the top level of the Item JSON and we can access them through attributes on the [PySTAC Item](https://pystac.readthedocs.io/en/latest/api.html#item) instance." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'type': 'Polygon',\n", - " 'coordinates': [[[-76.12180471942207, 39.95810181489563],\n", - " [-73.94910518227414, 39.55117185146004],\n", - " [-74.49564725552679, 37.826064511480496],\n", - " [-76.66550404911956, 38.240699151776084],\n", - " [-76.12180471942207, 39.95810181489563]]]}" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.geometry" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[-76.66703, 37.82561, -73.94861, 39.95958]" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.bbox" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "datetime.datetime(2018, 6, 15, 15, 39, 9, tzinfo=tzutc())" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.datetime" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'landsat-8-l1'" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.collection_id" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If we want the actual `Collection` instance instead of just the ID, we can use the [Item.get_collection](https://pystac.readthedocs.io/en/latest/api.html#pystac.Item.get_collection) method." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "
\n", - "
\n", - "
    \n", - " \n", - " \n", - " \n", - "
  • \n", - " type\n", - " \"Collection\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " id\n", - " \"landsat-8-l1\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " stac_version\n", - " \"1.0.0\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " description\n", - " \"Landsat 8 imagery radiometrically calibrated and orthorectified using ground points and Digital Elevation Model (DEM) data to correct relief displacement.\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " links\n", - " [] 7 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"root\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/catalog.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"application/json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"STAC for Landsat data\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 1\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"item\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-06/LC80140332018166LGN00.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 2\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"item\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-05/LC80150322018141LGN00.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 3\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"item\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-07/LC80150332018189LGN00.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 4\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"item\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-06/LC80300332018166LGN00.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 5\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"self\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/collection.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"application/json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 6\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"parent\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/catalog.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"application/json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"STAC for Landsat data\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " stac_extensions\n", - " [] 1 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - " \"https://example.com/stac/landsat-extension/1.0/schema.json\"\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " properties\n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " collection\n", - " \"landsat-8-l1\"\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " \n", - " instruments\n", - " [] 1 items\n", - " \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 0\n", - " \"OLI_TIRS\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " view:sun_azimuth\n", - " 149.01607154\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " \n", - " eo:bands\n", - " [] 11 items\n", - " \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 0\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B1\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.02\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.44\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"coastal\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 1\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B2\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.06\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.48\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"blue\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 2\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B3\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.06\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.56\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"green\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 3\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B4\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.04\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.65\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"red\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 4\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B5\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.03\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.86\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"nir\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 5\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B6\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.08\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 1.6\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"swir16\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 6\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B7\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.22\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 2.2\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"swir22\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 7\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B8\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.18\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.59\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"pan\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 8\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B9\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.02\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 1.37\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"cirrus\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 9\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B10\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.8\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 10.9\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"lwir11\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 10\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B11\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 1\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 12\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"lwir2\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " view:off_nadir\n", - " 0\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " view:azimuth\n", - " 0\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " platform\n", - " \"landsat-8\"\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " gsd\n", - " 15\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " view:sun_elevation\n", - " 59.214247\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " title\n", - " \"Landsat 8 L1\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " extent\n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " spatial\n", - "
        \n", - " \n", - " \n", - "
      • \n", - " \n", - " bbox\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - "
        • \n", - " \n", - " 0\n", - " [] 4 items\n", - " \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 0\n", - " -180.0\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 1\n", - " -90.0\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 2\n", - " 180.0\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 3\n", - " 90.0\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
        • \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " temporal\n", - "
        \n", - " \n", - " \n", - "
      • \n", - " \n", - " interval\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - "
        • \n", - " \n", - " 0\n", - " [] 2 items\n", - " \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 0\n", - " \"2018-05-21T15:44:59Z\"\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 1\n", - " \"2018-07-08T15:45:34Z\"\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
        • \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " license\n", - " \"proprietary\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " keywords\n", - " [] 3 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - " \"landsat\"\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 1\n", - " \"earth observation\"\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 2\n", - " \"usgs\"\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " providers\n", - " [] 1 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " name\n", - " \"Development Seed\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " 0\n", - " \"processor\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " url\n", - " \"https://github.com/sat-utils/sat-api\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - "
\n", - "
\n", - "
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.get_collection()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Common Metadata\n", - "\n", - "Certain fields that are commonly used in Items, but may also be found in other objects (e.g. Assets) are defined in the [Common Metadata](https://github.com/radiantearth/stac-spec/blob/master/item-spec/common-metadata.md) section of the spec. These include licensing and instrument information, descriptions of datetime ranges, and some other common fields. These properties can be found as attributes of the `Item.common_metadata` property, which is an instance of the [CommonMetadata class](https://pystac.readthedocs.io/en/latest/api.html#pystac.CommonMetadata)." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['OLI_TIRS']" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.common_metadata.instruments" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'landsat-8'" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.common_metadata.platform" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "30" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.common_metadata.gsd" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### STAC Extensions\n", - "\n", - "[STAC Extensions](https://stac-extensions.github.io/) are a mechanism for providing additional metadata not covered by the core STAC Spec. We can see which STAC Extensions are implemented by this particular Item by examining the list of extension URIs in the `stac_extensions` field." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['https://stac-extensions.github.io/eo/v1.1.0/schema.json',\n", - " 'https://stac-extensions.github.io/view/v1.0.0/schema.json',\n", - " 'https://stac-extensions.github.io/projection/v1.1.0/schema.json']" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.stac_extensions" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This Item implements the [Electro-Optical](https://github.com/stac-extensions/eo), [View Geometry](https://github.com/stac-extensions/view), and [Projection](https://github.com/stac-extensions/projection) Extensions. \n", - "\n", - "We can also check if a specific extension is implemented using [ext.has](https://pystac.readthedocs.io/en/latest/api.html#pystac.item.ext.has) with the name of that extension." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.ext.has(\"eo\")" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "False" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.ext.has(\"raster\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can access fields associated with the extension as attributes on the extension instance. For instance, the [\"eo:cloud_cover\" field](https://github.com/stac-extensions/eo#item-properties-or-asset-fields) defined in the Electro-Optical Extension can be accessed using the [item.ext.eo.cloud_cover](https://pystac.readthedocs.io/en/latest/api.html#pystac.extensions.eo.EOExtension.cloud_cover) attribute." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "22" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.ext.eo.cloud_cover" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can also access the cloud cover field directly in the Item properties." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "22" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.properties[\"eo:cloud_cover\"]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can access the Item's assets through the `assets` attribute, which is a dictionary:" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "index: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/index.html (text/html)\n", - "thumbnail: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_thumb_large.jpg (image/jpeg)\n", - "B1: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B1.TIF (image/tiff)\n", - "B2: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B2.TIF (image/tiff)\n", - "B3: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B3.TIF (image/tiff)\n", - "B4: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B4.TIF (image/tiff)\n", - "B5: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B5.TIF (image/tiff)\n", - "B6: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B6.TIF (image/tiff)\n", - "B7: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B7.TIF (image/tiff)\n", - "B8: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B8.TIF (image/tiff)\n", - "B9: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B9.TIF (image/tiff)\n", - "B10: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B10.TIF (image/tiff)\n", - "B11: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B11.TIF (image/tiff)\n", - "ANG: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_ANG.txt (text/plain)\n", - "MTL: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_MTL.txt (text/plain)\n", - "BQA: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_BQA.TIF (image/tiff)\n" - ] - } - ], - "source": [ - "for asset_key in item.assets:\n", - " asset = item.assets[asset_key]\n", - " print(\"{}: {} ({})\".format(asset_key, asset.href, asset.media_type))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can use the `to_dict()` method to convert an Asset, or any PySTAC object, into a dictionary:" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'href': 'https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B3.TIF',\n", - " 'type': 'image/tiff',\n", - " 'title': 'Band 3 (green)',\n", - " 'eo:bands': [{'name': 'B3',\n", - " 'full_width_half_max': 0.06,\n", - " 'center_wavelength': 0.56,\n", - " 'common_name': 'green'}],\n", - " 'roles': []}" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "asset = item.assets[\"B3\"]\n", - "asset.to_dict()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here we use the eo extension to get the band information for the asset:" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "bands = asset.ext.eo.bands\n", - "bands" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'name': 'B3',\n", - " 'full_width_half_max': 0.06,\n", - " 'center_wavelength': 0.56,\n", - " 'common_name': 'green'}" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "bands[0].to_dict()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Writing STAC Objects\n", - "\n", - "We can also use PySTAC to create and/or update STAC objects and write them to disk. This Quickstart Tutorial will introduce you to some very basic concepts in writing STAC objects; for a more thorough tutorial, please see the [\"How to create STAC Catalogs\"](./tutorials/how-to-create-stac-catalogs.ipynb) tutorial.\n", - "\n", - "Suppose there was a mistake in the cloud cover value that we looked at earlier and that we would like to add a value for the `instrument` field, which is currently null. We can update these values using the same attributes and properties as before, then save the entire catalog to our local drive." - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [], - "source": [ - "new_catalog = root_catalog.clone()" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [], - "source": [ - "item_to_update = next(root_catalog.get_items(\"LC80140332018166LGN00\", recursive=True))\n", - "\n", - "# Update the cloud cover\n", - "item_to_update.ext.eo.cloud_cover = 30\n", - "\n", - "# Add the instrument field\n", - "item_to_update.common_metadata.instruments = [\"LANDSAT\"]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we can examine the Item properties directly to verify that the changes have taken effect." - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "New Cloud Cover: 30\n", - "New Instruments: ['LANDSAT']\n" - ] - } - ], - "source": [ - "print(f\"New Cloud Cover: {item_to_update.properties['eo:cloud_cover']}\")\n", - "print(f\"New Instruments: {item_to_update.properties['instruments']}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We will write this updated catalog to a temporary directory in our local drive using the [Catalog.normalize_and_save](https://pystac.readthedocs.io/en/latest/api.html#pystac.Catalog.normalize_and_save) method." - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [], - "source": [ - "# Create a temporary directory\n", - "tmp_dir = tempfile.mkdtemp()" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Catalog saved to: /tmp/tmp9bmp70k9/catalog.json\n" - ] - } - ], - "source": [ - "# Save the catalog and normalize all paths\n", - "new_catalog.normalize_and_save(tmp_dir)\n", - "print(f\"Catalog saved to: {new_catalog.get_self_href()}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can open up Item that we just updated to verify that the new values were written to disk." - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": {}, - "outputs": [], - "source": [ - "item_path = Path(tmp_dir) / \"landsat-8-l1\" / \"LC80140332018166LGN00\" / \"\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, we clean up the temporary directory." - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "metadata": {}, - "outputs": [], - "source": [ - "shutil.rmtree(tmp_dir, ignore_errors=True)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.6" - }, - "vscode": { - "interpreter": { - "hash": "28618a729221ed2dc6301bcedf20e90b9d193b9b884dd15c675da71a09b73fa8" - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/tutorials.rst b/docs/tutorials.rst deleted file mode 100644 index 91b5159b9..000000000 --- a/docs/tutorials.rst +++ /dev/null @@ -1,71 +0,0 @@ -.. _tutorials: - -Tutorials -######### - -PySTAC Introduction -------------------- - -- :tutorial:`GitHub version ` -- :ref:`Docs version ` - -This tutorial gives an introduction to PySTAC concepts through code examples. - -How to read data from STAC --------------------------- - -- :tutorial:`GitHub version ` -- :ref:`Docs version ` - -This tutorial shows how to read data from PySTAC into xarray. - -PySTAC SpaceNet tutorial ------------------------- - -- :tutorial:`GitHub version ` -- :ref:`Docs version ` - -This tutorial shows how to create and manipulate a STAC of `SpaceNet `_ data. - -How to create STAC Catalogs with PySTAC ---------------------------------------- - -- :tutorial:`GitHub version ` -- :ref:`Docs version ` - -This was a tutorial that was part of a 30 minute presentation at the `community STAC -sprint -`_ -in Arlington, VA in November 2019. It runs through creating a STAC of images -from the `SpaceNet 5 `_ dataset. - -Creating a Landsat 8 STAC -------------------------- - -- :tutorial:`GitHub version ` -- :ref:`Docs version ` - -This tutorial was presented at [Cloud Native Geospatial Outreach -Day](https://sites.google.com/radiant.earth/cng-agenda/) on September 8th, 2020. It -shows how to create a STAC collection from a subset of Landsat 8 scenes over a location. - -Adding New and Custom Extensions --------------------------------- - -- :tutorial:`GitHub version ` -- :ref:`Docs version ` - -This tutorial goes over how to contribute new extensions to PySTAC as well as how to -implement your own custom extensions. - -.. toctree:: - :hidden: - :maxdepth: 2 - :glob: - - tutorials/pystac-introduction.ipynb - tutorials/how-to-read-data-from-stac.ipynb - tutorials/pystac-spacenet-tutorial.ipynb - tutorials/how-to-create-stac-catalogs.ipynb - tutorials/creating-a-landsat-stac.ipynb - tutorials/adding-new-and-custom-extensions.ipynb diff --git a/docs/tutorials/adding-new-and-custom-extensions.ipynb b/docs/tutorials/adding-new-and-custom-extensions.ipynb deleted file mode 100644 index 0e869ef3a..000000000 --- a/docs/tutorials/adding-new-and-custom-extensions.ipynb +++ /dev/null @@ -1,444 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adding New and Custom Extensions\n", - "\n", - "This tutorial will cover using the `PropertiesExtension` and `ExtensionManagementMixin` classes in `pystac.extensions.base` to implement a new extension in PySTAC, and how to make that class accessible via the `pystac.Item.ext` interface.\n", - "\n", - "For this exercise, we will implement an imaginary Order Request Extension that allows us to track an internal order ID associated with a given satellite image, as well as the history of that imagery order. This use-case is specific enough that it would probably not be a good candidate for an actual STAC Extension, but it gives us an opportunity to highlight some of the key aspects and patterns used in implementing STAC Extensions in PySTAC." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "First, we import the PySTAC modules and classes that we will be using throughout the tutorial." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from datetime import datetime, timedelta\n", - "from pprint import pprint\n", - "from typing import Any, Dict, List, Literal, Optional, Union\n", - "from uuid import uuid4\n", - "\n", - "import pystac\n", - "from pystac.extensions.base import ExtensionManagementMixin, PropertiesExtension\n", - "from pystac.utils import (\n", - " StringEnum,\n", - " datetime_to_str,\n", - " get_required,\n", - " map_opt,\n", - " str_to_datetime,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Define the Extension" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Our extension will extend STAC Items by adding the following properties:\n", - "\n", - "- `order:id`: A unique string ID associated with the internal order for this image. This field will be required.\n", - "- `order:history`: A chronological list of events associated with this order. Each of these \"events\" will have a timestamp and an event type, which will be one of the following: `submitted`, `started_processing`, `delivered`, `cancelled`. This field will be optional." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Create Extension Classes\n", - "\n", - "Let's start by creating a class to represent the order history events." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "class OrderEventType(StringEnum):\n", - " SUBMITTED = \"submitted\"\n", - " STARTED_PROCESSING = \"started_processing\"\n", - " DELIVERED = \"delivered\"\n", - " CANCELLED = \"cancelled\"\n", - "\n", - "\n", - "class OrderEvent:\n", - " properties: Dict[str, Any]\n", - "\n", - " def __init__(self, properties: Dict[str, Any]) -> None:\n", - " self.properties = properties\n", - "\n", - " @property\n", - " def event_type(self) -> OrderEventType:\n", - " return get_required(self.properties.get(\"type\"), self, \"event_type\")\n", - "\n", - " @event_type.setter\n", - " def event_type(self, v: OrderEventType) -> None:\n", - " self.properties[\"type\"] = str(v)\n", - "\n", - " @property\n", - " def timestamp(self) -> datetime:\n", - " return str_to_datetime(\n", - " get_required(self.properties.get(\"timestamp\"), self, \"timestamp\")\n", - " )\n", - "\n", - " @timestamp.setter\n", - " def timestamp(self, v: datetime) -> None:\n", - " self.properties[\"timestamp\"] = datetime_to_str(v)\n", - "\n", - " def __repr__(self) -> str:\n", - " return \"\"\n", - "\n", - " def apply(\n", - " self,\n", - " event_type: OrderEventType,\n", - " timestamp: datetime,\n", - " ) -> None:\n", - " self.event_type = event_type\n", - " self.timestamp = timestamp\n", - "\n", - " @classmethod\n", - " def create(\n", - " cls,\n", - " event_type: OrderEventType,\n", - " timestamp: datetime,\n", - " ) -> \"OrderEvent\":\n", - " oe = cls({})\n", - " oe.apply(event_type=event_type, timestamp=timestamp)\n", - " return oe\n", - "\n", - " def to_dict(self) -> Dict[str, Any]:\n", - " return self.properties" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "A few important notes about how we constructed this:\n", - "\n", - "- We used PySTAC's [StringEnum class](https://pystac.readthedocs.io/en/latest/api/utils.html#pystac.utils.StringEnum), which inherits from the Python [Enum](https://docs.python.org/3/library/enum.html) class, to capture the allowed event type values. This class has built-in methods that will convert these instances to strings when serializing STAC Items to JSON.\n", - "- We use property getters and setters to manipulate a `properties` dictionary in our `OrderEvent` class. We will see later how this pattern allows us to mutate Item property dictionaries in-place so that updates to the `OrderEvent` object are synced to the Item they extend.\n", - "- The `timestamp` property is converted to a string before it is saved in the `properties` dictionary. This ensures that dictionary is always JSON-serializable but allows us to work with the values as a Python `datetime` instance when using the property getter.\n", - "- We use `event_type` as our property name so that we do not shadow the built-in `type` function in the `apply` method. However, this values is stored under the desired `\"type\"` key in the underlying `properties` dictionary." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Next, we will create a new class inheriting from `PropertiesExtension` and `ExtensionManagementMixin`. Since this class only extends `pystac.Item` instance, we do not need to make it [generic](https://docs.python.org/3/library/typing.html#typing.Generic). If you were creating an extension that applied to multiple object types (e.g. `pystac.Item` and `pystac.Asset`) then you would need to inherit from `typing.Generic` as well and create concrete extension classed for each of these object types (see the [EOExtension](https://github.com/stac-utils/pystac/blob/3c5176f178a4345cb50d5dab83f1dab504ed2682/pystac/extensions/eo.py#L279), [ItemEOExtension](https://github.com/stac-utils/pystac/blob/3c5176f178a4345cb50d5dab83f1dab504ed2682/pystac/extensions/eo.py#L385), and [AssetEOExtension](https://github.com/stac-utils/pystac/blob/3c5176f178a4345cb50d5dab83f1dab504ed2682/pystac/extensions/eo.py#L429) classes for an example of this implementation)." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "SCHEMA_URI: str = \"https://example.com/image-order/v1.0.0/schema.json\"\n", - "PREFIX: str = \"order:\"\n", - "ID_PROP: str = PREFIX + \"id\"\n", - "HISTORY_PROP: str = PREFIX + \"history\"\n", - "\n", - "\n", - "class OrderExtension(\n", - " PropertiesExtension, ExtensionManagementMixin[Union[pystac.Item, pystac.Collection]]\n", - "):\n", - " name: Literal[\"order\"] = \"order\"\n", - "\n", - " def __init__(self, item: pystac.Item):\n", - " self.item = item\n", - " self.properties = item.properties\n", - "\n", - " def apply(\n", - " self, order_id: str = None, history: Optional[List[OrderEvent]] = None\n", - " ) -> None:\n", - " self.order_id = order_id\n", - " self.history = history\n", - "\n", - " @property\n", - " def order_id(self) -> str:\n", - " return get_required(self._get_property(ID_PROP, str), self, ID_PROP)\n", - "\n", - " @order_id.setter\n", - " def order_id(self, v: str) -> None:\n", - " self._set_property(ID_PROP, v, pop_if_none=False)\n", - "\n", - " @property\n", - " def history(self) -> Optional[List[OrderEvent]]:\n", - " return map_opt(\n", - " lambda history: [OrderEvent(d) for d in history],\n", - " self._get_property(HISTORY_PROP, List[OrderEvent]),\n", - " )\n", - "\n", - " @history.setter\n", - " def history(self, v: Optional[List[OrderEvent]]) -> None:\n", - " self._set_property(\n", - " HISTORY_PROP,\n", - " map_opt(lambda history: [event.to_dict() for event in history], v),\n", - " pop_if_none=True,\n", - " )\n", - "\n", - " @classmethod\n", - " def get_schema_uri(cls) -> str:\n", - " return SCHEMA_URI\n", - "\n", - " @classmethod\n", - " def ext(cls, obj: pystac.Item, add_if_missing: bool = False) -> \"OrderExtension\":\n", - " if isinstance(obj, pystac.Item):\n", - " cls.ensure_has_extension(obj, add_if_missing)\n", - " return OrderExtension(obj)\n", - " else:\n", - " raise pystac.ExtensionTypeError(\n", - " f\"OrderExtension does not apply to type '{type(obj).__name__}'\"\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As with the `OrderEvent` class, we use property getters and setters for our extension fields (the `PropertiesExtension` class has a `properties` attribute where these are stored). Rather than setting these values directly in the dictionary, we use the `_get_property` and `_set_property` methods that are built into the `PropertiesExtension` class). We also add an `ext` method that will be used to extend `pystac.Item` instances, and a `get_schema_uri` method that is required for all `PropertiesExtension` classes." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Use the Extension\n", - "\n", - "Let's try using our new classes to extend an `Item` and access the extension properties. We'll start by loading the core Item example from the STAC spec examples [here](https://github.com/radiantearth/stac-spec/blob/master/examples/core-item.json) and printing the existing properties." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'title': 'Core Item',\n", - " 'description': 'A sample STAC Item that includes examples of all common metadata',\n", - " 'datetime': None,\n", - " 'start_datetime': '2020-12-11T22:38:32.125Z',\n", - " 'end_datetime': '2020-12-11T22:38:32.327Z',\n", - " 'created': '2020-12-12T01:48:13.725Z',\n", - " 'updated': '2020-12-12T01:48:13.725Z',\n", - " 'platform': 'cool_sat1',\n", - " 'instruments': ['cool_sensor_v1'],\n", - " 'constellation': 'ion',\n", - " 'mission': 'collection 5624',\n", - " 'gsd': 0.512}" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item = pystac.read_file(\n", - " \"https://raw.githubusercontent.com/radiantearth/stac-spec/master/examples/core-item.json\"\n", - ")\n", - "item.properties" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Next, let's verify that this Item does not implement our new Order Extension yet and that it does not already contain any of our Order Extension properties." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Implements Extension: False\n", - "Order ID: None\n", - "History:\n" - ] - } - ], - "source": [ - "print(f\"Implements Extension: {OrderExtension.has_extension(item)}\")\n", - "print(f\"Order ID: {item.properties.get(ID_PROP)}\")\n", - "print(\"History:\")\n", - "for event in item.properties.get(HISTORY_PROP, []):\n", - " pprint(event)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As expected, this Item does not implement the extension (i.e. the schema URI is not in the Item's `stac_extensions` list). Let's add it, create an instance of `OrderExtension` that extends the `Item`, and add some values for our extension fields." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "order_ext = OrderExtension.ext(item, add_if_missing=True)\n", - "\n", - "# Create a unique string ID for the order ID\n", - "order_ext.order_id = str(uuid4())\n", - "\n", - "# Create some fake order history and set it using the extension\n", - "event_1 = OrderEvent.create(\n", - " event_type=OrderEventType.SUBMITTED, timestamp=datetime.now() - timedelta(days=1)\n", - ")\n", - "event_2 = OrderEvent.create(\n", - " event_type=OrderEventType.STARTED_PROCESSING,\n", - " timestamp=datetime.now() - timedelta(hours=12),\n", - ")\n", - "event_3 = OrderEvent.create(\n", - " event_type=OrderEventType.DELIVERED, timestamp=datetime.now() - timedelta(hours=1)\n", - ")\n", - "order_ext.history = [event_1, event_2, event_3]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now let's check to see if these values were written to our Item properties." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Implements Extension: True\n", - "Order ID: 7a206229-78f0-46cb-afc2-acf45e14afab\n", - "History:\n", - "{'timestamp': '2023-10-11T11:21:50.989315Z', 'type': 'submitted'}\n", - "{'timestamp': '2023-10-11T23:21:50.989372Z', 'type': 'started_processing'}\n", - "{'timestamp': '2023-10-12T10:21:50.989403Z', 'type': 'delivered'}\n" - ] - } - ], - "source": [ - "print(f\"Implements Extension: {OrderExtension.has_extension(item)}\")\n", - "print(f\"Order ID: {item.properties.get(ID_PROP)}\")\n", - "print(\"History:\")\n", - "for event in item.properties.get(HISTORY_PROP, []):\n", - " pprint(event)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## (Optional) Add access via `Item.ext`\n", - "\n", - "_This applies if you are planning on opening a Pull Request to add this implementation of the extension class to the pystac library_\n", - "\n", - "Now that you have a complete extension class, you can add access to it via the `pystac.Item.ext` interface by following these steps:\n", - "\n", - "1) Make sure that your Extension class has a `name` attribute with `Literal()` as the type.\n", - "2) Import your Extension class in `pystac/extensions/ext.py`\n", - "3) Add the `name` to `EXTENSION_NAMES`\n", - "4) Add the mapping from name to class to `EXTENSION_NAME_MAPPING`\n", - "5) Add a getter method to the Ext class for any object type that this extension works with.\n", - "\n", - "Here is an example of the diff:\n", - "\n", - "```diff\n", - "diff --git a/pystac/extensions/ext.py b/pystac/extensions/ext.py\n", - "index 93a30fe..2dbe5ca 100644\n", - "--- a/pystac/extensions/ext.py\n", - "+++ b/pystac/extensions/ext.py\n", - "@@ -9,6 +9,7 @@ from pystac.extensions.file import FileExtension\n", - " from pystac.extensions.grid import GridExtension\n", - " from pystac.extensions.item_assets import AssetDefinition, ItemAssetsExtension\n", - " from pystac.extensions.mgrs import MgrsExtension\n", - "+from pystac.extensions.order import OrderExtension\n", - " from pystac.extensions.pointcloud import PointcloudExtension\n", - " from pystac.extensions.projection import ProjectionExtension\n", - " from pystac.extensions.raster import RasterExtension\n", - "@@ -32,6 +33,7 @@ EXTENSION_NAMES = Literal[\n", - " \"grid\",\n", - " \"item_assets\",\n", - " \"mgrs\",\n", - "+ \"order\",\n", - " \"pc\",\n", - " \"proj\",\n", - " \"raster\",\n", - "@@ -54,6 +56,7 @@ EXTENSION_NAME_MAPPING: Dict[EXTENSION_NAMES, Any] = {\n", - " GridExtension.name: GridExtension,\n", - " ItemAssetsExtension.name: ItemAssetsExtension,\n", - " MgrsExtension.name: MgrsExtension,\n", - "+ OrderExtension.name: OrderExtension,\n", - " PointcloudExtension.name: PointcloudExtension,\n", - " ProjectionExtension.name: ProjectionExtension,\n", - " RasterExtension.name: RasterExtension,\n", - "@@ -150,6 +153,10 @@ class ItemExt:\n", - " def mgrs(self) -> MgrsExtension:\n", - " return MgrsExtension.ext(self.stac_object)\n", - " \n", - "+ @property\n", - "+ def order(self) -> OrderExtension:\n", - "+ return OrderExtension.ext(self.stac_object)\n", - "+\n", - " @property\n", - " def pc(self) -> PointcloudExtension[pystac.Item]:\n", - " return PointcloudExtension.ext(self.stac_object)\n", - "```\n", - "\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.6" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/tutorials/creating-a-landsat-stac.ipynb b/docs/tutorials/creating-a-landsat-stac.ipynb deleted file mode 100644 index 3845ac8e0..000000000 --- a/docs/tutorials/creating-a-landsat-stac.ipynb +++ /dev/null @@ -1,2543 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Creating a STAC of Landsat data\n", - "\n", - "In this tutorial we create a STAC of Landsat data provided by Microsoft's [Planetary Computer](https://planetarycomputer.microsoft.com/dataset/landsat-c2-l2). There's a lot of Landsat scenes, so we'll only take a subset of scenes that are from a specific year and over a specific location. We'll translate existing metadata about each scene to STAC information, utilizing the `eo`, `view`, `proj`, `raster` and `classification` extensions. Finally we'll write out the STAC catalog to our local machine, allowing us to use [stac-browser](https://github.com/radiantearth/stac-browser) to preview the images.\n", - "\n", - "### Requirements\n", - "\n", - "To run this tutorial you'll need to have installed PySTAC with the validation extra and the Planetary Computer package. To do this, use:\n", - "\n", - "```\n", - "pip install 'pystac[validation]' planetary-computer\n", - "```\n", - "\n", - "Also to run this notebook you'll need [jupyter](https://jupyter.org/) installed locally as well. If you're running in a docker container, make sure that port `5555` is exposed if you want to run the server at the end of the notebook." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import json\n", - "from datetime import datetime\n", - "from functools import partial\n", - "from os.path import dirname, join\n", - "from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast\n", - "from urllib.parse import urlparse, urlunparse\n", - "\n", - "import planetary_computer as pc\n", - "from dateutil.parser import parse\n", - "from typing_extensions import TypedDict\n", - "\n", - "import pystac\n", - "from pystac.extensions.classification import (\n", - " Bitfield,\n", - " Classification,\n", - " ClassificationExtension,\n", - ")\n", - "from pystac.extensions.eo import Band as EOBand\n", - "from pystac.extensions.eo import EOExtension\n", - "from pystac.extensions.projection import ProjectionExtension\n", - "from pystac.extensions.raster import RasterBand, RasterExtension\n", - "from pystac.extensions.view import ViewExtension" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Identify target scenes\n", - "\n", - "The Planetary Computer provides a STAC API that we could use to search for data within an area and time of interest, but since this notebook is intended to be a tutorial on creating STAC in the first place, doing so would put the cart ahead of the horse. Instead, we supply a list of metadata files for Landsat-8 and Landsat-9 scenes covering the center of Philadelphia, Pennsylvania in autumn of 2022 that we will work with:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "location_name = \"Philly\"\n", - "scene_mtls = [\n", - " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_MTL.xml\",\n", - " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC09_L2SP_014032_20221211_20221213_02_T2/LC09_L2SP_014032_20221211_20221213_02_T2_MTL.xml\",\n", - " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221203_20221212_02_T2/LC08_L2SP_014032_20221203_20221212_02_T2_MTL.xml\",\n", - " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC09_L2SP_014032_20221125_20230320_02_T2/LC09_L2SP_014032_20221125_20230320_02_T2_MTL.xml\",\n", - " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221117_20221128_02_T1/LC08_L2SP_014032_20221117_20221128_02_T1_MTL.xml\",\n", - " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC09_L2SP_014032_20221109_20221111_02_T1/LC09_L2SP_014032_20221109_20221111_02_T1_MTL.xml\",\n", - " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221101_20221114_02_T1/LC08_L2SP_014032_20221101_20221114_02_T1_MTL.xml\",\n", - " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC09_L2SP_014032_20221024_20221026_02_T2/LC09_L2SP_014032_20221024_20221026_02_T2_MTL.xml\",\n", - " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC09_L2SP_014032_20221008_20221010_02_T1/LC09_L2SP_014032_20221008_20221010_02_T1_MTL.xml\",\n", - "]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Read metadata from the MTL file\n", - "\n", - "Landsat metadata is contained in an `MTL` file that comes in either `.txt` or `.xml` formats. We'll rely on the XML version since it is more consistently available. This will require that we provide some facility for parsing the XML into a more usable format:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "# Taken from https://stackoverflow.com/questions/2148119/how-to-convert-an-xml-string-to-a-dictionary\n", - "from xml.etree import cElementTree as ElementTree\n", - "\n", - "\n", - "class XmlListConfig(list):\n", - " def __init__(self, aList):\n", - " for element in aList:\n", - " if element:\n", - " if len(element) == 1 or element[0].tag != element[1].tag:\n", - " self.append(XmlDictConfig(element))\n", - " elif element[0].tag == element[1].tag:\n", - " self.append(XmlListConfig(element))\n", - " elif element.text:\n", - " text = element.text.strip()\n", - " if text:\n", - " self.append(text)\n", - "\n", - "\n", - "class XmlDictConfig(dict):\n", - " def __init__(self, parent_element):\n", - " if parent_element.items():\n", - " self.update(dict(parent_element.items()))\n", - " for element in parent_element:\n", - " if element:\n", - " if len(element) == 1 or element[0].tag != element[1].tag:\n", - " aDict = XmlDictConfig(element)\n", - " else:\n", - " aDict = {element[0].tag: XmlListConfig(element)}\n", - " if element.items():\n", - " aDict.update(dict(element.items()))\n", - " self.update({element.tag: aDict})\n", - " elif element.items():\n", - " self.update({element.tag: dict(element.items())})\n", - " else:\n", - " self.update({element.tag: element.text})" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can then use these classes to get the MTL file for our scene. Notice we use `pystac.STAC_IO.read_text`; this is the method that PySTAC uses to read text as it crawls a STAC. It can read from the local filesystem or HTTP/HTTPS by default. Also, it can be extended to read from other sources such as cloud providers—[see the documentation here](https://pystac.readthedocs.io/en/latest/concepts.html#using-stac-io). For now we'll use it directly as an easy way to read a text file from an HTTPS source." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "stac_io = pystac.StacIO.default()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Since we're reading our files from the Planetary Computer's blob storage, we're also going to have to take the additional step of signing our requests using the `planetary-computer` package's `sign()` function. The raw URL is passed in, and the result has a shared access token applied. See the [planetary-computer Python package](https://github.com/microsoft/planetary-computer-sdk-for-python) for more details. We'll see the use of `pc.sign()` throughout the code below, and it will be necessary for asset HREFs to be passed through this function by the user of the catalog as well." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "def get_metadata(xml_url: str) -> Dict[str, Any]:\n", - " result = XmlDictConfig(ElementTree.XML(stac_io.read_text(pc.sign(xml_url))))\n", - " result[\"ORIGINAL_URL\"] = (\n", - " xml_url # Include the original URL in the metadata for use later\n", - " )\n", - " return result" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's read the MTL file for the first scene and see what it looks like." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"PRODUCT_CONTENTS\": {\n", - " \"ORIGIN\": \"Image courtesy of the U.S. Geological Survey\",\n", - " \"DIGITAL_OBJECT_IDENTIFIER\": \"https://doi.org/10.5066/P9OGBGM6\",\n", - " \"LANDSAT_PRODUCT_ID\": \"LC08_L2SP_014032_20221219_20230113_02_T1\",\n", - " \"PROCESSING_LEVEL\": \"L2SP\",\n", - " \"COLLECTION_NUMBER\": \"02\",\n", - " \"COLLECTION_CATEGORY\": \"T1\",\n", - " \"OUTPUT_FORMAT\": \"GEOTIFF\",\n", - " \"FILE_NAME_BAND_1\": \"LC08_L2SP_014032_20221219_20230113_02_T1_SR_B1.TIF\",\n", - " \"FILE_NAME_BAND_2\": \"LC08_L2SP_014032_20221219_20230113_02_T1_SR_B2.TIF\",\n", - " \"FILE_NAME_BAND_3\": \"LC08_L2SP_014032_20221219_20230113_02_T1_SR_B3.TIF\",\n", - " \"FILE_NAME_BAND_4\": \"LC08_L2SP_014032_20221219_20230113_02_T1_SR_B4.TIF\",\n", - " \"FILE_NAME_BAND_5\": \"LC08_L2SP_014032_20221219_20230113_02_T1_SR_B5.TIF\",\n", - " \"FILE_NAME_BAND_6\": \"LC08_L2SP_014032_20221219_20230113_02_T1_SR_B6.TIF\",\n", - " \"FILE_NAME_BAND_7\": \"LC08_L2SP_014032_20221219_20230113_02_T1_SR_B7.TIF\",\n", - " \"FILE_NAME_BAND_ST_B10\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_B10.TIF\",\n", - " \"FILE_NAME_THERMAL_RADIANCE\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_TRAD.TIF\",\n", - " \"FILE_NAME_UPWELL_RADIANCE\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_URAD.TIF\",\n", - " \"FILE_NAME_DOWNWELL_RADIANCE\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_DRAD.TIF\",\n", - " \"FILE_NAME_ATMOSPHERIC_TRANSMITTANCE\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_ATRAN.TIF\",\n", - " \"FILE_NAME_EMISSIVITY\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_EMIS.TIF\",\n", - " \"FILE_NAME_EMISSIVITY_STDEV\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_EMSD.TIF\",\n", - " \"FILE_NAME_CLOUD_DISTANCE\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_CDIST.TIF\",\n", - " \"FILE_NAME_QUALITY_L2_AEROSOL\": \"LC08_L2SP_014032_20221219_20230113_02_T1_SR_QA_AEROSOL.TIF\",\n", - " \"FILE_NAME_QUALITY_L2_SURFACE_TEMPERATURE\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_QA.TIF\",\n", - " \"FILE_NAME_QUALITY_L1_PIXEL\": \"LC08_L2SP_014032_20221219_20230113_02_T1_QA_PIXEL.TIF\",\n", - " \"FILE_NAME_QUALITY_L1_RADIOMETRIC_SATURATION\": \"LC08_L2SP_014032_20221219_20230113_02_T1_QA_RADSAT.TIF\",\n", - " \"FILE_NAME_ANGLE_COEFFICIENT\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ANG.txt\",\n", - " \"FILE_NAME_METADATA_ODL\": \"LC08_L2SP_014032_20221219_20230113_02_T1_MTL.txt\",\n", - " \"FILE_NAME_METADATA_XML\": \"LC08_L2SP_014032_20221219_20230113_02_T1_MTL.xml\",\n", - " \"DATA_TYPE_BAND_1\": \"UINT16\",\n", - " \"DATA_TYPE_BAND_2\": \"UINT16\",\n", - " \"DATA_TYPE_BAND_3\": \"UINT16\",\n", - " \"DATA_TYPE_BAND_4\": \"UINT16\",\n", - " \"DATA_TYPE_BAND_5\": \"UINT16\",\n", - " \"DATA_TYPE_BAND_6\": \"UINT16\",\n", - " \"DATA_TYPE_BAND_7\": \"UINT16\",\n", - " \"DATA_TYPE_BAND_ST_B10\": \"UINT16\",\n", - " \"DATA_TYPE_THERMAL_RADIANCE\": \"INT16\",\n", - " \"DATA_TYPE_UPWELL_RADIANCE\": \"INT16\",\n", - " \"DATA_TYPE_DOWNWELL_RADIANCE\": \"INT16\",\n", - " \"DATA_TYPE_ATMOSPHERIC_TRANSMITTANCE\": \"INT16\",\n", - " \"DATA_TYPE_EMISSIVITY\": \"INT16\",\n", - " \"DATA_TYPE_EMISSIVITY_STDEV\": \"INT16\",\n", - " \"DATA_TYPE_CLOUD_DISTANCE\": \"INT16\",\n", - " \"DATA_TYPE_QUALITY_L2_AEROSOL\": \"UINT8\",\n", - " \"DATA_TYPE_QUALITY_L2_SURFACE_TEMPERATURE\": \"INT16\",\n", - " \"DATA_TYPE_QUALITY_L1_PIXEL\": \"UINT16\",\n", - " \"DATA_TYPE_QUALITY_L1_RADIOMETRIC_SATURATION\": \"UINT16\"\n", - " },\n", - " \"IMAGE_ATTRIBUTES\": {\n", - " \"SPACECRAFT_ID\": \"LANDSAT_8\",\n", - " \"SENSOR_ID\": \"OLI_TIRS\",\n", - " \"WRS_TYPE\": \"2\",\n", - " \"WRS_PATH\": \"14\",\n", - " \"WRS_ROW\": \"32\",\n", - " \"NADIR_OFFNADIR\": \"NADIR\",\n", - " \"TARGET_WRS_PATH\": \"14\",\n", - " \"TARGET_WRS_ROW\": \"32\",\n", - " \"DATE_ACQUIRED\": \"2022-12-19\",\n", - " \"SCENE_CENTER_TIME\": \"15:40:17.7299160Z\",\n", - " \"STATION_ID\": \"LGN\",\n", - " \"CLOUD_COVER\": \"43.42\",\n", - " \"CLOUD_COVER_LAND\": \"48.41\",\n", - " \"IMAGE_QUALITY_OLI\": \"9\",\n", - " \"IMAGE_QUALITY_TIRS\": \"9\",\n", - " \"SATURATION_BAND_1\": \"N\",\n", - " \"SATURATION_BAND_2\": \"Y\",\n", - " \"SATURATION_BAND_3\": \"N\",\n", - " \"SATURATION_BAND_4\": \"Y\",\n", - " \"SATURATION_BAND_5\": \"Y\",\n", - " \"SATURATION_BAND_6\": \"Y\",\n", - " \"SATURATION_BAND_7\": \"Y\",\n", - " \"SATURATION_BAND_8\": \"N\",\n", - " \"SATURATION_BAND_9\": \"N\",\n", - " \"ROLL_ANGLE\": \"-0.001\",\n", - " \"SUN_AZIMUTH\": \"160.86021018\",\n", - " \"SUN_ELEVATION\": \"23.81656674\",\n", - " \"EARTH_SUN_DISTANCE\": \"0.9839500\",\n", - " \"TRUNCATION_OLI\": \"UPPER\",\n", - " \"TIRS_SSM_MODEL\": \"FINAL\",\n", - " \"TIRS_SSM_POSITION_STATUS\": \"ESTIMATED\"\n", - " },\n", - " \"PROJECTION_ATTRIBUTES\": {\n", - " \"MAP_PROJECTION\": \"UTM\",\n", - " \"DATUM\": \"WGS84\",\n", - " \"ELLIPSOID\": \"WGS84\",\n", - " \"UTM_ZONE\": \"18\",\n", - " \"GRID_CELL_SIZE_REFLECTIVE\": \"30.00\",\n", - " \"GRID_CELL_SIZE_THERMAL\": \"30.00\",\n", - " \"REFLECTIVE_LINES\": \"7861\",\n", - " \"REFLECTIVE_SAMPLES\": \"7731\",\n", - " \"THERMAL_LINES\": \"7861\",\n", - " \"THERMAL_SAMPLES\": \"7731\",\n", - " \"ORIENTATION\": \"NORTH_UP\",\n", - " \"CORNER_UL_LAT_PRODUCT\": \"41.38441\",\n", - " \"CORNER_UL_LON_PRODUCT\": \"-76.26178\",\n", - " \"CORNER_UR_LAT_PRODUCT\": \"41.38140\",\n", - " \"CORNER_UR_LON_PRODUCT\": \"-73.48833\",\n", - " \"CORNER_LL_LAT_PRODUCT\": \"39.26052\",\n", - " \"CORNER_LL_LON_PRODUCT\": \"-76.22284\",\n", - " \"CORNER_LR_LAT_PRODUCT\": \"39.25773\",\n", - " \"CORNER_LR_LON_PRODUCT\": \"-73.53498\",\n", - " \"CORNER_UL_PROJECTION_X_PRODUCT\": \"394500.000\",\n", - " \"CORNER_UL_PROJECTION_Y_PRODUCT\": \"4582200.000\",\n", - " \"CORNER_UR_PROJECTION_X_PRODUCT\": \"626400.000\",\n", - " \"CORNER_UR_PROJECTION_Y_PRODUCT\": \"4582200.000\",\n", - " \"CORNER_LL_PROJECTION_X_PRODUCT\": \"394500.000\",\n", - " \"CORNER_LL_PROJECTION_Y_PRODUCT\": \"4346400.000\",\n", - " \"CORNER_LR_PROJECTION_X_PRODUCT\": \"626400.000\",\n", - " \"CORNER_LR_PROJECTION_Y_PRODUCT\": \"4346400.000\"\n", - " },\n", - " \"LEVEL2_PROCESSING_RECORD\": {\n", - " \"ORIGIN\": \"Image courtesy of the U.S. Geological Survey\",\n", - " \"DIGITAL_OBJECT_IDENTIFIER\": \"https://doi.org/10.5066/P9OGBGM6\",\n", - " \"REQUEST_ID\": \"1626123_00008\",\n", - " \"LANDSAT_PRODUCT_ID\": \"LC08_L2SP_014032_20221219_20230113_02_T1\",\n", - " \"PROCESSING_LEVEL\": \"L2SP\",\n", - " \"OUTPUT_FORMAT\": \"GEOTIFF\",\n", - " \"DATE_PRODUCT_GENERATED\": \"2023-01-13T02:53:40Z\",\n", - " \"PROCESSING_SOFTWARE_VERSION\": \"LPGS_16.1.0\",\n", - " \"ALGORITHM_SOURCE_SURFACE_REFLECTANCE\": \"LaSRC_1.5.0\",\n", - " \"DATA_SOURCE_OZONE\": \"MODIS\",\n", - " \"DATA_SOURCE_PRESSURE\": \"Calculated\",\n", - " \"DATA_SOURCE_WATER_VAPOR\": \"MODIS\",\n", - " \"DATA_SOURCE_AIR_TEMPERATURE\": \"MODIS\",\n", - " \"ALGORITHM_SOURCE_SURFACE_TEMPERATURE\": \"st_1.3.0\",\n", - " \"DATA_SOURCE_REANALYSIS\": \"GEOS-5 FP-IT\"\n", - " },\n", - " \"LEVEL2_SURFACE_REFLECTANCE_PARAMETERS\": {\n", - " \"REFLECTANCE_MAXIMUM_BAND_1\": \"1.602213\",\n", - " \"REFLECTANCE_MINIMUM_BAND_1\": \"-0.199972\",\n", - " \"REFLECTANCE_MAXIMUM_BAND_2\": \"1.602213\",\n", - " \"REFLECTANCE_MINIMUM_BAND_2\": \"-0.199972\",\n", - " \"REFLECTANCE_MAXIMUM_BAND_3\": \"1.602213\",\n", - " \"REFLECTANCE_MINIMUM_BAND_3\": \"-0.199972\",\n", - " \"REFLECTANCE_MAXIMUM_BAND_4\": \"1.602213\",\n", - " \"REFLECTANCE_MINIMUM_BAND_4\": \"-0.199972\",\n", - " \"REFLECTANCE_MAXIMUM_BAND_5\": \"1.602213\",\n", - " \"REFLECTANCE_MINIMUM_BAND_5\": \"-0.199972\",\n", - " \"REFLECTANCE_MAXIMUM_BAND_6\": \"1.602213\",\n", - " \"REFLECTANCE_MINIMUM_BAND_6\": \"-0.199972\",\n", - " \"REFLECTANCE_MAXIMUM_BAND_7\": \"1.602213\",\n", - " \"REFLECTANCE_MINIMUM_BAND_7\": \"-0.199972\",\n", - " \"QUANTIZE_CAL_MAX_BAND_1\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_1\": \"1\",\n", - " \"QUANTIZE_CAL_MAX_BAND_2\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_2\": \"1\",\n", - " \"QUANTIZE_CAL_MAX_BAND_3\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_3\": \"1\",\n", - " \"QUANTIZE_CAL_MAX_BAND_4\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_4\": \"1\",\n", - " \"QUANTIZE_CAL_MAX_BAND_5\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_5\": \"1\",\n", - " \"QUANTIZE_CAL_MAX_BAND_6\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_6\": \"1\",\n", - " \"QUANTIZE_CAL_MAX_BAND_7\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_7\": \"1\",\n", - " \"REFLECTANCE_MULT_BAND_1\": \"2.75e-05\",\n", - " \"REFLECTANCE_MULT_BAND_2\": \"2.75e-05\",\n", - " \"REFLECTANCE_MULT_BAND_3\": \"2.75e-05\",\n", - " \"REFLECTANCE_MULT_BAND_4\": \"2.75e-05\",\n", - " \"REFLECTANCE_MULT_BAND_5\": \"2.75e-05\",\n", - " \"REFLECTANCE_MULT_BAND_6\": \"2.75e-05\",\n", - " \"REFLECTANCE_MULT_BAND_7\": \"2.75e-05\",\n", - " \"REFLECTANCE_ADD_BAND_1\": \"-0.2\",\n", - " \"REFLECTANCE_ADD_BAND_2\": \"-0.2\",\n", - " \"REFLECTANCE_ADD_BAND_3\": \"-0.2\",\n", - " \"REFLECTANCE_ADD_BAND_4\": \"-0.2\",\n", - " \"REFLECTANCE_ADD_BAND_5\": \"-0.2\",\n", - " \"REFLECTANCE_ADD_BAND_6\": \"-0.2\",\n", - " \"REFLECTANCE_ADD_BAND_7\": \"-0.2\"\n", - " },\n", - " \"LEVEL2_SURFACE_TEMPERATURE_PARAMETERS\": {\n", - " \"TEMPERATURE_MAXIMUM_BAND_ST_B10\": \"372.999941\",\n", - " \"TEMPERATURE_MINIMUM_BAND_ST_B10\": \"149.003418\",\n", - " \"QUANTIZE_CAL_MAXIMUM_BAND_ST_B10\": \"65535\",\n", - " \"QUANTIZE_CAL_MINIMUM_BAND_ST_B10\": \"1\",\n", - " \"TEMPERATURE_MULT_BAND_ST_B10\": \"0.00341802\",\n", - " \"TEMPERATURE_ADD_BAND_ST_B10\": \"149.0\"\n", - " },\n", - " \"LEVEL1_PROCESSING_RECORD\": {\n", - " \"ORIGIN\": \"Image courtesy of the U.S. Geological Survey\",\n", - " \"DIGITAL_OBJECT_IDENTIFIER\": \"https://doi.org/10.5066/P975CC9B\",\n", - " \"REQUEST_ID\": \"1626123_00008\",\n", - " \"LANDSAT_SCENE_ID\": \"LC80140322022353LGN00\",\n", - " \"LANDSAT_PRODUCT_ID\": \"LC08_L1TP_014032_20221219_20230113_02_T1\",\n", - " \"PROCESSING_LEVEL\": \"L1TP\",\n", - " \"COLLECTION_CATEGORY\": \"T1\",\n", - " \"OUTPUT_FORMAT\": \"GEOTIFF\",\n", - " \"DATE_PRODUCT_GENERATED\": \"2023-01-13T02:38:55Z\",\n", - " \"PROCESSING_SOFTWARE_VERSION\": \"LPGS_16.1.0\",\n", - " \"FILE_NAME_BAND_1\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B1.TIF\",\n", - " \"FILE_NAME_BAND_2\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B2.TIF\",\n", - " \"FILE_NAME_BAND_3\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B3.TIF\",\n", - " \"FILE_NAME_BAND_4\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B4.TIF\",\n", - " \"FILE_NAME_BAND_5\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B5.TIF\",\n", - " \"FILE_NAME_BAND_6\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B6.TIF\",\n", - " \"FILE_NAME_BAND_7\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B7.TIF\",\n", - " \"FILE_NAME_BAND_8\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B8.TIF\",\n", - " \"FILE_NAME_BAND_9\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B9.TIF\",\n", - " \"FILE_NAME_BAND_10\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B10.TIF\",\n", - " \"FILE_NAME_BAND_11\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B11.TIF\",\n", - " \"FILE_NAME_QUALITY_L1_PIXEL\": \"LC08_L1TP_014032_20221219_20230113_02_T1_QA_PIXEL.TIF\",\n", - " \"FILE_NAME_QUALITY_L1_RADIOMETRIC_SATURATION\": \"LC08_L1TP_014032_20221219_20230113_02_T1_QA_RADSAT.TIF\",\n", - " \"FILE_NAME_ANGLE_COEFFICIENT\": \"LC08_L1TP_014032_20221219_20230113_02_T1_ANG.txt\",\n", - " \"FILE_NAME_ANGLE_SENSOR_AZIMUTH_BAND_4\": \"LC08_L1TP_014032_20221219_20230113_02_T1_VAA.TIF\",\n", - " \"FILE_NAME_ANGLE_SENSOR_ZENITH_BAND_4\": \"LC08_L1TP_014032_20221219_20230113_02_T1_VZA.TIF\",\n", - " \"FILE_NAME_ANGLE_SOLAR_AZIMUTH_BAND_4\": \"LC08_L1TP_014032_20221219_20230113_02_T1_SAA.TIF\",\n", - " \"FILE_NAME_ANGLE_SOLAR_ZENITH_BAND_4\": \"LC08_L1TP_014032_20221219_20230113_02_T1_SZA.TIF\",\n", - " \"FILE_NAME_METADATA_ODL\": \"LC08_L1TP_014032_20221219_20230113_02_T1_MTL.txt\",\n", - " \"FILE_NAME_METADATA_XML\": \"LC08_L1TP_014032_20221219_20230113_02_T1_MTL.xml\",\n", - " \"FILE_NAME_CPF\": \"LC08CPF_20221001_20221231_02.03\",\n", - " \"FILE_NAME_BPF_OLI\": \"LO8BPF20221219152831_20221219170353.01\",\n", - " \"FILE_NAME_BPF_TIRS\": \"LT8BPF20221215135451_20221222101440.01\",\n", - " \"FILE_NAME_RLUT\": \"LC08RLUT_20150303_20431231_02_01.h5\",\n", - " \"DATA_SOURCE_TIRS_STRAY_LIGHT_CORRECTION\": \"TIRS\",\n", - " \"DATA_SOURCE_ELEVATION\": \"GLS2000\",\n", - " \"GROUND_CONTROL_POINTS_VERSION\": \"5\",\n", - " \"GROUND_CONTROL_POINTS_MODEL\": \"462\",\n", - " \"GEOMETRIC_RMSE_MODEL\": \"8.179\",\n", - " \"GEOMETRIC_RMSE_MODEL_Y\": \"7.213\",\n", - " \"GEOMETRIC_RMSE_MODEL_X\": \"3.856\",\n", - " \"GROUND_CONTROL_POINTS_VERIFY\": \"120\",\n", - " \"GEOMETRIC_RMSE_VERIFY\": \"7.426\"\n", - " },\n", - " \"LEVEL1_MIN_MAX_RADIANCE\": {\n", - " \"RADIANCE_MAXIMUM_BAND_1\": \"785.06079\",\n", - " \"RADIANCE_MINIMUM_BAND_1\": \"-64.83057\",\n", - " \"RADIANCE_MAXIMUM_BAND_2\": \"803.91187\",\n", - " \"RADIANCE_MINIMUM_BAND_2\": \"-66.38730\",\n", - " \"RADIANCE_MAXIMUM_BAND_3\": \"740.79791\",\n", - " \"RADIANCE_MINIMUM_BAND_3\": \"-61.17533\",\n", - " \"RADIANCE_MAXIMUM_BAND_4\": \"624.68250\",\n", - " \"RADIANCE_MINIMUM_BAND_4\": \"-51.58648\",\n", - " \"RADIANCE_MAXIMUM_BAND_5\": \"382.27454\",\n", - " \"RADIANCE_MINIMUM_BAND_5\": \"-31.56836\",\n", - " \"RADIANCE_MAXIMUM_BAND_6\": \"95.06820\",\n", - " \"RADIANCE_MINIMUM_BAND_6\": \"-7.85076\",\n", - " \"RADIANCE_MAXIMUM_BAND_7\": \"32.04307\",\n", - " \"RADIANCE_MINIMUM_BAND_7\": \"-2.64613\",\n", - " \"RADIANCE_MAXIMUM_BAND_8\": \"706.96869\",\n", - " \"RADIANCE_MINIMUM_BAND_8\": \"-58.38170\",\n", - " \"RADIANCE_MAXIMUM_BAND_9\": \"149.40157\",\n", - " \"RADIANCE_MINIMUM_BAND_9\": \"-12.33763\",\n", - " \"RADIANCE_MAXIMUM_BAND_10\": \"22.00180\",\n", - " \"RADIANCE_MINIMUM_BAND_10\": \"0.10033\",\n", - " \"RADIANCE_MAXIMUM_BAND_11\": \"22.00180\",\n", - " \"RADIANCE_MINIMUM_BAND_11\": \"0.10033\"\n", - " },\n", - " \"LEVEL1_MIN_MAX_REFLECTANCE\": {\n", - " \"REFLECTANCE_MAXIMUM_BAND_1\": \"1.210700\",\n", - " \"REFLECTANCE_MINIMUM_BAND_1\": \"-0.099980\",\n", - " \"REFLECTANCE_MAXIMUM_BAND_2\": \"1.210700\",\n", - " \"REFLECTANCE_MINIMUM_BAND_2\": \"-0.099980\",\n", - " \"REFLECTANCE_MAXIMUM_BAND_3\": \"1.210700\",\n", - " \"REFLECTANCE_MINIMUM_BAND_3\": \"-0.099980\",\n", - " \"REFLECTANCE_MAXIMUM_BAND_4\": \"1.210700\",\n", - " \"REFLECTANCE_MINIMUM_BAND_4\": \"-0.099980\",\n", - " \"REFLECTANCE_MAXIMUM_BAND_5\": \"1.210700\",\n", - " \"REFLECTANCE_MINIMUM_BAND_5\": \"-0.099980\",\n", - " \"REFLECTANCE_MAXIMUM_BAND_6\": \"1.210700\",\n", - " \"REFLECTANCE_MINIMUM_BAND_6\": \"-0.099980\",\n", - " \"REFLECTANCE_MAXIMUM_BAND_7\": \"1.210700\",\n", - " \"REFLECTANCE_MINIMUM_BAND_7\": \"-0.099980\",\n", - " \"REFLECTANCE_MAXIMUM_BAND_8\": \"1.210700\",\n", - " \"REFLECTANCE_MINIMUM_BAND_8\": \"-0.099980\",\n", - " \"REFLECTANCE_MAXIMUM_BAND_9\": \"1.210700\",\n", - " \"REFLECTANCE_MINIMUM_BAND_9\": \"-0.099980\"\n", - " },\n", - " \"LEVEL1_MIN_MAX_PIXEL_VALUE\": {\n", - " \"QUANTIZE_CAL_MAX_BAND_1\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_1\": \"1\",\n", - " \"QUANTIZE_CAL_MAX_BAND_2\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_2\": \"1\",\n", - " \"QUANTIZE_CAL_MAX_BAND_3\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_3\": \"1\",\n", - " \"QUANTIZE_CAL_MAX_BAND_4\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_4\": \"1\",\n", - " \"QUANTIZE_CAL_MAX_BAND_5\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_5\": \"1\",\n", - " \"QUANTIZE_CAL_MAX_BAND_6\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_6\": \"1\",\n", - " \"QUANTIZE_CAL_MAX_BAND_7\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_7\": \"1\",\n", - " \"QUANTIZE_CAL_MAX_BAND_8\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_8\": \"1\",\n", - " \"QUANTIZE_CAL_MAX_BAND_9\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_9\": \"1\",\n", - " \"QUANTIZE_CAL_MAX_BAND_10\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_10\": \"1\",\n", - " \"QUANTIZE_CAL_MAX_BAND_11\": \"65535\",\n", - " \"QUANTIZE_CAL_MIN_BAND_11\": \"1\"\n", - " },\n", - " \"LEVEL1_RADIOMETRIC_RESCALING\": {\n", - " \"RADIANCE_MULT_BAND_1\": \"1.2969E-02\",\n", - " \"RADIANCE_MULT_BAND_2\": \"1.3280E-02\",\n", - " \"RADIANCE_MULT_BAND_3\": \"1.2238E-02\",\n", - " \"RADIANCE_MULT_BAND_4\": \"1.0319E-02\",\n", - " \"RADIANCE_MULT_BAND_5\": \"6.3149E-03\",\n", - " \"RADIANCE_MULT_BAND_6\": \"1.5705E-03\",\n", - " \"RADIANCE_MULT_BAND_7\": \"5.2933E-04\",\n", - " \"RADIANCE_MULT_BAND_8\": \"1.1679E-02\",\n", - " \"RADIANCE_MULT_BAND_9\": \"2.4680E-03\",\n", - " \"RADIANCE_MULT_BAND_10\": \"3.3420E-04\",\n", - " \"RADIANCE_MULT_BAND_11\": \"3.3420E-04\",\n", - " \"RADIANCE_ADD_BAND_1\": \"-64.84355\",\n", - " \"RADIANCE_ADD_BAND_2\": \"-66.40058\",\n", - " \"RADIANCE_ADD_BAND_3\": \"-61.18757\",\n", - " \"RADIANCE_ADD_BAND_4\": \"-51.59680\",\n", - " \"RADIANCE_ADD_BAND_5\": \"-31.57467\",\n", - " \"RADIANCE_ADD_BAND_6\": \"-7.85233\",\n", - " \"RADIANCE_ADD_BAND_7\": \"-2.64666\",\n", - " \"RADIANCE_ADD_BAND_8\": \"-58.39338\",\n", - " \"RADIANCE_ADD_BAND_9\": \"-12.34010\",\n", - " \"RADIANCE_ADD_BAND_10\": \"0.10000\",\n", - " \"RADIANCE_ADD_BAND_11\": \"0.10000\",\n", - " \"REFLECTANCE_MULT_BAND_1\": \"2.0000E-05\",\n", - " \"REFLECTANCE_MULT_BAND_2\": \"2.0000E-05\",\n", - " \"REFLECTANCE_MULT_BAND_3\": \"2.0000E-05\",\n", - " \"REFLECTANCE_MULT_BAND_4\": \"2.0000E-05\",\n", - " \"REFLECTANCE_MULT_BAND_5\": \"2.0000E-05\",\n", - " \"REFLECTANCE_MULT_BAND_6\": \"2.0000E-05\",\n", - " \"REFLECTANCE_MULT_BAND_7\": \"2.0000E-05\",\n", - " \"REFLECTANCE_MULT_BAND_8\": \"2.0000E-05\",\n", - " \"REFLECTANCE_MULT_BAND_9\": \"2.0000E-05\",\n", - " \"REFLECTANCE_ADD_BAND_1\": \"-0.100000\",\n", - " \"REFLECTANCE_ADD_BAND_2\": \"-0.100000\",\n", - " \"REFLECTANCE_ADD_BAND_3\": \"-0.100000\",\n", - " \"REFLECTANCE_ADD_BAND_4\": \"-0.100000\",\n", - " \"REFLECTANCE_ADD_BAND_5\": \"-0.100000\",\n", - " \"REFLECTANCE_ADD_BAND_6\": \"-0.100000\",\n", - " \"REFLECTANCE_ADD_BAND_7\": \"-0.100000\",\n", - " \"REFLECTANCE_ADD_BAND_8\": \"-0.100000\",\n", - " \"REFLECTANCE_ADD_BAND_9\": \"-0.100000\"\n", - " },\n", - " \"LEVEL1_THERMAL_CONSTANTS\": {\n", - " \"K1_CONSTANT_BAND_10\": \"774.8853\",\n", - " \"K2_CONSTANT_BAND_10\": \"1321.0789\",\n", - " \"K1_CONSTANT_BAND_11\": \"480.8883\",\n", - " \"K2_CONSTANT_BAND_11\": \"1201.1442\"\n", - " },\n", - " \"LEVEL1_PROJECTION_PARAMETERS\": {\n", - " \"MAP_PROJECTION\": \"UTM\",\n", - " \"DATUM\": \"WGS84\",\n", - " \"ELLIPSOID\": \"WGS84\",\n", - " \"UTM_ZONE\": \"18\",\n", - " \"GRID_CELL_SIZE_PANCHROMATIC\": \"15.00\",\n", - " \"GRID_CELL_SIZE_REFLECTIVE\": \"30.00\",\n", - " \"GRID_CELL_SIZE_THERMAL\": \"30.00\",\n", - " \"ORIENTATION\": \"NORTH_UP\",\n", - " \"RESAMPLING_OPTION\": \"CUBIC_CONVOLUTION\"\n", - " },\n", - " \"ORIGINAL_URL\": \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_MTL.xml\"\n", - "}\n" - ] - } - ], - "source": [ - "metadata = get_metadata(scene_mtls[0])\n", - "print(json.dumps(metadata, indent=4))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There are a number of files referred to by this metadata file which are in the same tree in the cloud. We must provide an easy means for creating a URL for these sidecar files. We can then use `partial` to create a helper function to turn a file name into a URL." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "def download_sidecar(metadata: Dict[str, Any], filename: str) -> str:\n", - " parsed = urlparse(metadata[\"ORIGINAL_URL\"])\n", - " return urlunparse(parsed._replace(path=join(dirname(parsed.path), filename)))" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "download_url = partial(download_sidecar, metadata)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Create a STAC Item from a scene\n", - "\n", - "Now that we have metadata for the scene let's use it to create a [STAC Item](https://github.com/radiantearth/stac-spec/blob/master/item-spec/item-spec.md).\n", - "\n", - "We can use the `help` method to see the signature of the `__init__` method on `pystac.Item`. You can also call `help` directly on `pystac.Item` for broader documentation, or check the [API docs for Item here](https://pystac.readthedocs.io/en/latest/api.html#item)." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Help on function __init__ in module pystac.item:\n", - "\n", - "__init__(self, id: 'str', geometry: 'Optional[Dict[str, Any]]', bbox: 'Optional[List[float]]', datetime: 'Optional[Datetime]', properties: 'Dict[str, Any]', start_datetime: 'Optional[Datetime]' = None, end_datetime: 'Optional[Datetime]' = None, stac_extensions: 'Optional[List[str]]' = None, href: 'Optional[str]' = None, collection: 'Optional[Union[str, Collection]]' = None, extra_fields: 'Optional[Dict[str, Any]]' = None, assets: 'Optional[Dict[str, Asset]]' = None)\n", - " Initialize self. See help(type(self)) for accurate signature.\n", - "\n" - ] - } - ], - "source": [ - "help(pystac.Item.__init__)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can see we'll need at least an `id`, `geometry`, `bbox`, and `datetime`. Properties is required, but can be an empty dictionary that we fill out on the Item once it's created.\n", - "\n", - "> Caution! The `Optional` type hint is used when None can be provided in place of a meaningful argument; it does not indicate that the argument does not need to be supplied—that is only true if a default value is indicated in the type signature." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Item `id`\n", - "\n", - "For the Item's `id`, we'll use the scene ID. We'll chop off the last 5 characters as they are repeated for each ID and so aren't necessary: " - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "def get_item_id(metadata: Dict[str, Any]) -> str:\n", - " return cast(str, metadata[\"LEVEL1_PROCESSING_RECORD\"][\"LANDSAT_SCENE_ID\"][:-5])" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'LC80140322022353'" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item_id = get_item_id(metadata)\n", - "item_id" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Item `datetime`\n", - "\n", - "Here we parse the datetime of the Item from two metadata fields that describe the date and time the scene was captured:" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "def get_datetime(metadata: Dict[str, Any]) -> datetime:\n", - " return parse(\n", - " \"%sT%s\"\n", - " % (\n", - " metadata[\"IMAGE_ATTRIBUTES\"][\"DATE_ACQUIRED\"],\n", - " metadata[\"IMAGE_ATTRIBUTES\"][\"SCENE_CENTER_TIME\"],\n", - " )\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "datetime.datetime(2022, 12, 19, 15, 40, 17, 729916, tzinfo=tzutc())" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item_datetime = get_datetime(metadata)\n", - "item_datetime" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Item `bbox`\n", - "\n", - "Here we read in the bounding box information from the scene and transform it into the format of the Item's `bbox` property:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "def get_bbox(metadata: Dict[str, Any]) -> List[float]:\n", - " metadata = metadata[\"PROJECTION_ATTRIBUTES\"]\n", - " coords = [\n", - " [\n", - " [\n", - " float(metadata[\"CORNER_UL_LON_PRODUCT\"]),\n", - " float(metadata[\"CORNER_UL_LAT_PRODUCT\"]),\n", - " ],\n", - " [\n", - " float(metadata[\"CORNER_UR_LON_PRODUCT\"]),\n", - " float(metadata[\"CORNER_UR_LAT_PRODUCT\"]),\n", - " ],\n", - " [\n", - " float(metadata[\"CORNER_LR_LON_PRODUCT\"]),\n", - " float(metadata[\"CORNER_LR_LAT_PRODUCT\"]),\n", - " ],\n", - " [\n", - " float(metadata[\"CORNER_LL_LON_PRODUCT\"]),\n", - " float(metadata[\"CORNER_LL_LAT_PRODUCT\"]),\n", - " ],\n", - " [\n", - " float(metadata[\"CORNER_UL_LON_PRODUCT\"]),\n", - " float(metadata[\"CORNER_UL_LAT_PRODUCT\"]),\n", - " ],\n", - " ]\n", - " ]\n", - " lats = [c[1] for c in coords[0]]\n", - " lons = [c[0] for c in coords[0]]\n", - " return [min(lons), min(lats), max(lons), max(lats)]" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[-76.26178, 39.25773, -73.48833, 41.38441]" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item_bbox = get_bbox(metadata)\n", - "item_bbox" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Item `geometry`\n", - "\n", - "Getting the geometry of the scene is a little more tricky. The bounding box will be a axis-aligned rectangle of the area the scene occupies, but will not represent the true footprint of the image - Landsat scenes are \"tilted\" according the the coordinate reference system, so there will be areas in the corner where no image data exists. When constructing a STAC Item it's best if you have the Item geometry represent the true footprint of the assets.\n", - "\n", - "To get the footprint of the scene we'll read in another metadata file that lives alongside the MTL - the `ANG.txt` file. This function uses the ANG file and the bbox to construct the GeoJSON polygon that represents the footprint of the scene:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [], - "source": [ - "def get_geometry(metadata: Dict[str, Any], bbox: List[float]) -> Dict[str, Any]:\n", - " url = download_sidecar(\n", - " metadata, metadata[\"PRODUCT_CONTENTS\"][\"FILE_NAME_ANGLE_COEFFICIENT\"]\n", - " )\n", - " sz = []\n", - " coords = []\n", - " ang_text = stac_io.read_text(pc.sign(url))\n", - " if not ang_text.startswith(\"GROUP\"):\n", - " raise ValueError(f\"ANG file for url {url} is incorrectly formatted\")\n", - " for line in ang_text.split(\"\\n\"):\n", - " if \"BAND01_NUM_L1T_LINES\" in line or \"BAND01_NUM_L1T_SAMPS\" in line:\n", - " sz.append(float(line.split(\"=\")[1]))\n", - " if (\n", - " \"BAND01_L1T_IMAGE_CORNER_LINES\" in line\n", - " or \"BAND01_L1T_IMAGE_CORNER_SAMPS\" in line\n", - " ):\n", - " coords.append(\n", - " [float(v) for v in line.split(\"=\")[1].strip().strip(\"()\").split(\",\")]\n", - " )\n", - " if len(coords) == 2:\n", - " break\n", - " dlon = bbox[2] - bbox[0]\n", - " dlat = bbox[3] - bbox[1]\n", - " lons = [c / sz[1] * dlon + bbox[0] for c in coords[1]]\n", - " lats = [((sz[0] - c) / sz[0]) * dlat + bbox[1] for c in coords[0]]\n", - " coordinates = [\n", - " [\n", - " [lons[0], lats[0]],\n", - " [lons[1], lats[1]],\n", - " [lons[2], lats[2]],\n", - " [lons[3], lats[3]],\n", - " [lons[0], lats[0]],\n", - " ]\n", - " ]\n", - "\n", - " return {\"type\": \"Polygon\", \"coordinates\": coordinates}" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"type\": \"Polygon\",\n", - " \"coordinates\": [\n", - " [\n", - " [\n", - " -75.71075270108336,\n", - " 41.3823086369878\n", - " ],\n", - " [\n", - " -73.48924866988654,\n", - " 40.980654308234485\n", - " ],\n", - " [\n", - " -74.0425618957281,\n", - " 39.25823722657151\n", - " ],\n", - " [\n", - " -76.26093009667797,\n", - " 39.66800780107756\n", - " ],\n", - " [\n", - " -75.71075270108336,\n", - " 41.3823086369878\n", - " ]\n", - " ]\n", - " ]\n", - "}\n" - ] - } - ], - "source": [ - "item_geometry = get_geometry(metadata, item_bbox)\n", - "print(json.dumps(item_geometry, indent=2))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This would be a good time to check our work - we can print out the GeoJSON and use [geojson.io](https://geojson.io/) to check and make sure we're using scenes that overlap our location. If this footprint is somewhere unexpected in the world, make sure the Lat/Long coordinates are correct and in the right order!" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Create the item\n", - "\n", - "Now that we have the required attributes for an Item we can create it:" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [], - "source": [ - "item = pystac.Item(\n", - " id=item_id,\n", - " datetime=item_datetime,\n", - " geometry=item_geometry,\n", - " bbox=item_bbox,\n", - " properties={},\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "PySTAC has a `validate` method on STAC objects, which you can use to make sure you're constructing things correctly. If there's an issue the following line will throw an exception:" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json']" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.validate()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Add Ground Sample Distance to common metadata\n", - "\n", - "We'll add the Ground Sample Distance that is defined as part of the Item [Common Metadata](https://github.com/radiantearth/stac-spec/blob/master/item-spec/common-metadata.md). We define this on the Item level as 30 meters, which is the GSD for most of the Landsat bands. However, if some bands have a different resolution; we can account for this by setting the GSD explicitly for each of those bands below." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [], - "source": [ - "item.common_metadata.gsd = 30.0" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Adding the EO extension\n", - "\n", - "STAC has a rich [set of extensions](https://stac-extensions.github.io/) that allow STAC objects to encode information that is not part of the core spec but is used widely and standardized. These extensions allow us to augment STAC objects with additional structured metadata that describe referenced data with semantically-meaningful fields. An example of this is the [eo extension](https://github.com/stac-extensions/eo), which captures fields needed for electro-optical data, like center wavelength and full-width half maximum values.\n", - "\n", - "This notebook will also rely on other extensions; but as they will apply to different objects, not just the item itself, they will be invoked later.\n", - "\n", - "For now, we will enable the EO extension for this item by using the `ext` property provided by the extension object:" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [], - "source": [ - "eo_ext = EOExtension.ext(item, add_if_missing=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Add cloud cover\n", - "\n", - "Here we add cloud cover from the metadata as part of the `eo` extension." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [], - "source": [ - "def get_cloud_cover(metadata: Dict[str, Any]) -> float:\n", - " return float(metadata[\"IMAGE_ATTRIBUTES\"][\"CLOUD_COVER\"])" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "43.42" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "eo_ext.cloud_cover = get_cloud_cover(metadata)\n", - "eo_ext.cloud_cover" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Adding assets\n", - "\n", - "STAC Items contain a list of [Assets](https://github.com/radiantearth/stac-spec/blob/master/item-spec/item-spec.md#asset-object), which are a list of files that relate to the Item. In our case we'll be cataloging each file related to the scene, including the Landsat band files as well as the metadata files associated with the scene.\n", - "\n", - "Each asset will have a name, some basic properties, and then possibly some properties defined by the various extensions in use (`eo`, `raster`, and `classification`). So, we begin by defining a type alias for this package of information and some helper functions for creating them:" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [], - "source": [ - "class BandInfo(TypedDict):\n", - " name: str\n", - " asset_fields: Dict[str, str]\n", - " extensions: List[Union[EOBand, RasterBand, List[Bitfield]]]\n", - "\n", - "\n", - "def eo_band_info(\n", - " common_name: str,\n", - " href: str,\n", - " name: str,\n", - " description: str,\n", - " center: float,\n", - " fwhm: float,\n", - " default_raster_band: Optional[RasterBand] = None,\n", - "):\n", - " raster_band = (\n", - " RasterBand.create(\n", - " spatial_resolution=30.0,\n", - " scale=0.0000275,\n", - " nodata=0,\n", - " offset=-0.2,\n", - " data_type=\"uint16\",\n", - " )\n", - " if default_raster_band is None\n", - " else default_raster_band\n", - " )\n", - " return {\n", - " \"name\": common_name,\n", - " \"asset_fields\": {\n", - " \"href\": href,\n", - " \"media_type\": str(pystac.media_type.MediaType.COG),\n", - " },\n", - " \"extensions\": [\n", - " EOBand.create(\n", - " name=name,\n", - " common_name=common_name,\n", - " description=description,\n", - " center_wavelength=center,\n", - " full_width_half_max=fwhm,\n", - " ),\n", - " raster_band,\n", - " ],\n", - " }\n", - "\n", - "\n", - "def plain_band_info(name: str, href: str, title: str, ext: RasterBand):\n", - " return {\n", - " \"name\": name,\n", - " \"asset_fields\": {\n", - " \"href\": href,\n", - " \"media_type\": str(pystac.media_type.MediaType.COG),\n", - " \"title\": title,\n", - " },\n", - " \"extensions\": [ext],\n", - " }" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Some common raster band information definitions will also be useful." - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [], - "source": [ - "thermal_raster_band = RasterBand.create(\n", - " spatial_resolution=30.0,\n", - " scale=0.00341802,\n", - " nodata=0,\n", - " offset=149.0,\n", - " data_type=\"uint6\",\n", - " unit=\"kelvin\",\n", - ")\n", - "radiance_raster_band = RasterBand.create(\n", - " unit=\"watt/steradian/square_meter/micrometer\",\n", - " scale=1e-3,\n", - " nodata=-9999,\n", - " data_type=\"uint16\",\n", - " spatial_resolution=30.0,\n", - ")\n", - "emissivity_transmission_raster_band = RasterBand.create(\n", - " scale=1e-4,\n", - " nodata=-9999,\n", - " data_type=\"int16\",\n", - " spatial_resolution=30.0,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Several QA bands are provided that utilize bit-wise masks which we can define using the [classification extension](https://github.com/stac-extensions/classification). Because these definitions can be verbose, we provide some additional helper functions to minimize the length of their definition." - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [], - "source": [ - "def create_bitfield(\n", - " offset: int,\n", - " length: int,\n", - " name: str,\n", - " field_names_descriptions: List[Tuple[str, str]],\n", - " description: Optional[str] = None,\n", - ") -> Bitfield:\n", - " return Bitfield.create(\n", - " offset=offset,\n", - " length=length,\n", - " name=name,\n", - " description=description,\n", - " classes=[\n", - " Classification.create(value=i, name=n, description=d)\n", - " for (i, (n, d)) in enumerate(field_names_descriptions)\n", - " ],\n", - " )\n", - "\n", - "\n", - "def create_qa_bitfield(\n", - " offset: int,\n", - " class_name: str,\n", - " description: Optional[Union[str, Tuple[str, str]]] = None,\n", - ") -> Bitfield:\n", - " if description is None:\n", - " descr0 = f\"{class_name.replace('_', ' ').capitalize()} confidence is not high\"\n", - " descr1 = f\"High confidence {class_name.replace('_', ' ')}\"\n", - " elif isinstance(description, str):\n", - " descr0 = f\"{description.capitalize()} confidence is not high\"\n", - " descr1 = f\"High confidence {description.lower()}\"\n", - " else:\n", - " descr0 = description[0]\n", - " descr1 = description[1]\n", - "\n", - " return create_bitfield(\n", - " offset, 1, class_name, [(f\"not_{class_name}\", descr0), (class_name, descr1)]\n", - " )\n", - "\n", - "\n", - "def create_confidence_bitfield(\n", - " offset: int, class_name: str, use_medium: bool = False\n", - ") -> Bitfield:\n", - " label = class_name.replace(\"_\", \" \")\n", - " return create_bitfield(\n", - " offset,\n", - " 2,\n", - " f\"{class_name}_confidence\",\n", - " [\n", - " (\"not_set\", \"No confidence level set\"),\n", - " (\"low\", f\"Low confidence {label}\"),\n", - " (\n", - " (\"medium\", f\"Medium confidence {label}\")\n", - " if use_medium\n", - " else (\"reserved\", \"Reserved - value not used\")\n", - " ),\n", - " (\"high\", f\"High confidence {label}\"),\n", - " ],\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can now create the `BandInfo` definitions for the Landsat scenes. This begins with the definition of a function to convert metadata into a list of `BandInfo` records, which is lengthy but ultimately straightforward." - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [], - "source": [ - "def landsat_band_info(\n", - " metadata: Dict[str, Any], downloader: Callable[[str], str]\n", - ") -> List[BandInfo]:\n", - " product_contents = metadata[\"PRODUCT_CONTENTS\"]\n", - " return [\n", - " {\n", - " \"name\": \"ang\",\n", - " \"asset_fields\": {\n", - " \"href\": downloader(product_contents[\"FILE_NAME_ANGLE_COEFFICIENT\"]),\n", - " \"media_type\": \"text/plain\",\n", - " \"title\": \"Angle coefficients\",\n", - " },\n", - " \"extensions\": [],\n", - " },\n", - " {\n", - " \"name\": \"mtl.txt\",\n", - " \"asset_fields\": {\n", - " \"href\": downloader(product_contents[\"FILE_NAME_METADATA_ODL\"]),\n", - " \"media_type\": \"text/plain\",\n", - " \"title\": \"Product metadata\",\n", - " },\n", - " \"extensions\": [],\n", - " },\n", - " eo_band_info(\n", - " \"coastal\",\n", - " downloader(product_contents[\"FILE_NAME_BAND_1\"]),\n", - " \"OLI_B1\",\n", - " \"Coastal/Aerosol (Operational Land Imager)\",\n", - " 0.44,\n", - " 0.02,\n", - " ),\n", - " eo_band_info(\n", - " \"blue\",\n", - " downloader(product_contents[\"FILE_NAME_BAND_2\"]),\n", - " \"OLI_B2\",\n", - " \"Visible blue (Operational Land Imager)\",\n", - " 0.48,\n", - " 0.06,\n", - " ),\n", - " eo_band_info(\n", - " \"green\",\n", - " downloader(product_contents[\"FILE_NAME_BAND_3\"]),\n", - " \"OLI_B3\",\n", - " \"Visible green (Operational Land Imager)\",\n", - " 0.56,\n", - " 0.06,\n", - " ),\n", - " eo_band_info(\n", - " \"red\",\n", - " downloader(product_contents[\"FILE_NAME_BAND_4\"]),\n", - " \"OLI_B4\",\n", - " \"Visible red (Operational Land Imager)\",\n", - " 0.65,\n", - " 0.04,\n", - " ),\n", - " eo_band_info(\n", - " \"nir08\",\n", - " downloader(product_contents[\"FILE_NAME_BAND_5\"]),\n", - " \"OLI_B5\",\n", - " \"Near infrared (Operational Land Imager)\",\n", - " 0.87,\n", - " 0.03,\n", - " ),\n", - " eo_band_info(\n", - " \"swir16\",\n", - " downloader(product_contents[\"FILE_NAME_BAND_6\"]),\n", - " \"OLI_B6\",\n", - " \"Short-wave infrared (Operational Land Imager)\",\n", - " 1.61,\n", - " 0.09,\n", - " ),\n", - " eo_band_info(\n", - " \"swir22\",\n", - " downloader(product_contents[\"FILE_NAME_BAND_7\"]),\n", - " \"OLI_B7\",\n", - " \"Short-wave infrared (Operational Land Imager)\",\n", - " 2.2,\n", - " 0.19,\n", - " ),\n", - " eo_band_info(\n", - " \"lwir11\",\n", - " downloader(product_contents[\"FILE_NAME_BAND_ST_B10\"]),\n", - " \"TIRS_B10\",\n", - " \"Long-wave infrared (Thermal InfraRed Sensor)\",\n", - " 10.9,\n", - " 0.59,\n", - " thermal_raster_band,\n", - " ),\n", - " plain_band_info(\n", - " \"trad\",\n", - " downloader(product_contents[\"FILE_NAME_THERMAL_RADIANCE\"]),\n", - " \"Thermal radiance\",\n", - " radiance_raster_band,\n", - " ),\n", - " plain_band_info(\n", - " \"urad\",\n", - " downloader(product_contents[\"FILE_NAME_UPWELL_RADIANCE\"]),\n", - " \"Upwelled radiance\",\n", - " radiance_raster_band,\n", - " ),\n", - " plain_band_info(\n", - " \"drad\",\n", - " downloader(product_contents[\"FILE_NAME_DOWNWELL_RADIANCE\"]),\n", - " \"Downwelled radiance\",\n", - " radiance_raster_band,\n", - " ),\n", - " plain_band_info(\n", - " \"emis\",\n", - " downloader(product_contents[\"FILE_NAME_EMISSIVITY\"]),\n", - " \"Emissivity\",\n", - " emissivity_transmission_raster_band,\n", - " ),\n", - " plain_band_info(\n", - " \"emsd\",\n", - " downloader(product_contents[\"FILE_NAME_EMISSIVITY_STDEV\"]),\n", - " \"Emissivity standard deviation\",\n", - " emissivity_transmission_raster_band,\n", - " ),\n", - " plain_band_info(\n", - " \"atran\",\n", - " downloader(product_contents[\"FILE_NAME_ATMOSPHERIC_TRANSMITTANCE\"]),\n", - " \"Atmospheric transmission\",\n", - " emissivity_transmission_raster_band,\n", - " ),\n", - " plain_band_info(\n", - " \"cdist\",\n", - " downloader(product_contents[\"FILE_NAME_CLOUD_DISTANCE\"]),\n", - " \"Cloud distance\",\n", - " RasterBand.create(\n", - " unit=\"kilometer\",\n", - " scale=1e-2,\n", - " nodata=-9999,\n", - " data_type=\"uint16\",\n", - " spatial_resolution=30.0,\n", - " ),\n", - " ),\n", - " {\n", - " \"name\": \"qa\",\n", - " \"asset_fields\": {\n", - " \"href\": downloader(\n", - " product_contents[\"FILE_NAME_QUALITY_L2_SURFACE_TEMPERATURE\"]\n", - " ),\n", - " \"title\": \"Surface Temperature Quality Assessment Band\",\n", - " },\n", - " \"extensions\": [\n", - " RasterBand.create(\n", - " unit=\"kelvin\",\n", - " scale=1e-2,\n", - " nodata=-9999,\n", - " data_type=\"int16\",\n", - " spatial_resolution=30,\n", - " )\n", - " ],\n", - " },\n", - " {\n", - " \"name\": \"qa_pixel\",\n", - " \"asset_fields\": {\n", - " \"href\": downloader(product_contents[\"FILE_NAME_QUALITY_L1_PIXEL\"]),\n", - " \"media_type\": str(pystac.media_type.MediaType.COG),\n", - " \"title\": \"Pixel quality assessment\",\n", - " },\n", - " \"extensions\": [\n", - " [\n", - " create_qa_bitfield(0, \"fill\", (\"Image data\", \"Fill data\")),\n", - " create_qa_bitfield(\n", - " 1,\n", - " \"dilated_cloud\",\n", - " (\"Cloud is not dilated or no cloud\", \"Dilated cloud\"),\n", - " ),\n", - " create_qa_bitfield(2, \"cirrus\"),\n", - " create_qa_bitfield(3, \"cloud\"),\n", - " create_qa_bitfield(4, \"cloud_shadow\"),\n", - " create_qa_bitfield(5, \"snow\"),\n", - " create_qa_bitfield(6, \"clear\"),\n", - " create_qa_bitfield(7, \"water\"),\n", - " create_confidence_bitfield(8, \"cloud\", True),\n", - " create_confidence_bitfield(10, \"cloud_shadow\"),\n", - " create_confidence_bitfield(12, \"snow\"),\n", - " create_confidence_bitfield(14, \"cirrus\"),\n", - " ]\n", - " ],\n", - " },\n", - " {\n", - " \"name\": \"qa_radsat\",\n", - " \"asset_fields\": {\n", - " \"href\": downloader(\n", - " product_contents[\"FILE_NAME_QUALITY_L1_RADIOMETRIC_SATURATION\"]\n", - " ),\n", - " \"media_type\": str(pystac.media_type.MediaType.COG),\n", - " \"description\": (\n", - " \"Collection 2 Level-1 Radiometric Saturation and \"\n", - " \"Terrain Occlusion Quality Assessment \"\n", - " \"Band (QA_RADSAT)\"\n", - " ),\n", - " },\n", - " \"extensions\": [\n", - " [\n", - " Bitfield.create(\n", - " offset=i - 1,\n", - " length=1,\n", - " description=f\"Band {i} radiometric saturation\",\n", - " classes=[\n", - " Classification.create(\n", - " 0, f\"Band {i} not saturated\", \"not_saturated\"\n", - " ),\n", - " Classification.create(\n", - " 1, f\"Band {i} saturated\", \"saturated\"\n", - " ),\n", - " ],\n", - " )\n", - " for i in [1, 2, 3, 4, 5, 6, 7, 9]\n", - " ]\n", - " + [\n", - " Bitfield.create(\n", - " offset=11,\n", - " length=1,\n", - " description=(\n", - " \"Terrain not visible from sensor due to \"\n", - " \"intervening terrain\"\n", - " ),\n", - " classes=[\n", - " Classification.create(\n", - " 0, \"Terrain is not occluded\", \"not_occluded\"\n", - " ),\n", - " Classification.create(1, \"Terrain is occluded\", \"occluded\"),\n", - " ],\n", - " )\n", - " ]\n", - " ],\n", - " },\n", - " {\n", - " \"name\": \"qa_aerosol\",\n", - " \"asset_fields\": {\n", - " \"href\": downloader(product_contents[\"FILE_NAME_QUALITY_L2_AEROSOL\"]),\n", - " \"media_type\": str(pystac.media_type.MediaType.COG),\n", - " \"title\": \"Aerosol Quality Assessment Band\",\n", - " },\n", - " \"extensions\": [\n", - " [\n", - " create_bitfield(\n", - " 0,\n", - " 1,\n", - " \"fill\",\n", - " [(\"not_fill\", \"Pixel is not fill\"), (\"fill\", \"Pixel is fill\")],\n", - " \"Image or fill data\",\n", - " ),\n", - " create_bitfield(\n", - " 1,\n", - " 1,\n", - " \"retrieval\",\n", - " [\n", - " (\"not_valid\", \"Pixel retrieval is not valid\"),\n", - " (\"valid\", \"Pixel retrieval is valid\"),\n", - " ],\n", - " \"Valid aerosol retrieval\",\n", - " ),\n", - " create_bitfield(\n", - " 2,\n", - " 1,\n", - " \"water\",\n", - " [\n", - " (\"not_water\", \"Pixel is not water\"),\n", - " (\"water\", \"Pixel is water\"),\n", - " ],\n", - " \"Water mask\",\n", - " ),\n", - " create_bitfield(\n", - " 5,\n", - " 1,\n", - " \"interpolated\",\n", - " [\n", - " (\"not_interpolated\", \"Pixel is not interpolated\"),\n", - " (\"interpolated\", \"Pixel is interpolated\"),\n", - " ],\n", - " \"Aerosol interpolation\",\n", - " ),\n", - " create_bitfield(\n", - " 6,\n", - " 2,\n", - " \"level\",\n", - " [\n", - " (\"climatology\", \"No aerosol correction applied\"),\n", - " (\"low\", \"Low aerosol level\"),\n", - " (\"medium\", \"Medium aerosol level\"),\n", - " (\"high\", \"High aerosol level\"),\n", - " ],\n", - " \"Aerosol level\",\n", - " ),\n", - " ]\n", - " ],\n", - " },\n", - " ]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For illustration purposes, we can look at the band info records for an example scene:" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'name': 'ang',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ANG.txt',\n", - " 'media_type': 'text/plain',\n", - " 'title': 'Angle coefficients'},\n", - " 'extensions': []},\n", - " {'name': 'mtl.txt',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_MTL.txt',\n", - " 'media_type': 'text/plain',\n", - " 'title': 'Product metadata'},\n", - " 'extensions': []},\n", - " {'name': 'coastal',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_B1.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized'},\n", - " 'extensions': []},\n", - " {'name': 'blue',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_B2.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized'},\n", - " 'extensions': []},\n", - " {'name': 'green',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_B3.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized'},\n", - " 'extensions': []},\n", - " {'name': 'red',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_B4.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized'},\n", - " 'extensions': []},\n", - " {'name': 'nir08',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_B5.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized'},\n", - " 'extensions': []},\n", - " {'name': 'swir16',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_B6.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized'},\n", - " 'extensions': []},\n", - " {'name': 'swir22',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_B7.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized'},\n", - " 'extensions': []},\n", - " {'name': 'lwir11',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_B10.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized'},\n", - " 'extensions': []},\n", - " {'name': 'trad',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_TRAD.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", - " 'title': 'Thermal radiance'},\n", - " 'extensions': []},\n", - " {'name': 'urad',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_URAD.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", - " 'title': 'Upwelled radiance'},\n", - " 'extensions': []},\n", - " {'name': 'drad',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_DRAD.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", - " 'title': 'Downwelled radiance'},\n", - " 'extensions': []},\n", - " {'name': 'emis',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_EMIS.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", - " 'title': 'Emissivity'},\n", - " 'extensions': []},\n", - " {'name': 'emsd',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_EMSD.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", - " 'title': 'Emissivity standard deviation'},\n", - " 'extensions': []},\n", - " {'name': 'atran',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_ATRAN.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", - " 'title': 'Atmospheric transmission'},\n", - " 'extensions': []},\n", - " {'name': 'cdist',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_CDIST.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", - " 'title': 'Cloud distance'},\n", - " 'extensions': []},\n", - " {'name': 'qa',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_QA.TIF',\n", - " 'title': 'Surface Temperature Quality Assessment Band'},\n", - " 'extensions': []},\n", - " {'name': 'qa_pixel',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_QA_PIXEL.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", - " 'title': 'Pixel quality assessment'},\n", - " 'extensions': [[, ]>,\n", - " , ]>,\n", - " , ]>,\n", - " , ]>,\n", - " , ]>,\n", - " , ]>,\n", - " , ]>,\n", - " , ]>,\n", - " , , , ]>,\n", - " , , , ]>,\n", - " , , , ]>,\n", - " , , , ]>]]},\n", - " {'name': 'qa_radsat',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_QA_RADSAT.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", - " 'description': 'Collection 2 Level-1 Radiometric Saturation and Terrain Occlusion Quality Assessment Band (QA_RADSAT)'},\n", - " 'extensions': [[, ]>,\n", - " , ]>,\n", - " , ]>,\n", - " , ]>,\n", - " , ]>,\n", - " , ]>,\n", - " , ]>,\n", - " , ]>,\n", - " , ]>]]},\n", - " {'name': 'qa_aerosol',\n", - " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_QA_AEROSOL.TIF',\n", - " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", - " 'title': 'Aerosol Quality Assessment Band'},\n", - " 'extensions': [[, ]>,\n", - " , ]>,\n", - " , ]>,\n", - " , ]>,\n", - " , , , ]>]]}]" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "landsat_band_info(metadata, download_url)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "With this information we can now define a method that adds all the relevant assets for a scene to an item:" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "metadata": {}, - "outputs": [], - "source": [ - "def add_assets(item: pystac.Item, band_info: List[BandInfo]) -> None:\n", - " for band in band_info:\n", - " asset = pystac.Asset(**band[\"asset_fields\"])\n", - " asset.set_owner(item)\n", - " for ext_data in band[\"extensions\"]:\n", - " if isinstance(ext_data, EOBand):\n", - " EOExtension.ext(asset, add_if_missing=True).bands = [ext_data]\n", - " elif isinstance(ext_data, RasterBand):\n", - " RasterExtension.ext(asset, add_if_missing=True).bands = [ext_data]\n", - " elif isinstance(ext_data, list):\n", - " ClassificationExtension.ext(\n", - " asset, add_if_missing=True\n", - " ).bitfields = ext_data\n", - " item.add_asset(band[\"name\"], asset)" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": {}, - "outputs": [], - "source": [ - "add_assets(item, landsat_band_info(metadata, download_url))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can examine the item to ensure that the assets appear as expected." - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_B5.TIF',\n", - " 'type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", - " 'eo:bands': [{'name': 'OLI_B5',\n", - " 'common_name': 'nir08',\n", - " 'description': 'Near infrared (Operational Land Imager)',\n", - " 'center_wavelength': 0.87,\n", - " 'full_width_half_max': 0.03}]}" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.assets[\"nir08\"].to_dict()" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_QA_AEROSOL.TIF',\n", - " 'type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", - " 'title': 'Aerosol Quality Assessment Band',\n", - " 'classification:bitfields': [{'offset': 0,\n", - " 'length': 1,\n", - " 'classes': [{'value': 0,\n", - " 'name': 'not_fill',\n", - " 'description': 'Pixel is not fill'},\n", - " {'value': 1, 'name': 'fill', 'description': 'Pixel is fill'}],\n", - " 'description': 'Image or fill data',\n", - " 'name': 'fill'},\n", - " {'offset': 1,\n", - " 'length': 1,\n", - " 'classes': [{'value': 0,\n", - " 'name': 'not_valid',\n", - " 'description': 'Pixel retrieval is not valid'},\n", - " {'value': 1, 'name': 'valid', 'description': 'Pixel retrieval is valid'}],\n", - " 'description': 'Valid aerosol retrieval',\n", - " 'name': 'retrieval'},\n", - " {'offset': 2,\n", - " 'length': 1,\n", - " 'classes': [{'value': 0,\n", - " 'name': 'not_water',\n", - " 'description': 'Pixel is not water'},\n", - " {'value': 1, 'name': 'water', 'description': 'Pixel is water'}],\n", - " 'description': 'Water mask',\n", - " 'name': 'water'},\n", - " {'offset': 5,\n", - " 'length': 1,\n", - " 'classes': [{'value': 0,\n", - " 'name': 'not_interpolated',\n", - " 'description': 'Pixel is not interpolated'},\n", - " {'value': 1,\n", - " 'name': 'interpolated',\n", - " 'description': 'Pixel is interpolated'}],\n", - " 'description': 'Aerosol interpolation',\n", - " 'name': 'interpolated'},\n", - " {'offset': 6,\n", - " 'length': 2,\n", - " 'classes': [{'value': 0,\n", - " 'name': 'climatology',\n", - " 'description': 'No aerosol correction applied'},\n", - " {'value': 1, 'name': 'low', 'description': 'Low aerosol level'},\n", - " {'value': 2, 'name': 'medium', 'description': 'Medium aerosol level'},\n", - " {'value': 3, 'name': 'high', 'description': 'High aerosol level'}],\n", - " 'description': 'Aerosol level',\n", - " 'name': 'level'}]}" - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.assets[\"qa_aerosol\"].to_dict()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Add projection information\n", - "\n", - "We can specify the EPSG code for the scene as part of the [projection extension](https://github.com/stac-extensions/projection). The below method, adapted from [stactools](https://github.com/stactools-packages/landsat/blob/9f595a9d5ed6b62a2e96338e79f5bb502a7d90d0/src/stactools/landsat/mtl_metadata.py#L86-L109), figures out the correct UTM Zone EPSG:" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": {}, - "outputs": [], - "source": [ - "def get_epsg(metadata: Dict[str, Any], min_lat: float, max_lat: float) -> Optional[int]:\n", - " if \"UTM_ZONE\" in metadata[\"PROJECTION_ATTRIBUTES\"]:\n", - " utm_zone_integer = metadata[\"PROJECTION_ATTRIBUTES\"][\"UTM_ZONE\"].zfill(2)\n", - " return int(f\"326{utm_zone_integer}\")\n", - " else:\n", - " lat_ts = metadata[\"PROJECTION_ATTRIBUTES\"][\"TRUE_SCALE_LAT\"]\n", - " if lat_ts == \"-71.00000\":\n", - " # Antarctic\n", - " return 3031\n", - " elif lat_ts == \"71.00000\":\n", - " # Arctic\n", - " return 3995\n", - " else:\n", - " raise ValueError(\n", - " f\"Unexpeced value for PROJECTION_ATTRIBUTES/TRUE_SCALE_LAT: {lat_ts} \"\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "32618" - ] - }, - "execution_count": 34, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "proj_ext = ProjectionExtension.ext(item, add_if_missing=True)\n", - "assert item.bbox is not None\n", - "proj_ext.epsg = get_epsg(metadata, item.bbox[1], item.bbox[3])\n", - "proj_ext.epsg" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Add view geometry information\n", - "\n", - "The [View Geometry](https://github.com/stac-extensions/view) extension specifies information related to angles of sensors and other radiance angles that affect the view of resulting data. The Landsat metadata specifies two of these parameters, so we add them to our Item:" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "metadata": {}, - "outputs": [], - "source": [ - "def get_view_info(metadata: Dict[str, Any]) -> Dict[str, float]:\n", - " return {\n", - " \"sun_azimuth\": float(metadata[\"IMAGE_ATTRIBUTES\"][\"SUN_AZIMUTH\"]),\n", - " \"sun_elevation\": float(metadata[\"IMAGE_ATTRIBUTES\"][\"SUN_ELEVATION\"]),\n", - " }" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'datetime': '2022-12-19T15:40:17.729916Z',\n", - " 'gsd': 30.0,\n", - " 'eo:cloud_cover': 43.42,\n", - " 'proj:epsg': 32618,\n", - " 'view:sun_azimuth': 160.86021018,\n", - " 'view:sun_elevation': 23.81656674}" - ] - }, - "execution_count": 36, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "view_ext = ViewExtension.ext(item, add_if_missing=True)\n", - "view_info = get_view_info(metadata)\n", - "view_ext.sun_azimuth = view_info[\"sun_azimuth\"]\n", - "view_ext.sun_elevation = view_info[\"sun_elevation\"]\n", - "item.properties" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now that we've added all the metadata to the Item, let's check the validator to make sure we've specified everything correctly. The validation logic will take into account the new extensions that have been enabled and validate against the proper schemas for those extensions." - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json',\n", - " 'https://stac-extensions.github.io/eo/v1.1.0/schema.json',\n", - " 'https://stac-extensions.github.io/raster/v1.1.0/schema.json',\n", - " 'https://stac-extensions.github.io/classification/v1.1.0/schema.json',\n", - " 'https://stac-extensions.github.io/projection/v1.1.0/schema.json',\n", - " 'https://stac-extensions.github.io/view/v1.0.0/schema.json']" - ] - }, - "execution_count": 37, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.validate()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Building the Collection\n", - "\n", - "Now that we know how to build an Item for a scene, let's build the Collection that will contain all the Items.\n", - "\n", - "If we look at the `__init__` method for `pystac.Collection`, we can see what properties are required:" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Help on function __init__ in module pystac.collection:\n", - "\n", - "__init__(self, id: 'str', description: 'str', extent: 'Extent', title: 'Optional[str]' = None, stac_extensions: 'Optional[List[str]]' = None, href: 'Optional[str]' = None, extra_fields: 'Optional[Dict[str, Any]]' = None, catalog_type: 'Optional[CatalogType]' = None, license: 'str' = 'proprietary', keywords: 'Optional[List[str]]' = None, providers: 'Optional[List[Provider]]' = None, summaries: 'Optional[Summaries]' = None, assets: 'Optional[Dict[str, Asset]]' = None)\n", - " Initialize self. See help(type(self)) for accurate signature.\n", - "\n" - ] - } - ], - "source": [ - "help(pystac.Collection.__init__)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Collection `id`\n", - "\n", - "We'll use the location name we defined above in the ID for our Collection:" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'philly-landsat-collection-2'" - ] - }, - "execution_count": 39, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "collection_id = \"{}-landsat-collection-2\".format(location_name.lower())\n", - "collection_id" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Collection `title`\n", - "\n", - "Here we set a simple title for our collection." - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'2022 Landsat images over philly'" - ] - }, - "execution_count": 40, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "collection_title = \"2022 Landsat images over {}\".format(location_name.lower())\n", - "collection_title" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Collection `description`\n", - "\n", - "Here we give a brief description of the Collection. If this were a real Collection that were being published, I'd recommend putting a much more detailed description to ensure anyone using your STAC knows what they are working with!\n", - "\n", - "Notice we are using [Markdown](https://www.markdownguide.org/) to write the description. The `description` field can be Markdown to help tools that render information about STAC to display the information in a more readable way." - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "### Philly Landsat Collection 2\n", - "\n", - "A collection of Landsat scenes around Philly in 2022.\n", - "\n" - ] - } - ], - "source": [ - "collection_description = \"\"\"### {} Landsat Collection 2\n", - "\n", - "A collection of Landsat scenes around {} in 2022.\n", - "\"\"\".format(location_name, location_name)\n", - "print(collection_description)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Collection `extent`\n", - "\n", - "A Collection specifies the spatial and temporal extent of all the item it contains. Since Landsat spans the globe, we'll simply put a global extent here. We'll also specify an open-ended time interval.\n", - "\n", - "Towards the end of the notebook, we'll use a method to easily scope this down to cover the times and space the Items occupy once we've added all the items." - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "metadata": {}, - "outputs": [], - "source": [ - "spatial_extent = pystac.SpatialExtent([[-180.0, -90.0, 180.0, 90.0]])\n", - "temporal_extent = pystac.TemporalExtent([[datetime(2013, 6, 1), None]])\n", - "collection_extent = pystac.Extent(spatial_extent, temporal_extent)" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "metadata": {}, - "outputs": [], - "source": [ - "collection = pystac.Collection(\n", - " id=collection_id,\n", - " title=collection_title,\n", - " description=collection_description,\n", - " extent=collection_extent,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can now look at our Collection as a `dict` to check our values." - ] - }, - { - "cell_type": "code", - "execution_count": 44, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'type': 'Collection',\n", - " 'id': 'philly-landsat-collection-2',\n", - " 'stac_version': '1.0.0',\n", - " 'description': '### Philly Landsat Collection 2\\n\\nA collection of Landsat scenes around Philly in 2022.\\n',\n", - " 'links': [],\n", - " 'title': '2022 Landsat images over philly',\n", - " 'extent': {'spatial': {'bbox': [[-180.0, -90.0, 180.0, 90.0]]},\n", - " 'temporal': {'interval': [['2013-06-01T00:00:00Z', None]]}},\n", - " 'license': 'proprietary'}" - ] - }, - "execution_count": 44, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "collection.to_dict()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Set the license\n", - "\n", - "Notice the `license` above is `proprietary`. This is the default in PySTAC if no license is specified; however Landsat is certainly not proprietary (thankfully!), so let's change the license to the correct [SPDX](https://spdx.org/licenses/) string for public domain data:" - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "metadata": {}, - "outputs": [], - "source": [ - "collection_license = \"PDDL-1.0\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Set the providers\n", - "\n", - "A collection will specify the providers of the data, including what role they have played. We can set our provider information by instantiating `pystac.Provider` objects:" - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "metadata": {}, - "outputs": [], - "source": [ - "collection.providers = [\n", - " pystac.Provider(\n", - " name=\"NASA\",\n", - " roles=[pystac.ProviderRole.PRODUCER, pystac.ProviderRole.LICENSOR],\n", - " url=\"https://landsat.gsfc.nasa.gov/\",\n", - " ),\n", - " pystac.Provider(\n", - " name=\"USGS\",\n", - " roles=[\n", - " pystac.ProviderRole.PROCESSOR,\n", - " pystac.ProviderRole.PRODUCER,\n", - " pystac.ProviderRole.LICENSOR,\n", - " ],\n", - " url=\"https://www.usgs.gov/landsat-missions/landsat-collection-2-level-2-science-products\",\n", - " ),\n", - " pystac.Provider(\n", - " name=\"Microsoft\",\n", - " roles=[pystac.ProviderRole.HOST],\n", - " url=\"https://planetarycomputer.microsoft.com\",\n", - " ),\n", - "]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Create items for each scene\n", - "\n", - "We created an Item for a single scene above. This method consolidates that logic into a single method that can construct an Item from a scene, so we can create an Item for every scene in our subset:" - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "metadata": {}, - "outputs": [], - "source": [ - "def item_from_metadata(mtl_xml_url: str) -> pystac.Item:\n", - " metadata = get_metadata(mtl_xml_url)\n", - " download_url = partial(download_sidecar, metadata)\n", - "\n", - " bbox = get_bbox(metadata)\n", - " item = pystac.Item(\n", - " id=get_item_id(metadata),\n", - " datetime=get_datetime(metadata),\n", - " geometry=get_geometry(metadata, bbox),\n", - " bbox=bbox,\n", - " properties={},\n", - " )\n", - "\n", - " item.common_metadata.gsd = 30.0\n", - "\n", - " item_eo_ext = EOExtension.ext(item, add_if_missing=True)\n", - " item_eo_ext.cloud_cover = get_cloud_cover(metadata)\n", - "\n", - " add_assets(item, landsat_band_info(metadata, download_url))\n", - "\n", - " item_proj_ext = ProjectionExtension.ext(item, add_if_missing=True)\n", - " assert item.bbox is not None\n", - " item_proj_ext.epsg = get_epsg(metadata, item.bbox[1], item.bbox[3])\n", - "\n", - " item_view_ext = ViewExtension.ext(item, add_if_missing=True)\n", - " view_info = get_view_info(metadata)\n", - " item_view_ext.sun_azimuth = view_info[\"sun_azimuth\"]\n", - " item_view_ext.sun_elevation = view_info[\"sun_elevation\"]\n", - "\n", - " item.validate()\n", - "\n", - " return item" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here we create an item per scene and add it to our collection. Since this is reading multiple metadata files per scene from the internet, it may take a little bit to run:" - ] - }, - { - "cell_type": "code", - "execution_count": 48, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ANG file for url https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC09_L2SP_014032_20221024_20221026_02_T2/LC09_L2SP_014032_20221024_20221026_02_T2_ANG.txt is incorrectly formatted\n" - ] - } - ], - "source": [ - "for url in scene_mtls:\n", - " try:\n", - " item = item_from_metadata(url)\n", - " collection.add_item(item)\n", - " except Exception as e:\n", - " print(e)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Reset collection extent based on items\n", - "\n", - "Now that we've added all the item we can use the `update_extent_from_items` method on the Collection to set the extent based on the contained items:" - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'spatial': {'bbox': [[-76.29048, 39.25502, -73.48833, 41.38441]]},\n", - " 'temporal': {'interval': [['2022-10-08T15:40:14.577173Z',\n", - " '2022-12-19T15:40:17.729916Z']]}}" - ] - }, - "execution_count": 49, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "collection.update_extent_from_items()\n", - "collection.extent.to_dict()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Set the HREFs for everything in the catalog\n", - "\n", - "We've been building up our Collection and Items in memory. This has been convenient as it allows us not to think about file paths as we construct our Catalog. However, a STAC is not valid without any HREFs! \n", - "\n", - "We can use the `normalize_hrefs` method to set all the HREFs in the entire STAC based on a root directory. This will use the [STAC Best Practices](https://github.com/radiantearth/stac-spec/blob/master/best-practices.md#catalog-layout) recommendations for STAC file layout for each Catalog, Collection and Item in the STAC.\n", - "\n", - "Here we use that method and set the root directory to a subdirectory of our user's `home` directory:" - ] - }, - { - "cell_type": "code", - "execution_count": 50, - "metadata": {}, - "outputs": [], - "source": [ - "from pathlib import Path\n", - "\n", - "root_path = str(Path.home() / \"{}-landsat-stac\".format(location_name))\n", - "\n", - "collection.normalize_hrefs(root_path)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now that we have all the Collection's data set and HREFs in place we can validate the entire STAC using `validate_all`, which recursively crawls through a catalog and validates every STAC object in the catalog:" - ] - }, - { - "cell_type": "code", - "execution_count": 51, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "8" - ] - }, - "execution_count": 51, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "collection.validate_all()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Write the catalog locally\n", - "\n", - "Now that we have our complete, validated STAC in memory, let's write it out. This is as simple as calling `save` on the Collection. We need to specify the type of catalog in order to property write out links - these types are described again in the STAC [Best Practices](https://github.com/radiantearth/stac-spec/blob/master/best-practices.md#use-of-links) documentation.\n", - "\n", - "We'll use the \"self contained\" type, which uses relative paths and does not specify absolute \"self\" links to any object. This makes the catalog more portable, as it remains valid even if you copy the STAC to new locations." - ] - }, - { - "cell_type": "code", - "execution_count": 52, - "metadata": {}, - "outputs": [], - "source": [ - "collection.save(pystac.CatalogType.SELF_CONTAINED)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now that we've written our STAC out we probably want to view it. We can use the `describe` method to print out a simple representation of the catalog:" - ] - }, - { - "cell_type": "code", - "execution_count": 53, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "* \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n" - ] - } - ], - "source": [ - "collection.describe()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can also use the `to_dict` method on individual STAC objects in order to see the data, as we've been doing in the tutorial:" - ] - }, - { - "cell_type": "code", - "execution_count": 54, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'type': 'Collection',\n", - " 'id': 'philly-landsat-collection-2',\n", - " 'stac_version': '1.0.0',\n", - " 'description': '### Philly Landsat Collection 2\\n\\nA collection of Landsat scenes around Philly in 2022.\\n',\n", - " 'links': [{'rel': 'root',\n", - " 'href': './collection.json',\n", - " 'type': 'application/json',\n", - " 'title': '2022 Landsat images over philly'},\n", - " {'rel': 'item',\n", - " 'href': './LC80140322022353/LC80140322022353.json',\n", - " 'type': 'application/json'},\n", - " {'rel': 'item',\n", - " 'href': './LC90140322022345/LC90140322022345.json',\n", - " 'type': 'application/json'},\n", - " {'rel': 'item',\n", - " 'href': './LC80140322022337/LC80140322022337.json',\n", - " 'type': 'application/json'},\n", - " {'rel': 'item',\n", - " 'href': './LC90140322022329/LC90140322022329.json',\n", - " 'type': 'application/json'},\n", - " {'rel': 'item',\n", - " 'href': './LC80140322022321/LC80140322022321.json',\n", - " 'type': 'application/json'},\n", - " {'rel': 'item',\n", - " 'href': './LC90140322022313/LC90140322022313.json',\n", - " 'type': 'application/json'},\n", - " {'rel': 'item',\n", - " 'href': './LC80140322022305/LC80140322022305.json',\n", - " 'type': 'application/json'},\n", - " {'rel': 'item',\n", - " 'href': './LC90140322022281/LC90140322022281.json',\n", - " 'type': 'application/json'},\n", - " {'rel': 'self',\n", - " 'href': '/Users/pjh/Philly-landsat-stac/collection.json',\n", - " 'type': 'application/json'}],\n", - " 'title': '2022 Landsat images over philly',\n", - " 'extent': {'spatial': {'bbox': [[-76.29048, 39.25502, -73.48833, 41.38441]]},\n", - " 'temporal': {'interval': [['2022-10-08T15:40:14.577173Z',\n", - " '2022-12-19T15:40:17.729916Z']]}},\n", - " 'license': 'proprietary',\n", - " 'providers': [{'name': 'NASA',\n", - " 'roles': [,\n", - " ],\n", - " 'url': 'https://landsat.gsfc.nasa.gov/'},\n", - " {'name': 'USGS',\n", - " 'roles': [,\n", - " ,\n", - " ],\n", - " 'url': 'https://www.usgs.gov/landsat-missions/landsat-collection-2-level-2-science-products'},\n", - " {'name': 'Microsoft',\n", - " 'roles': [],\n", - " 'url': 'https://planetarycomputer.microsoft.com'}]}" - ] - }, - "execution_count": 54, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "collection.to_dict()" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "However, if we want to browse our STAC more interactively, we can serve the Collection from a local webserver and then browse the Collection with [stac-browser](https://github.com/radiantearth/stac-browser).\n", - "\n", - "We can use this simple Python server (copied from [this gist](https://gist.github.com/acdha/925e9ffc3d74ad59c3ea)) to serve our our directory at port 5555:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "from http.server import HTTPServer, SimpleHTTPRequestHandler\n", - "\n", - "os.chdir(root_path)\n", - "\n", - "\n", - "class CORSRequestHandler(SimpleHTTPRequestHandler):\n", - " def end_headers(self) -> None:\n", - " self.send_header(\"Access-Control-Allow-Origin\", \"*\")\n", - " self.send_header(\"Access-Control-Allow-Methods\", \"GET\")\n", - " self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate\")\n", - " return super(CORSRequestHandler, self).end_headers()\n", - "\n", - "\n", - "with HTTPServer((\"localhost\", 5555), CORSRequestHandler) as httpd:\n", - " httpd.serve_forever()" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we can browse our STAC Collection with stac-browser in a few different ways:\n", - "1. Follow the [instructions](https://github.com/radiantearth/stac-browser/blob/main/local_files.md) for starting a stac-browser instance and point it at `http://localhost:5555/collection.json` to serve out the STAC.\n", - "2. If you want to avoid setting up your own stac-browser instance, you can use the [STAC Browser Demo](https://radiantearth.github.io/stac-browser/) hosted by Radiant Earth: [https://radiantearth.github.io/stac-browser/#/http://localhost:5555/collection.json](https://radiantearth.github.io/stac-browser/#/http://localhost:5555/collection.json)\n", - "\n", - "To quit the server, use the `Kernel` -> `Interrupt` menu option." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Acknowledgements\n", - "\n", - "Credit to [sat-stac-landsat](https://github.com/sat-utils/sat-stac-landsat) from which a lot of this code was based." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "pystac", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.6" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/tutorials/how-to-create-stac-catalogs.ipynb b/docs/tutorials/how-to-create-stac-catalogs.ipynb deleted file mode 100644 index 18fb87a74..000000000 --- a/docs/tutorials/how-to-create-stac-catalogs.ipynb +++ /dev/null @@ -1,3609 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# How to create STAC Catalogs \n", - "## STAC Community Sprint, Arlington, November 7th 2019" - ] - }, - { - "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, updated to work with current PySTAC." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This tutorial will require the `boto3`, `rasterio`, and `shapely` libraries:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Note: you may need to restart the kernel to use updated packages.\n" - ] - } - ], - "source": [ - "%pip install boto3 rasterio shapely pystac --quiet" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can import pystac and access most of the functionality we need with the single import:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import pystac" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Creating a catalog from a local file" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To give us some material to work with, lets download a single image from the [Spacenet 5 challenge](https://www.topcoder.com/challenges/30099956). We'll use a temporary directory to save off our single-item STAC." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import urllib.request\n", - "from tempfile import TemporaryDirectory\n", - "\n", - "tmp_dir = TemporaryDirectory()\n", - "img_path = os.path.join(tmp_dir.name, \"image.tif\")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "('/tmp/tmpdsdpun_y/image.tif', )" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "url = (\n", - " \"https://spacenet-dataset.s3.amazonaws.com/\"\n", - " \"spacenet/SN5_roads/train/AOI_7_Moscow/MS/\"\n", - " \"SN5_roads_train_AOI_7_Moscow_MS_chip996.tif\"\n", - ")\n", - "urllib.request.urlretrieve(url, img_path)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We want to create a Catalog. Let's check the docs for `Catalog` to see what information we'll need." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[0;31mInit signature:\u001b[0m\n", - "\u001b[0mpystac\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mCatalog\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mid\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'str'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mdescription\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'str'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mtitle\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[str]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mstac_extensions\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[List[str]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mextra_fields\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Dict[str, Any]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mhref\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[str]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mcatalog_type\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'CatalogType'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mABSOLUTE_PUBLISHED\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m \n", - "A PySTAC Catalog represents a STAC catalog in memory.\n", - "\n", - "A Catalog is a :class:`~pystac.STACObject` that may contain children,\n", - "which are instances of :class:`~pystac.Catalog` or :class:`~pystac.Collection`,\n", - "as well as :class:`~pystac.Item` s.\n", - "\n", - "Args:\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.29 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", - "\u001b[0;31mFile:\u001b[0m ~/pystac/pystac/catalog.py\n", - "\u001b[0;31mType:\u001b[0m ABCMeta\n", - "\u001b[0;31mSubclasses:\u001b[0m Collection" - ] - } - ], - "source": [ - "?pystac.Catalog" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's just give an ID and a description. We don't have to worry about the HREF right now; that will be set later." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "catalog = pystac.Catalog(id=\"test-catalog\", description=\"Tutorial catalog.\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There are no children or items in the catalog, since we haven't added anything yet." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[]\n", - "[]\n" - ] - } - ], - "source": [ - "print(list(catalog.get_children()))\n", - "print(list(catalog.get_items()))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We'll now create an Item to represent the image. Check the pydocs to see what you need to supply:" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[0;31mInit signature:\u001b[0m\n", - "\u001b[0mpystac\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mItem\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mid\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'str'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mgeometry\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Dict[str, Any]]'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mbbox\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[List[float]]'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mdatetime\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Datetime]'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mproperties\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Dict[str, Any]'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mstart_datetime\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Datetime]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mend_datetime\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Datetime]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mstac_extensions\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[List[str]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mhref\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[str]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mcollection\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Union[str, Collection]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mextra_fields\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Dict[str, Any]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0massets\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Dict[str, Asset]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m \n", - "An Item is the core granular entity in a STAC, containing the core metadata\n", - "that enables any client to search or crawl online catalogs of spatial 'assets' -\n", - "satellite imagery, derived data, DEM's, etc.\n", - "\n", - "Args:\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.\n", - " properties : A dictionary of additional metadata for the item.\n", - " start_datetime : Optional start datetime, part of common metadata. This value\n", - " will override any `start_datetime` key in properties.\n", - " end_datetime : Optional end datetime, part of common metadata. This value\n", - " will override any `end_datetime` key in properties.\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 : The Collection or Collection ID that this item\n", - " belongs to.\n", - " extra_fields : Extra fields that are part of the top-level JSON\n", - " properties of the Item.\n", - " assets : A dictionary mapping string keys to :class:`~pystac.Asset` objects. All\n", - " :class:`~pystac.Asset` values in the dictionary will have their\n", - " :attr:`~pystac.Asset.owner` attribute set to the created Item.\n", - "\u001b[0;31mFile:\u001b[0m ~/pystac/pystac/item.py\n", - "\u001b[0;31mType:\u001b[0m ABCMeta\n", - "\u001b[0;31mSubclasses:\u001b[0m " - ] - } - ], - "source": [ - "?pystac.Item" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Using [rasterio](https://rasterio.readthedocs.io/en/stable/), we can pull out the bounding box of the image to use for the image metadata. If the image contained a NoData border, we would ideally pull out the footprint and save it as the geometry; in this case, we're working with a small chip that most likely has no NoData values." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "import rasterio\n", - "from shapely.geometry import Polygon, mapping\n", - "\n", - "\n", - "def get_bbox_and_footprint(raster_uri):\n", - " with rasterio.open(raster_uri) as ds:\n", - " bounds = ds.bounds\n", - " bbox = [bounds.left, bounds.bottom, bounds.right, bounds.top]\n", - " footprint = Polygon(\n", - " [\n", - " [bounds.left, bounds.bottom],\n", - " [bounds.left, bounds.top],\n", - " [bounds.right, bounds.top],\n", - " [bounds.right, bounds.bottom],\n", - " ]\n", - " )\n", - "\n", - " return (bbox, mapping(footprint))" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[37.6616853489879, 55.73478197572927, 37.66573047610874, 55.73882710285011]\n", - "{'type': 'Polygon', 'coordinates': (((37.6616853489879, 55.73478197572927), (37.6616853489879, 55.73882710285011), (37.66573047610874, 55.73882710285011), (37.66573047610874, 55.73478197572927), (37.6616853489879, 55.73478197572927)),)}\n" - ] - } - ], - "source": [ - "bbox, footprint = get_bbox_and_footprint(img_path)\n", - "print(bbox)\n", - "print(footprint)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We're also using `datetime.utcnow()` to supply the required datetime property for our Item. Since this is a required property, you might often find yourself making up a time to fill in if you don't know the exact capture time." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "from datetime import datetime\n", - "\n", - "item = pystac.Item(\n", - " id=\"local-image\",\n", - " geometry=footprint,\n", - " bbox=bbox,\n", - " datetime=datetime.utcnow(),\n", - " properties={},\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We haven't added it to a catalog yet, so it's parent isn't set. Once we add it to the catalog, we can see it correctly links to it's parent." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "assert item.get_parent() is None" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "
\n", - "
\n", - "
    \n", - " \n", - " \n", - " \n", - "
  • \n", - " rel\n", - " \"item\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " href\n", - " None\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " type\n", - " \"application/json\"\n", - "
  • \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - "
" - ], - "text/plain": [ - ">" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "catalog.add_item(item)" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "
\n", - "
\n", - "
    \n", - " \n", - " \n", - " \n", - "
  • \n", - " type\n", - " \"Catalog\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " id\n", - " \"test-catalog\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " stac_version\n", - " \"1.0.0\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " description\n", - " \"Tutorial catalog.\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " links\n", - " [] 1 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"item\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " None\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"application/json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - "
\n", - "
\n", - "
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.get_parent()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`describe()` is a useful method on `Catalog` - but be careful when using it on large catalogs, as it will walk the entire tree of the STAC." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "* \n", - " * \n" - ] - } - ], - "source": [ - "catalog.describe()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Adding Assets\n", - "\n", - "We've created an Item, but there aren't any assets associated with it. Let's create one:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[0;31mInit signature:\u001b[0m\n", - "\u001b[0mpystac\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mAsset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mhref\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'str'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mtitle\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[str]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mdescription\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[str]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mmedia_type\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[str]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mroles\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[List[str]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mextra_fields\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Dict[str, Any]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0;34m'None'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m \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 : 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", - " 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", - "\u001b[0;31mFile:\u001b[0m ~/pystac/pystac/asset.py\n", - "\u001b[0;31mType:\u001b[0m type\n", - "\u001b[0;31mSubclasses:\u001b[0m " - ] - } - ], - "source": [ - "?pystac.Asset" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [], - "source": [ - "item.add_asset(\n", - " key=\"image\", asset=pystac.Asset(href=img_path, media_type=pystac.MediaType.GEOTIFF)\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "At any time we can call `to_dict()` on STAC objects to see how the STAC JSON is shaping up. Notice the asset is now set:" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"type\": \"Feature\",\n", - " \"stac_version\": \"1.0.0\",\n", - " \"id\": \"local-image\",\n", - " \"properties\": {\n", - " \"datetime\": \"2023-10-12T15:35:17.290343Z\"\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\": null,\n", - " \"type\": \"application/json\"\n", - " },\n", - " {\n", - " \"rel\": \"parent\",\n", - " \"href\": null,\n", - " \"type\": \"application/json\"\n", - " }\n", - " ],\n", - " \"assets\": {\n", - " \"image\": {\n", - " \"href\": \"/tmp/tmpdsdpun_y/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": [ - "import json\n", - "\n", - "print(json.dumps(item.to_dict(), indent=4))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that the link `href` properties are `null`. This is OK, as we're working with the STAC in memory. Next, we'll talk about writing the catalog out, and how to set those HREFs." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Saving the catalog" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As the JSON above indicates, there's no HREFs set on these in-memory items. PySTAC uses the `self` link on STAC objects to track where the file lives. Because we haven't set them, they evaluate to `None`:" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "True\n", - "True\n" - ] - } - ], - "source": [ - "print(catalog.get_self_href() is None)\n", - "print(item.get_self_href() is None)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In order to set them, we can use `normalize_hrefs`. This method will create a normalized set of HREFs for each STAC object in the catalog, according to the [best practices document](https://github.com/radiantearth/stac-spec/blob/v0.8.1/best-practices.md#catalog-layout)'s recommendations on how to lay out a catalog." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [], - "source": [ - "catalog.normalize_hrefs(os.path.join(tmp_dir.name, \"stac\"))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now that we've normalized to a root directory (the temporary directory), we see that the `self` links are set:" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "/tmp/tmpdsdpun_y/stac/catalog.json\n", - "/tmp/tmpdsdpun_y/stac/local-image/local-image.json\n" - ] - } - ], - "source": [ - "print(catalog.get_self_href())\n", - "print(item.get_self_href())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can now call `save` on the catalog, which will recursively save all the STAC objects to their respective self HREFs.\n", - "\n", - "Save requires a `CatalogType` to be set. You can review the [API docs](https://pystac.readthedocs.io/en/stable/api.html#catalogtype) on `CatalogType` to see what each type means (unfortunately `help` doesn't show docstrings for attributes)." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [], - "source": [ - "catalog.save(catalog_type=pystac.CatalogType.SELF_CONTAINED)" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "/tmp/tmpdsdpun_y/stac/catalog.json\n", - "\n", - "/tmp/tmpdsdpun_y/stac/local-image:\n", - "local-image.json\n" - ] - } - ], - "source": [ - "!ls {tmp_dir.name}/stac/*" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\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", - "}\n" - ] - } - ], - "source": [ - "with open(catalog.self_href) as f:\n", - " print(f.read())" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"type\": \"Feature\",\n", - " \"stac_version\": \"1.0.0\",\n", - " \"id\": \"local-image\",\n", - " \"properties\": {\n", - " \"datetime\": \"2023-10-12T15:35:17.290343Z\"\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\": \"../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\": \"/tmp/tmpdsdpun_y/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.self_href) as f:\n", - " print(f.read())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As you can see, all links are saved with relative paths. That's because we used `catalog_type=CatalogType.SELF_CONTAINED`. If we save an Absolute Published catalog, we'll see absolute paths:" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [], - "source": [ - "catalog.save(catalog_type=pystac.CatalogType.ABSOLUTE_PUBLISHED)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now the links included in the STAC item are all absolute:" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"type\": \"Feature\",\n", - " \"stac_version\": \"1.0.0\",\n", - " \"id\": \"local-image\",\n", - " \"properties\": {\n", - " \"datetime\": \"2023-10-12T15:35:17.290343Z\"\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\": \"/tmp/tmpdsdpun_y/stac/catalog.json\",\n", - " \"type\": \"application/json\"\n", - " },\n", - " {\n", - " \"rel\": \"parent\",\n", - " \"href\": \"/tmp/tmpdsdpun_y/stac/catalog.json\",\n", - " \"type\": \"application/json\"\n", - " },\n", - " {\n", - " \"rel\": \"self\",\n", - " \"href\": \"/tmp/tmpdsdpun_y/stac/local-image/local-image.json\",\n", - " \"type\": \"application/json\"\n", - " }\n", - " ],\n", - " \"assets\": {\n", - " \"image\": {\n", - " \"href\": \"/tmp/tmpdsdpun_y/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", - " print(f.read())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Notice that the Asset HREF is absolute in both cases. We can make the Asset HREF relative to the STAC Item by using `.make_all_asset_hrefs_relative()`:" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [], - "source": [ - "catalog.make_all_asset_hrefs_relative()\n", - "catalog.save(catalog_type=pystac.CatalogType.SELF_CONTAINED)" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"type\": \"Feature\",\n", - " \"stac_version\": \"1.0.0\",\n", - " \"id\": \"local-image\",\n", - " \"properties\": {\n", - " \"datetime\": \"2023-10-12T15:35:17.290343Z\"\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\": \"../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" - ] - } - ], - "source": [ - "with open(item.get_self_href()) as f:\n", - " print(f.read())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Creating an Item that implements the EO extension\n", - "\n", - "In the code above our item only implemented the core STAC Item specification. With [extensions](https://github.com/radiantearth/stac-spec/tree/v0.9.0/extensions) we can record more information and add additional functionality to the Item. Given that we know this is a World View 3 image that has earth observation data, we can enable the [eo extension](https://github.com/radiantearth/stac-spec/tree/v0.8.1/extensions/eo) to add band information." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To add eo information to an item we'll need to specify some more data. First, let's define the bands of World View 3:" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": {}, - "outputs": [], - "source": [ - "from pystac.extensions.eo import Band\n", - "\n", - "# From: https://www.spaceimagingme.com/downloads/sensors/datasheets/DG_WorldView3_DS_2014.pdf\n", - "\n", - "wv3_bands = [\n", - " Band.create(\n", - " name=\"Coastal\", description=\"Coastal: 400 - 450 nm\", common_name=\"coastal\"\n", - " ),\n", - " Band.create(name=\"Blue\", description=\"Blue: 450 - 510 nm\", common_name=\"blue\"),\n", - " Band.create(name=\"Green\", description=\"Green: 510 - 580 nm\", common_name=\"green\"),\n", - " Band.create(\n", - " name=\"Yellow\", description=\"Yellow: 585 - 625 nm\", common_name=\"yellow\"\n", - " ),\n", - " Band.create(name=\"Red\", description=\"Red: 630 - 690 nm\", common_name=\"red\"),\n", - " Band.create(\n", - " name=\"Red Edge\", description=\"Red Edge: 705 - 745 nm\", common_name=\"rededge\"\n", - " ),\n", - " Band.create(\n", - " name=\"Near-IR1\", description=\"Near-IR1: 770 - 895 nm\", common_name=\"nir08\"\n", - " ),\n", - " Band.create(\n", - " name=\"Near-IR2\", description=\"Near-IR2: 860 - 1040 nm\", common_name=\"nir09\"\n", - " ),\n", - "]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Notice that we used the `.create` method create new band information." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can now create an Item, enable the eo extension, add the band information and add it to our catalog:" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "metadata": {}, - "outputs": [], - "source": [ - "eo_item = pystac.Item(\n", - " id=\"local-image-eo\",\n", - " geometry=footprint,\n", - " bbox=bbox,\n", - " datetime=datetime.utcnow(),\n", - " properties={},\n", - ")\n", - "eo_item.ext.add(\"eo\")\n", - "eo_item.ext.eo.bands = wv3_bands" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There are also [common metadata](https://github.com/radiantearth/stac-spec/blob/v0.9.0/item-spec/common-metadata.md) fields that we can use to capture additional information about the WorldView 3 imagery:" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": {}, - "outputs": [], - "source": [ - "eo_item.common_metadata.platform = \"Maxar\"\n", - "eo_item.common_metadata.instruments = [\"WorldView3\"]\n", - "eo_item.common_metadata.gsd = 0.3" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "
\n", - "
\n", - "
    \n", - " \n", - " \n", - " \n", - "
  • \n", - " type\n", - " \"Feature\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " stac_version\n", - " \"1.0.0\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " id\n", - " \"local-image-eo\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " properties\n", - "
      \n", - " \n", - " \n", - "
    • \n", - " \n", - " eo:bands\n", - " [] 8 items\n", - " \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 0\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"Coastal\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"coastal\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " description\n", - " \"Coastal: 400 - 450 nm\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 1\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"Blue\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"blue\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " description\n", - " \"Blue: 450 - 510 nm\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 2\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"Green\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"green\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " description\n", - " \"Green: 510 - 580 nm\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 3\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"Yellow\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"yellow\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " description\n", - " \"Yellow: 585 - 625 nm\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 4\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"Red\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"red\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " description\n", - " \"Red: 630 - 690 nm\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 5\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"Red Edge\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"rededge\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " description\n", - " \"Red Edge: 705 - 745 nm\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 6\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"Near-IR1\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"nir08\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " description\n", - " \"Near-IR1: 770 - 895 nm\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 7\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"Near-IR2\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"nir09\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " description\n", - " \"Near-IR2: 860 - 1040 nm\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " platform\n", - " \"Maxar\"\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " \n", - " instruments\n", - " [] 1 items\n", - " \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 0\n", - " \"WorldView3\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " gsd\n", - " 0.3\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " datetime\n", - " \"2023-10-12T15:35:17.781985Z\"\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " geometry\n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " type\n", - " \"Polygon\"\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " \n", - " coordinates\n", - " [] 1 items\n", - " \n", - " \n", - "
        \n", - " \n", - " \n", - "
      • \n", - " \n", - " 0\n", - " [] 5 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - "
        • \n", - " \n", - " 0\n", - " [] 2 items\n", - " \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 0\n", - " 37.6616853489879\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 1\n", - " 55.73478197572927\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
        • \n", - " \n", - " \n", - "
        \n", - " \n", - "
          \n", - " \n", - " \n", - "
        • \n", - " \n", - " 1\n", - " [] 2 items\n", - " \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 0\n", - " 37.6616853489879\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 1\n", - " 55.73882710285011\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
        • \n", - " \n", - " \n", - "
        \n", - " \n", - "
          \n", - " \n", - " \n", - "
        • \n", - " \n", - " 2\n", - " [] 2 items\n", - " \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 0\n", - " 37.66573047610874\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 1\n", - " 55.73882710285011\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
        • \n", - " \n", - " \n", - "
        \n", - " \n", - "
          \n", - " \n", - " \n", - "
        • \n", - " \n", - " 3\n", - " [] 2 items\n", - " \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 0\n", - " 37.66573047610874\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 1\n", - " 55.73478197572927\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
        • \n", - " \n", - " \n", - "
        \n", - " \n", - "
          \n", - " \n", - " \n", - "
        • \n", - " \n", - " 4\n", - " [] 2 items\n", - " \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 0\n", - " 37.6616853489879\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 1\n", - " 55.73478197572927\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
        • \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - " \n", - "
    • \n", - " \n", - " \n", - "
    \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " links\n", - " [] 0 items\n", - " \n", - " \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " assets\n", - "
      \n", - " \n", - "
    \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " bbox\n", - " [] 4 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - " 37.6616853489879\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 1\n", - " 55.73478197572927\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 2\n", - " 37.66573047610874\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 3\n", - " 55.73882710285011\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " stac_extensions\n", - " [] 1 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - " \"https://stac-extensions.github.io/eo/v1.1.0/schema.json\"\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - "
\n", - "
\n", - "
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "eo_item" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can use the eo extension to add bands to the assets we add to the item:" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "metadata": {}, - "outputs": [], - "source": [ - "asset = pystac.Asset(href=img_path, media_type=pystac.MediaType.GEOTIFF)\n", - "eo_item.add_asset(\"image\", asset)\n", - "asset.ext.eo.bands = wv3_bands" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If we look at the asset, we can see the appropriate band indexes are set:" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "
\n", - "
\n", - "
    \n", - " \n", - " \n", - " \n", - "
  • \n", - " href\n", - " \"/tmp/tmpdsdpun_y/image.tif\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " type\n", - " \"image/tiff; application=geotiff\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " eo:bands\n", - " [] 8 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " name\n", - " \"Coastal\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " common_name\n", - " \"coastal\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " description\n", - " \"Coastal: 400 - 450 nm\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 1\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " name\n", - " \"Blue\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " common_name\n", - " \"blue\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " description\n", - " \"Blue: 450 - 510 nm\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 2\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " name\n", - " \"Green\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " common_name\n", - " \"green\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " description\n", - " \"Green: 510 - 580 nm\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 3\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " name\n", - " \"Yellow\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " common_name\n", - " \"yellow\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " description\n", - " \"Yellow: 585 - 625 nm\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 4\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " name\n", - " \"Red\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " common_name\n", - " \"red\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " description\n", - " \"Red: 630 - 690 nm\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 5\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " name\n", - " \"Red Edge\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " common_name\n", - " \"rededge\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " description\n", - " \"Red Edge: 705 - 745 nm\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 6\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " name\n", - " \"Near-IR1\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " common_name\n", - " \"nir08\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " description\n", - " \"Near-IR1: 770 - 895 nm\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 7\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " name\n", - " \"Near-IR2\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " common_name\n", - " \"nir09\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " description\n", - " \"Near-IR2: 860 - 1040 nm\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - "
\n", - "
\n", - "
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 35, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "asset" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's clear the in-memory catalog, add the EO item, and save to a new STAC:" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 36, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "catalog.clear_items()\n", - "list(catalog.get_items())" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 37, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "catalog.add_item(eo_item)\n", - "list(catalog.get_items())" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "metadata": {}, - "outputs": [], - "source": [ - "catalog.normalize_and_save(\n", - " root_href=os.path.join(tmp_dir.name, \"stac-eo\"),\n", - " catalog_type=pystac.CatalogType.SELF_CONTAINED,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, if we read the catalog from the filesystem, PySTAC recognizes that the item implements eo and so use it's functionality, e.g. getting the bands off the asset:" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": {}, - "outputs": [], - "source": [ - "catalog2 = pystac.read_file(os.path.join(tmp_dir.name, \"stac-eo\", \"catalog.json\"))" - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 40, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "assert isinstance(catalog2, pystac.Catalog)\n", - "list(catalog2.get_items())" - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "metadata": {}, - "outputs": [], - "source": [ - "item: pystac.Item = next(catalog2.get_items(recursive=True))" - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "metadata": {}, - "outputs": [], - "source": [ - "assert item.ext.has(\"eo\")" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ]" - ] - }, - "execution_count": 43, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.assets[\"image\"].ext.eo.bands" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Collections\n", - "\n", - "Collections are a subtype of Catalog that have some additional properties to make them more searchable. They also can define common properties so that items in the collection don't have to duplicate common data for each item. Let's create a collection to hold common properties between two images from the Spacenet 5 challenge.\n", - "\n", - "First we'll get another image, and it's bbox and footprint:" - ] - }, - { - "cell_type": "code", - "execution_count": 44, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "('/tmp/tmpdsdpun_y/image.tif', )" - ] - }, - "execution_count": 44, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "url2 = (\n", - " \"https://spacenet-dataset.s3.amazonaws.com/\"\n", - " \"spacenet/SN5_roads/train/AOI_7_Moscow/MS/\"\n", - " \"SN5_roads_train_AOI_7_Moscow_MS_chip997.tif\"\n", - ")\n", - "img_path2 = os.path.join(tmp_dir.name, \"image.tif\")\n", - "urllib.request.urlretrieve(url2, img_path2)" - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "metadata": {}, - "outputs": [], - "source": [ - "bbox2, footprint2 = get_bbox_and_footprint(img_path2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can take a look at the pydocs for Collection to see what information we need to supply in order to satisfy the spec." - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[0;31mInit signature:\u001b[0m\n", - "\u001b[0mpystac\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mCollection\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mid\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'str'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mdescription\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'str'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mextent\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Extent'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mtitle\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[str]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mstac_extensions\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[List[str]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mhref\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[str]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mextra_fields\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Dict[str, Any]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mcatalog_type\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[CatalogType]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mlicense\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'str'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m'proprietary'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mkeywords\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[List[str]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mproviders\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[List[Provider]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0msummaries\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Summaries]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0massets\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Dict[str, Asset]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m \n", - "A Collection extends the Catalog spec with additional metadata that helps\n", - "enable discovery.\n", - "\n", - "Args:\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.29 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 : 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 : Extra fields that are part of the top-level\n", - " JSON properties of the Collection.\n", - " assets : A dictionary mapping string keys to :class:`~pystac.Asset` objects. All\n", - " :class:`~pystac.Asset` values in the dictionary will have their\n", - " :attr:`~pystac.Asset.owner` attribute set to the created Collection.\n", - "\u001b[0;31mFile:\u001b[0m ~/pystac/pystac/collection.py\n", - "\u001b[0;31mType:\u001b[0m ABCMeta\n", - "\u001b[0;31mSubclasses:\u001b[0m " - ] - } - ], - "source": [ - "?pystac.Collection" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Beyond what a Catalog requires, a Collection requires a license, and an `Extent` that describes the range of space and time that the items it hold occupy." - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[0;31mInit signature:\u001b[0m\n", - "\u001b[0mpystac\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mExtent\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mspatial\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'SpatialExtent'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mtemporal\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'TemporalExtent'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mextra_fields\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Dict[str, Any]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m \n", - "Describes the spatiotemporal extents of a Collection.\n", - "\n", - "Args:\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", - "\u001b[0;31mFile:\u001b[0m ~/pystac/pystac/collection.py\n", - "\u001b[0;31mType:\u001b[0m type\n", - "\u001b[0;31mSubclasses:\u001b[0m " - ] - } - ], - "source": [ - "?pystac.Extent" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "An Extent is comprised of a SpatialExtent and a TemporalExtent. These hold one or more bounding boxes and time intervals, respectively, that completely cover the items contained in the collections.\n", - "\n", - "Let's start with creating two new items - these will be core Items. We can set these items to implement the `eo` extension by specifying them in the `stac_extensions`." - ] - }, - { - "cell_type": "code", - "execution_count": 48, - "metadata": {}, - "outputs": [], - "source": [ - "collection_item = pystac.Item(\n", - " id=\"local-image-col-1\",\n", - " geometry=footprint,\n", - " bbox=bbox,\n", - " datetime=datetime.utcnow(),\n", - " properties={},\n", - ")\n", - "\n", - "collection_item.common_metadata.gsd = 0.3\n", - "collection_item.common_metadata.platform = \"Maxar\"\n", - "collection_item.common_metadata.instruments = [\"WorldView3\"]\n", - "\n", - "asset = pystac.Asset(href=img_path, media_type=pystac.MediaType.GEOTIFF)\n", - "collection_item.add_asset(\"image\", asset)\n", - "asset.ext.add(\"eo\")\n", - "asset.ext.eo.bands = wv3_bands\n", - "\n", - "collection_item2 = pystac.Item(\n", - " id=\"local-image-col-2\",\n", - " geometry=footprint2,\n", - " bbox=bbox2,\n", - " datetime=datetime.utcnow(),\n", - " properties={},\n", - ")\n", - "\n", - "collection_item2.common_metadata.gsd = 0.3\n", - "collection_item2.common_metadata.platform = \"Maxar\"\n", - "collection_item2.common_metadata.instruments = [\"WorldView3\"]\n", - "\n", - "asset2 = pystac.Asset(href=img_path, media_type=pystac.MediaType.GEOTIFF)\n", - "collection_item2.add_asset(\"image\", asset2)\n", - "asset2.ext.add(\"eo\")\n", - "asset2.ext.eo.bands = [\n", - " band for band in wv3_bands if band.name in [\"Red\", \"Green\", \"Blue\"]\n", - "]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can use our two items' metadata to find out what the proper bounds are:" - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "metadata": {}, - "outputs": [], - "source": [ - "from shapely.geometry import shape\n", - "\n", - "unioned_footprint = shape(footprint).union(shape(footprint2))\n", - "collection_bbox = list(unioned_footprint.bounds)\n", - "spatial_extent = pystac.SpatialExtent(bboxes=[collection_bbox])" - ] - }, - { - "cell_type": "code", - "execution_count": 50, - "metadata": {}, - "outputs": [], - "source": [ - "collection_interval = sorted([collection_item.datetime, collection_item2.datetime])\n", - "temporal_extent = pystac.TemporalExtent(intervals=[collection_interval])" - ] - }, - { - "cell_type": "code", - "execution_count": 51, - "metadata": {}, - "outputs": [], - "source": [ - "collection_extent = pystac.Extent(spatial=spatial_extent, temporal=temporal_extent)" - ] - }, - { - "cell_type": "code", - "execution_count": 52, - "metadata": {}, - "outputs": [], - "source": [ - "collection = pystac.Collection(\n", - " id=\"wv3-images\",\n", - " description=\"Spacenet 5 images over Moscow\",\n", - " extent=collection_extent,\n", - " license=\"CC-BY-SA-4.0\",\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now if we add our items to our Collection, and our Collection to our Catalog, we get the following STAC that can be saved:" - ] - }, - { - "cell_type": "code", - "execution_count": 53, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[>,\n", - " >]" - ] - }, - "execution_count": 53, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "collection.add_items([collection_item, collection_item2])" - ] - }, - { - "cell_type": "code", - "execution_count": 54, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "
\n", - "
\n", - "
    \n", - " \n", - " \n", - " \n", - "
  • \n", - " rel\n", - " \"child\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " href\n", - " \"./wv3-images/collection.json\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " type\n", - " \"application/json\"\n", - "
  • \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - "
" - ], - "text/plain": [ - ">" - ] - }, - "execution_count": 54, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "catalog.clear_items()\n", - "catalog.clear_children()\n", - "catalog.add_child(collection)" - ] - }, - { - "cell_type": "code", - "execution_count": 55, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "* \n", - " * \n", - " * \n", - " * \n" - ] - } - ], - "source": [ - "catalog.describe()" - ] - }, - { - "cell_type": "code", - "execution_count": 56, - "metadata": {}, - "outputs": [], - "source": [ - "catalog.normalize_and_save(\n", - " root_href=os.path.join(tmp_dir.name, \"stac-collection\"),\n", - " catalog_type=pystac.CatalogType.SELF_CONTAINED,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Cleanup\n", - "\n", - "Don't forget to clean up the temporary directory!" - ] - }, - { - "cell_type": "code", - "execution_count": 57, - "metadata": {}, - "outputs": [], - "source": [ - "tmp_dir.cleanup()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Creating a STAC of imagery from Spacenet 5 data" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, let's take what we've learned and create a Catalog with more data in it.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "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 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": 58, - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Any, Union\n", - "from urllib.parse import urlparse\n", - "\n", - "import boto3\n", - "\n", - "from pystac import Link\n", - "from pystac.stac_io import DefaultStacIO\n", - "\n", - "\n", - "class CustomStacIO(DefaultStacIO):\n", - " def __init__(self):\n", - " self.s3 = boto3.resource(\"s3\")\n", - " super().__init__()\n", - "\n", - " def read_text(self, source: Union[str, Link], *args: Any, **kwargs: Any) -> str:\n", - " parsed = urlparse(source)\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(dest)\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)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We'll need a utility to list keys for reading the lists of files from S3:" - ] - }, - { - "cell_type": "code", - "execution_count": 59, - "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", - "\n", - "def get_s3_keys(bucket, prefix):\n", - " \"\"\"Generate all the keys in an S3 bucket.\"\"\"\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", - " for obj in resp[\"Contents\"]:\n", - " yield obj[\"Key\"]\n", - "\n", - " try:\n", - " kwargs[\"ContinuationToken\"] = resp[\"NextContinuationToken\"]\n", - " except KeyError:\n", - " break" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's make a STAC of imagery over Moscow as part of the Spacenet 5 challenge. As a first step, we can list out the imagery and extract IDs from each of the chips." - ] - }, - { - "cell_type": "code", - "execution_count": 60, - "metadata": {}, - "outputs": [], - "source": [ - "moscow_training_chip_uris = list(\n", - " get_s3_keys(\n", - " bucket=\"spacenet-dataset\", prefix=\"spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/\"\n", - " )\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 61, - "metadata": {}, - "outputs": [], - "source": [ - "import re\n", - "\n", - "chip_id_to_data = {}\n", - "\n", - "\n", - "def get_chip_id(uri):\n", - " return re.search(r\".*\\_chip(\\d+)\\.\", uri).group(1)\n", - "\n", - "\n", - "for uri in moscow_training_chip_uris:\n", - " chip_id = get_chip_id(uri)\n", - " chip_id_to_data[chip_id] = {\"img\": \"s3://spacenet-dataset/{}\".format(uri)}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For this tutorial, we'll only take a subset of the data." - ] - }, - { - "cell_type": "code", - "execution_count": 62, - "metadata": {}, - "outputs": [], - "source": [ - "chip_id_to_data = dict(list(chip_id_to_data.items())[:10])" - ] - }, - { - "cell_type": "code", - "execution_count": 63, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'0': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip0.tif'},\n", - " '1': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1.tif'},\n", - " '10': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip10.tif'},\n", - " '100': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip100.tif'},\n", - " '1000': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1000.tif'},\n", - " '1001': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1001.tif'},\n", - " '1002': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1002.tif'},\n", - " '1003': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1003.tif'},\n", - " '1004': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1004.tif'},\n", - " '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": 63, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "chip_id_to_data" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's turn each of those chips into a STAC Item that represents the image." - ] - }, - { - "cell_type": "code", - "execution_count": 64, - "metadata": {}, - "outputs": [], - "source": [ - "chip_id_to_items = {}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We'll create core `Item`s for our imagery, but mark them with the `eo` extension as we did above, and store the `eo` data in a `Collection`.\n", - "\n", - "Note that the image CRS is in WGS:84 (Lat/Lng). If it wasn't, we'd have to reproject the footprint to WGS:84 in order to be compliant with the spec (which can easily be done with [pyproj](https://github.com/pyproj4/pyproj)).\n", - "\n", - "Here we're taking advantage of `rasterio`'s ability to read S3 URIs, which only grabs the GeoTIFF metadata and does not pull the whole file down." - ] - }, - { - "cell_type": "code", - "execution_count": 65, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip0.tif\n", - "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1.tif\n", - "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip10.tif\n", - "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip100.tif\n", - "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1000.tif\n", - "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1001.tif\n", - "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1002.tif\n", - "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1003.tif\n", - "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1004.tif\n", - "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1005.tif\n" - ] - } - ], - "source": [ - "import os\n", - "\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", - " bbox, footprint = get_bbox_and_footprint(img_uri)\n", - "\n", - " item = pystac.Item(\n", - " id=\"img_{}\".format(chip_id),\n", - " geometry=footprint,\n", - " bbox=bbox,\n", - " datetime=datetime.utcnow(),\n", - " properties={},\n", - " )\n", - "\n", - " item.common_metadata.gsd = 0.3\n", - " item.common_metadata.platform = \"Maxar\"\n", - " item.common_metadata.instruments = [\"WorldView3\"]\n", - "\n", - " item.ext.add(\"eo\")\n", - " item.ext.eo.bands = wv3_bands\n", - " asset = pystac.Asset(href=img_uri, media_type=pystac.MediaType.COG)\n", - " item.add_asset(key=\"ps-ms\", asset=asset)\n", - " asset.ext.eo.bands = wv3_bands\n", - " chip_id_to_items[chip_id] = item" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Creating the Collection\n", - "\n", - "All of these images are over Moscow. In Spacenet 5, we have a couple cities that have imagery; a good way to separate these collections of imagery. We can store all of the common `eo` metadata in the collection." - ] - }, - { - "cell_type": "code", - "execution_count": 66, - "metadata": {}, - "outputs": [], - "source": [ - "from shapely.geometry import MultiPolygon, shape\n", - "\n", - "footprints = list(map(lambda i: shape(i.geometry).envelope, chip_id_to_items.values()))\n", - "collection_bbox = MultiPolygon(footprints).bounds\n", - "spatial_extent = pystac.SpatialExtent(bboxes=[collection_bbox])" - ] - }, - { - "cell_type": "code", - "execution_count": 67, - "metadata": {}, - "outputs": [], - "source": [ - "datetimes = sorted(list(map(lambda i: i.datetime, chip_id_to_items.values())))\n", - "temporal_extent = pystac.TemporalExtent(intervals=[[datetimes[0], datetimes[-1]]])" - ] - }, - { - "cell_type": "code", - "execution_count": 68, - "metadata": {}, - "outputs": [], - "source": [ - "collection_extent = pystac.Extent(spatial=spatial_extent, temporal=temporal_extent)" - ] - }, - { - "cell_type": "code", - "execution_count": 69, - "metadata": {}, - "outputs": [], - "source": [ - "collection = pystac.Collection(\n", - " id=\"wv3-images\",\n", - " description=\"Spacenet 5 images over Moscow\",\n", - " extent=collection_extent,\n", - " license=\"CC-BY-SA-4.0\",\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 70, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[>,\n", - " >,\n", - " >,\n", - " >,\n", - " >,\n", - " >,\n", - " >,\n", - " >,\n", - " >,\n", - " >]" - ] - }, - "execution_count": 70, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "collection.add_items(chip_id_to_items.values())" - ] - }, - { - "cell_type": "code", - "execution_count": 71, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "* \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n" - ] - } - ], - "source": [ - "collection.describe()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, we can create a Catalog and add the collection." - ] - }, - { - "cell_type": "code", - "execution_count": 72, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "
\n", - "
\n", - "
    \n", - " \n", - " \n", - " \n", - "
  • \n", - " rel\n", - " \"child\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " href\n", - " None\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " type\n", - " \"application/json\"\n", - "
  • \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - "
" - ], - "text/plain": [ - ">" - ] - }, - "execution_count": 72, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "catalog = pystac.Catalog(id=\"spacenet5\", description=\"Spacenet 5 Data (Test)\")\n", - "catalog.add_child(collection)" - ] - }, - { - "cell_type": "code", - "execution_count": 73, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "* \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n" - ] - } - ], - "source": [ - "catalog.describe()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "pystac", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.6" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/tutorials/how-to-read-data-from-stac.ipynb b/docs/tutorials/how-to-read-data-from-stac.ipynb deleted file mode 100644 index 8e10f3ba5..000000000 --- a/docs/tutorials/how-to-read-data-from-stac.ipynb +++ /dev/null @@ -1,1543 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "be2c57c1-798a-4eaf-b2b8-41c261b657d1", - "metadata": {}, - "source": [ - "# How to read data from STAC\n", - "\n", - "This notebook shows how to read the data in from a STAC asset using [xarray](https://docs.xarray.dev/en/stable/) and a little hidden helper library called [xpystac](https://pypi.org/project/xpystac/).\n", - "\n", - "## tl;dr\n", - "\n", - "For any PySTAC object that can be represented as an ndimensional dataset you can read the data using the following command:\n", - "\n", - "```python\n", - "xr.open_dataset(object)\n", - "```\n", - "\n", - "## Dependencies\n", - "\n", - "There are lots of optional dependencies depending on where and how the data you are interested in are stored. Here are some of the libraries that you will probably need:\n", - "\n", - "- dask - to delay data loading until access\n", - "- fsspec - to access data from remote storage\n", - "- pystac - STAC object structures\n", - "- xarray, rioxarray - data structures\n", - "- xpystac, stackstac - helper for loading pystac into xarray objects" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "11dddb09-6313-4822-90ba-26eb6e5c143b", - "metadata": {}, - "outputs": [], - "source": [ - "!pip install adlfs dask 'fsspec[http]' planetary_computer --quiet\n", - "!pip install stackstac xarray xpystac zarr --quiet" - ] - }, - { - "cell_type": "markdown", - "id": "ad3fb6dc-3529-47bd-a5b3-f5260f23db88", - "metadata": {}, - "source": [ - "Despite all these install instructions, the import block is very straightforward" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "2a8afebd-b397-4e7a-b448-0f59cc030e66", - "metadata": {}, - "outputs": [], - "source": [ - "import xarray as xr\n", - "\n", - "import pystac" - ] - }, - { - "cell_type": "markdown", - "id": "6b24745c-b2d5-43d6-9c7e-66458b3a88e3", - "metadata": {}, - "source": [ - "## Examples\n", - "\n", - "Here are a few examples of the different types of objects that you can open in xarray." - ] - }, - { - "cell_type": "markdown", - "id": "30da7cfd-2861-4095-b15b-9952a7d824d9", - "metadata": {}, - "source": [ - "### COGs\n", - "\n", - "Read all the data from the COGs referenced by the assets on an item." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "c77432e6-8b0d-44d2-a947-ec74a529b8cb", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.Dataset>\n",
-       "Dimensions:                      (time: 1, y: 7802, x: 7762, band: 19)\n",
-       "Coordinates: (12/32)\n",
-       "  * time                         (time) datetime64[ns] 2023-04-08T23:37:51.63...\n",
-       "    id                           (time) <U31 ...\n",
-       "  * x                            (x) float64 3.774e+05 3.774e+05 ... 6.102e+05\n",
-       "  * y                            (y) float64 -3.713e+06 ... -3.947e+06\n",
-       "    proj:shape                   object ...\n",
-       "    sci:doi                      <U16 ...\n",
-       "    ...                           ...\n",
-       "    raster:bands                 (band) object ...\n",
-       "    classification:bitfields     (band) object ...\n",
-       "    common_name                  (band) object ...\n",
-       "    center_wavelength            (band) object ...\n",
-       "    full_width_half_max          (band) object ...\n",
-       "    epsg                         int64 ...\n",
-       "Dimensions without coordinates: band\n",
-       "Data variables: (12/19)\n",
-       "    qa                           (time, y, x) float64 ...\n",
-       "    red                          (time, y, x) float64 ...\n",
-       "    blue                         (time, y, x) float64 ...\n",
-       "    drad                         (time, y, x) float64 ...\n",
-       "    emis                         (time, y, x) float64 ...\n",
-       "    emsd                         (time, y, x) float64 ...\n",
-       "    ...                           ...\n",
-       "    swir16                       (time, y, x) float64 ...\n",
-       "    swir22                       (time, y, x) float64 ...\n",
-       "    coastal                      (time, y, x) float64 ...\n",
-       "    qa_pixel                     (time, y, x) float64 ...\n",
-       "    qa_radsat                    (time, y, x) float64 ...\n",
-       "    qa_aerosol                   (time, y, x) float64 ...\n",
-       "Attributes:\n",
-       "    spec:        RasterSpec(epsg=32656, bounds=(377370.0, -3947130.0, 610230....\n",
-       "    crs:         epsg:32656\n",
-       "    transform:   | 30.00, 0.00, 377370.00|\\n| 0.00,-30.00,-3713070.00|\\n| 0.0...\n",
-       "    resolution:  30.0
" - ], - "text/plain": [ - "\n", - "Dimensions: (time: 1, y: 7802, x: 7762, band: 19)\n", - "Coordinates: (12/32)\n", - " * time (time) datetime64[ns] 2023-04-08T23:37:51.63...\n", - " id (time) \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.Dataset>\n",
-       "Dimensions:                  (time: 14965, y: 584, x: 284, nv: 2)\n",
-       "Coordinates:\n",
-       "    lat                      (y, x) float32 ...\n",
-       "    lon                      (y, x) float32 ...\n",
-       "  * time                     (time) datetime64[ns] 1980-01-01T12:00:00 ... 20...\n",
-       "  * x                        (x) float32 -5.802e+06 -5.801e+06 ... -5.519e+06\n",
-       "  * y                        (y) float32 -3.9e+04 -4e+04 ... -6.21e+05 -6.22e+05\n",
-       "Dimensions without coordinates: nv\n",
-       "Data variables:\n",
-       "    dayl                     (time, y, x) float32 ...\n",
-       "    lambert_conformal_conic  int16 ...\n",
-       "    prcp                     (time, y, x) float32 ...\n",
-       "    srad                     (time, y, x) float32 ...\n",
-       "    swe                      (time, y, x) float32 ...\n",
-       "    time_bnds                (time, nv) datetime64[ns] ...\n",
-       "    tmax                     (time, y, x) float32 ...\n",
-       "    tmin                     (time, y, x) float32 ...\n",
-       "    vp                       (time, y, x) float32 ...\n",
-       "    yearday                  (time) int16 ...\n",
-       "Attributes:\n",
-       "    Conventions:       CF-1.6\n",
-       "    Version_data:      Daymet Data Version 4.0\n",
-       "    Version_software:  Daymet Software Version 4.0\n",
-       "    citation:          Please see http://daymet.ornl.gov/ for current Daymet ...\n",
-       "    references:        Please see http://daymet.ornl.gov/ for current informa...\n",
-       "    source:            Daymet Software Version 4.0\n",
-       "    start_year:        1980
" - ], - "text/plain": [ - "\n", - "Dimensions: (time: 14965, y: 584, x: 284, nv: 2)\n", - "Coordinates:\n", - " lat (y, x) float32 ...\n", - " lon (y, x) float32 ...\n", - " * time (time) datetime64[ns] 1980-01-01T12:00:00 ... 20...\n", - " * x (x) float32 -5.802e+06 -5.801e+06 ... -5.519e+06\n", - " * y (y) float32 -3.9e+04 -4e+04 ... -6.21e+05 -6.22e+05\n", - "Dimensions without coordinates: nv\n", - "Data variables:\n", - " dayl (time, y, x) float32 ...\n", - " lambert_conformal_conic int16 ...\n", - " prcp (time, y, x) float32 ...\n", - " srad (time, y, x) float32 ...\n", - " swe (time, y, x) float32 ...\n", - " time_bnds (time, nv) datetime64[ns] ...\n", - " tmax (time, y, x) float32 ...\n", - " tmin (time, y, x) float32 ...\n", - " vp (time, y, x) float32 ...\n", - " yearday (time) int16 ...\n", - "Attributes:\n", - " Conventions: CF-1.6\n", - " Version_data: Daymet Data Version 4.0\n", - " Version_software: Daymet Software Version 4.0\n", - " citation: Please see http://daymet.ornl.gov/ for current Daymet ...\n", - " references: Please see http://daymet.ornl.gov/ for current informa...\n", - " source: Daymet Software Version 4.0\n", - " start_year: 1980" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "daymet_collection = pystac.Collection.from_file(\n", - " \"https://planetarycomputer.microsoft.com/api/stac/v1/collections/daymet-daily-hi\"\n", - ")\n", - "daymet_asset = daymet_collection.assets[\"zarr-abfs\"]\n", - "\n", - "xr.open_dataset(daymet_asset)" - ] - }, - { - "cell_type": "markdown", - "id": "fd4e0c53-90b0-4276-9caf-9014aa0a31f9", - "metadata": {}, - "source": [ - "### Reference file\n", - "\n", - "If the collection has a reference file we can use that" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "00efc688-a8b8-4b45-8ee8-1aa076a870f4", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.Dataset>\n",
-       "Dimensions:  (time: 23741, lat: 600, lon: 1440)\n",
-       "Coordinates:\n",
-       "  * lat      (lat) float64 -59.88 -59.62 -59.38 -59.12 ... 89.38 89.62 89.88\n",
-       "  * lon      (lon) float64 0.125 0.375 0.625 0.875 ... 359.1 359.4 359.6 359.9\n",
-       "  * time     (time) datetime64[us] 1950-01-01T12:00:00 ... 2014-12-31T12:00:00\n",
-       "Data variables:\n",
-       "    hurs     (time, lat, lon) float32 ...\n",
-       "    huss     (time, lat, lon) float32 ...\n",
-       "    pr       (time, lat, lon) float32 ...\n",
-       "    rlds     (time, lat, lon) float32 ...\n",
-       "    rsds     (time, lat, lon) float32 ...\n",
-       "    sfcWind  (time, lat, lon) float32 ...\n",
-       "    tas      (time, lat, lon) float32 ...\n",
-       "    tasmax   (time, lat, lon) float32 ...\n",
-       "    tasmin   (time, lat, lon) float32 ...\n",
-       "Attributes: (12/22)\n",
-       "    Conventions:           CF-1.7\n",
-       "    activity:              NEX-GDDP-CMIP6\n",
-       "    cmip6_institution_id:  CSIRO-ARCCSS\n",
-       "    cmip6_license:         CC-BY-SA 4.0\n",
-       "    cmip6_source_id:       ACCESS-CM2\n",
-       "    contact:               Dr. Rama Nemani: rama.nemani@nasa.gov, Dr. Bridget...\n",
-       "    ...                    ...\n",
-       "    scenario:              historical\n",
-       "    source:                BCSD\n",
-       "    title:                 ACCESS-CM2, r1i1p1f1, historical, global downscale...\n",
-       "    tracking_id:           16d27564-470f-41ea-8077-f4cc3efa5bfe\n",
-       "    variant_label:         r1i1p1f1\n",
-       "    version:               1.0
" - ], - "text/plain": [ - "\n", - "Dimensions: (time: 23741, lat: 600, lon: 1440)\n", - "Coordinates:\n", - " * lat (lat) float64 -59.88 -59.62 -59.38 -59.12 ... 89.38 89.62 89.88\n", - " * lon (lon) float64 0.125 0.375 0.625 0.875 ... 359.1 359.4 359.6 359.9\n", - " * time (time) datetime64[us] 1950-01-01T12:00:00 ... 2014-12-31T12:00:00\n", - "Data variables:\n", - " hurs (time, lat, lon) float32 ...\n", - " huss (time, lat, lon) float32 ...\n", - " pr (time, lat, lon) float32 ...\n", - " rlds (time, lat, lon) float32 ...\n", - " rsds (time, lat, lon) float32 ...\n", - " sfcWind (time, lat, lon) float32 ...\n", - " tas (time, lat, lon) float32 ...\n", - " tasmax (time, lat, lon) float32 ...\n", - " tasmin (time, lat, lon) float32 ...\n", - "Attributes: (12/22)\n", - " Conventions: CF-1.7\n", - " activity: NEX-GDDP-CMIP6\n", - " cmip6_institution_id: CSIRO-ARCCSS\n", - " cmip6_license: CC-BY-SA 4.0\n", - " cmip6_source_id: ACCESS-CM2\n", - " contact: Dr. Rama Nemani: rama.nemani@nasa.gov, Dr. Bridget...\n", - " ... ...\n", - " scenario: historical\n", - " source: BCSD\n", - " title: ACCESS-CM2, r1i1p1f1, historical, global downscale...\n", - " tracking_id: 16d27564-470f-41ea-8077-f4cc3efa5bfe\n", - " variant_label: r1i1p1f1\n", - " version: 1.0" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "cmip6_collection = pystac.Collection.from_file(\n", - " \"https://planetarycomputer.microsoft.com/api/stac/v1/collections/nasa-nex-gddp-cmip6\"\n", - ")\n", - "cmip6_asset = cmip6_collection.assets[\"ACCESS-CM2.historical\"]\n", - "\n", - "xr.open_dataset(cmip6_asset)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.15" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/tutorials/pystac-introduction.ipynb b/docs/tutorials/pystac-introduction.ipynb deleted file mode 100644 index 0cdf06f86..000000000 --- a/docs/tutorials/pystac-introduction.ipynb +++ /dev/null @@ -1,6362 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# PySTAC Introduction" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This tutorial includes a basic introduction on reading, writing, and creating STAC objects using Pystac.\n", - "\n", - "It is adapted from the tutorials within the [sat-stac repo](https://github.com/sat-utils/sat-stac/blob/master/tutorial-1.ipynb).\n", - "\n", - "It uses an example stac stored in the `../example-catalog` directory along-side this notebook. The example stac has the following format:\n", - "\n", - "```\n", - "../example-catalog\n", - "├── catalog.json\n", - "└── landsat-8-l1\n", - " ├── 2018-05\n", - " │ └── LC80150322018141LGN00.json\n", - " ├── 2018-06\n", - " │ ├── LC80140332018166LGN00.json\n", - " │ └── LC80300332018166LGN00.json\n", - " ├── 2018-07\n", - " │ └── LC80150332018189LGN00.json\n", - " └── collection.json\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import pystac" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Working with existing catalogs" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Open a root catalog from it's json file" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "cat = pystac.Catalog.from_file(\"../example-catalog/catalog.json\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can see all elements of the STAC using the `describe` method" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "* \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n" - ] - } - ], - "source": [ - "cat.describe()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Each STAC object has links that you can use to traverse the STAC tree" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[,\n", - " >,\n", - " >]" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "cat.links" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Pystac has several methods that allow you to access links:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[>]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Get all child links\n", - "cat.get_child_links()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "or the children directly:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "list(cat.get_children())" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "
\n", - "
\n", - "
    \n", - " \n", - " \n", - " \n", - "
  • \n", - " type\n", - " \"Collection\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " id\n", - " \"landsat-8-l1\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " stac_version\n", - " \"1.0.0\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " description\n", - " \"Landsat 8 imagery radiometrically calibrated and orthorectified using ground points and Digital Elevation Model (DEM) data to correct relief displacement.\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " links\n", - " [] 7 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"root\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/catalog.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"application/json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"STAC for Landsat data\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 1\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"item\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-06/LC80140332018166LGN00.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 2\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"item\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-05/LC80150322018141LGN00.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 3\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"item\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-07/LC80150332018189LGN00.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 4\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"item\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-06/LC80300332018166LGN00.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 5\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"self\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/collection.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"application/json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 6\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"parent\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/catalog.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"application/json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"STAC for Landsat data\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " stac_extensions\n", - " [] 1 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - " \"https://example.com/stac/landsat-extension/1.0/schema.json\"\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " properties\n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " collection\n", - " \"landsat-8-l1\"\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " \n", - " instruments\n", - " [] 1 items\n", - " \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 0\n", - " \"OLI_TIRS\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " view:sun_azimuth\n", - " 149.01607154\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " \n", - " eo:bands\n", - " [] 11 items\n", - " \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 0\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B1\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.02\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.44\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"coastal\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 1\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B2\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.06\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.48\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"blue\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 2\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B3\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.06\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.56\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"green\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 3\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B4\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.04\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.65\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"red\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 4\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B5\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.03\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.86\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"nir\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 5\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B6\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.08\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 1.6\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"swir16\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 6\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B7\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.22\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 2.2\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"swir22\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 7\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B8\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.18\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.59\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"pan\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 8\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B9\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.02\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 1.37\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"cirrus\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 9\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B10\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.8\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 10.9\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"lwir11\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 10\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B11\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 1\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 12\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"lwir2\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " view:off_nadir\n", - " 0\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " view:azimuth\n", - " 0\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " platform\n", - " \"landsat-8\"\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " gsd\n", - " 15\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " view:sun_elevation\n", - " 59.214247\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " title\n", - " \"Landsat 8 L1\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " extent\n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " spatial\n", - "
        \n", - " \n", - " \n", - "
      • \n", - " \n", - " bbox\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - "
        • \n", - " \n", - " 0\n", - " [] 4 items\n", - " \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 0\n", - " -180.0\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 1\n", - " -90.0\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 2\n", - " 180.0\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 3\n", - " 90.0\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
        • \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " temporal\n", - "
        \n", - " \n", - " \n", - "
      • \n", - " \n", - " interval\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - "
        • \n", - " \n", - " 0\n", - " [] 2 items\n", - " \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 0\n", - " \"2018-05-21T15:44:59Z\"\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 1\n", - " \"2018-07-08T15:45:34Z\"\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
        • \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " license\n", - " \"proprietary\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " keywords\n", - " [] 3 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - " \"landsat\"\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 1\n", - " \"earth observation\"\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 2\n", - " \"usgs\"\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " providers\n", - " [] 1 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " name\n", - " \"Development Seed\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " 0\n", - " \"processor\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " url\n", - " \"https://github.com/sat-utils/sat-api\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - "
\n", - "
\n", - "
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# or a single child by id\n", - "cat.get_child(\"landsat-8-l1\")" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "
\n", - "
\n", - "
    \n", - " \n", - " \n", - " \n", - "
  • \n", - " rel\n", - " \"self\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/catalog.json\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " type\n", - " \"application/json\"\n", - "
  • \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - "
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Get a single link by 'rel'\n", - "cat.get_single_link(\"self\")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Get item links directly within this catalog (there are none for this catalog)\n", - "cat.get_item_links()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "or the items directly:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# get item objects\n", - "list(cat.get_items())" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[,\n", - " ,\n", - " ,\n", - " ]" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# get all items anywhere below this catalog on the STAC tree\n", - "list(cat.get_items(recursive=True))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can access the stac item from a link using the `target` property" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - ">\n" - ] - } - ], - "source": [ - "link = cat.get_single_link(\"child\")\n", - "print(link)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n" - ] - } - ], - "source": [ - "print(link.target)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can convert any stac item to a python dict using the `to_dict` method." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'type': 'Catalog',\n", - " 'id': 'landsat-stac-collection-catalog',\n", - " 'stac_version': '1.0.0',\n", - " 'description': 'STAC for Landsat data',\n", - " 'links': [{'rel': 'root',\n", - " 'href': './catalog.json',\n", - " 'type': 'application/json',\n", - " 'title': 'STAC for Landsat data'},\n", - " {'rel': 'child',\n", - " 'href': './landsat-8-l1/collection.json',\n", - " 'title': 'Landsat 8 L1'}],\n", - " 'title': 'STAC for Landsat data'}" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "cat.to_dict(include_self_link=False)" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "# get first (and only in this case) sub-catalog\n", - "subcat = next(cat.get_children())" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Root Catalog: landsat-stac-collection-catalog\n", - "Sub Catalog: landsat-8-l1\n", - "Sub Catalog parent: landsat-stac-collection-catalog\n", - "Sub Catalog children:\n" - ] - } - ], - "source": [ - "# print some IDs\n", - "print(\"Root Catalog: \", cat.id)\n", - "print(\"Sub Catalog: \", subcat.id)\n", - "print(\"Sub Catalog parent: \", subcat.get_parent().id)\n", - "\n", - "# iterate through child catalogs of the sub-catalog\n", - "print(\"Sub Catalog children:\")\n", - "for child in subcat.get_children():\n", - " print(\" \", child.id)" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "**Items**\n", - "LC80140332018166LGN00\n", - "LC80150322018141LGN00\n", - "LC80150332018189LGN00\n", - "LC80300332018166LGN00\n" - ] - } - ], - "source": [ - "print(\"\\n**Items**\")\n", - "for i in cat.get_items(recursive=True):\n", - " print(i.id)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Creating new catalogs" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can initialize a new Catalog with an id and a description. Note that by default it sets a new catalog as root." - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "# create a Catalog object with JSON\n", - "mycat = pystac.Catalog(id=\"mycat\", description=\"My shiny new STAC catalog\")" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[>]" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "mycat.links" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Adding catalogs to catalogs" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "# add a new catalog to a root catalog\n", - "kitten = pystac.Catalog(\n", - " id=\"mykitten\", description=\"A child catalog of my shiny new STAC catalog\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "When you add a child catalog to a parent catalog, the child catalog assumes the root catalog of it's parent. 'Child' and 'parent' links are also added to the parent and child catalogs, respectively." - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[>]" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "kitten.links" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "
\n", - "
\n", - "
    \n", - " \n", - " \n", - " \n", - "
  • \n", - " rel\n", - " \"child\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " href\n", - " None\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " type\n", - " \"application/json\"\n", - "
  • \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - "
" - ], - "text/plain": [ - ">" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "mycat.add_child(kitten)" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[>,\n", - " >]" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "kitten.links" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[>,\n", - " >]" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "mycat.links" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "* \n", - " * \n" - ] - } - ], - "source": [ - "mycat.describe()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Adding collections to catalogs" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In the next two steps we will work with Pystac Collections and Items. We will pull them out of our example catalog and add them to the new STAC that we have created." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Collections are Catalogs but also include spatial and temporal extents as well as additional properties. " - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "
\n", - "
\n", - "
    \n", - " \n", - " \n", - " \n", - "
  • \n", - " type\n", - " \"Collection\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " id\n", - " \"landsat-8-l1\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " stac_version\n", - " \"1.0.0\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " description\n", - " \"Landsat 8 imagery radiometrically calibrated and orthorectified using ground points and Digital Elevation Model (DEM) data to correct relief displacement.\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " links\n", - " [] 7 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"self\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/collection.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"application/json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 1\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"root\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"../catalog.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 2\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"parent\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"../catalog.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 3\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"item\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"./2018-06/LC80140332018166LGN00.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 4\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"item\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"./2018-05/LC80150322018141LGN00.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 5\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"item\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"./2018-07/LC80150332018189LGN00.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 6\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"item\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"./2018-06/LC80300332018166LGN00.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " stac_extensions\n", - " [] 1 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - " \"https://example.com/stac/landsat-extension/1.0/schema.json\"\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " properties\n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " collection\n", - " \"landsat-8-l1\"\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " \n", - " instruments\n", - " [] 1 items\n", - " \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 0\n", - " \"OLI_TIRS\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " view:sun_azimuth\n", - " 149.01607154\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " \n", - " eo:bands\n", - " [] 11 items\n", - " \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 0\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B1\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.02\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.44\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"coastal\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 1\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B2\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.06\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.48\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"blue\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 2\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B3\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.06\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.56\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"green\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 3\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B4\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.04\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.65\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"red\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 4\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B5\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.03\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.86\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"nir\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 5\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B6\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.08\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 1.6\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"swir16\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 6\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B7\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.22\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 2.2\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"swir22\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 7\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B8\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.18\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 0.59\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"pan\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 8\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B9\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.02\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 1.37\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"cirrus\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 9\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B10\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 0.8\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 10.9\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"lwir11\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 10\n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " name\n", - " \"B11\"\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " full_width_half_max\n", - " 1\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " center_wavelength\n", - " 12\n", - "
        • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
        • \n", - " common_name\n", - " \"lwir2\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " view:off_nadir\n", - " 0\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " view:azimuth\n", - " 0\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " platform\n", - " \"landsat-8\"\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " gsd\n", - " 15\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " view:sun_elevation\n", - " 59.214247\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " title\n", - " \"Landsat 8 L1\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " extent\n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " spatial\n", - "
        \n", - " \n", - " \n", - "
      • \n", - " \n", - " bbox\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - "
        • \n", - " \n", - " 0\n", - " [] 4 items\n", - " \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 0\n", - " -180.0\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 1\n", - " -90.0\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 2\n", - " 180.0\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 3\n", - " 90.0\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
        • \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " temporal\n", - "
        \n", - " \n", - " \n", - "
      • \n", - " \n", - " interval\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - "
        • \n", - " \n", - " 0\n", - " [] 2 items\n", - " \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 0\n", - " \"2018-05-21T15:44:59Z\"\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 1\n", - " \"2018-07-08T15:45:34Z\"\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
        • \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " license\n", - " \"proprietary\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " keywords\n", - " [] 3 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - " \"landsat\"\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 1\n", - " \"earth observation\"\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 2\n", - " \"usgs\"\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " providers\n", - " [] 1 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " name\n", - " \"Development Seed\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " 0\n", - " \"processor\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " url\n", - " \"https://github.com/sat-utils/sat-api\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - "
\n", - "
\n", - "
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# open the Landsat collection\n", - "collection = pystac.Collection.from_file(\n", - " \"../example-catalog/landsat-8-l1/collection.json\"\n", - ")\n", - "collection" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "See the spatial and temporal extent of this collection" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'spatial': {'bbox': [[-180.0, -90.0, 180.0, 90.0]]},\n", - " 'temporal': {'interval': [['2018-05-21T15:44:59Z', '2018-07-08T15:45:34Z']]}}" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "collection.extent.to_dict()" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ]" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "collection.links" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "
\n", - "
\n", - "
    \n", - " \n", - " \n", - " \n", - "
  • \n", - " rel\n", - " \"child\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/collection.json\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " type\n", - " \"application/json\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " title\n", - " \"Landsat 8 L1\"\n", - "
  • \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - "
" - ], - "text/plain": [ - ">" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# add it to the child catalog created above\n", - "kitten.add_child(collection)" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[,\n", - " >,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " >]" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "collection.links" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Adding items to collection" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Items are stac objects whose parents can be either Catalogs or Collections. They also have spatio-temporal information and assets. Assets point directly to the data included in the STAC." - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "
\n", - "
\n", - "
    \n", - " \n", - " \n", - " \n", - "
  • \n", - " type\n", - " \"Feature\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " stac_version\n", - " \"1.0.0\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " id\n", - " \"LC80150322018141LGN00\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " properties\n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " collection\n", - " \"landsat-8-l1\"\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " datetime\n", - " \"2018-05-21T15:44:59Z\"\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " view:sun_azimuth\n", - " 134.8082647\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " view:sun_elevation\n", - " 64.00406717\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " eo:cloud_cover\n", - " 4\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " \n", - " instruments\n", - " [] 1 items\n", - " \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 0\n", - " \"OLI_TIRS\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " view:off_nadir\n", - " 0\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " platform\n", - " \"landsat-8\"\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " gsd\n", - " 30\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " proj:epsg\n", - " 32618\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " \n", - " proj:transform\n", - " [] 6 items\n", - " \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 0\n", - " 258885.0\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 1\n", - " 30.0\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 2\n", - " 0.0\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 3\n", - " 4584315.0\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 4\n", - " 0.0\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 5\n", - " -30.0\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " proj:geometry\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"Polygon\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " coordinates\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - "
        • \n", - " \n", - " 0\n", - " [] 5 items\n", - " \n", - " \n", - "
            \n", - " \n", - " \n", - "
          • \n", - " \n", - " 0\n", - " [] 2 items\n", - " \n", - " \n", - "
              \n", - " \n", - " \n", - " \n", - "
            • \n", - " 0\n", - " 258885.0\n", - "
            • \n", - " \n", - " \n", - " \n", - "
            \n", - " \n", - "
              \n", - " \n", - " \n", - " \n", - "
            • \n", - " 1\n", - " 4346085.0\n", - "
            • \n", - " \n", - " \n", - " \n", - "
            \n", - " \n", - "
          • \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - "
          • \n", - " \n", - " 1\n", - " [] 2 items\n", - " \n", - " \n", - "
              \n", - " \n", - " \n", - " \n", - "
            • \n", - " 0\n", - " 258885.0\n", - "
            • \n", - " \n", - " \n", - " \n", - "
            \n", - " \n", - "
              \n", - " \n", - " \n", - " \n", - "
            • \n", - " 1\n", - " 4584315.0\n", - "
            • \n", - " \n", - " \n", - " \n", - "
            \n", - " \n", - "
          • \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - "
          • \n", - " \n", - " 2\n", - " [] 2 items\n", - " \n", - " \n", - "
              \n", - " \n", - " \n", - " \n", - "
            • \n", - " 0\n", - " 493515.0\n", - "
            • \n", - " \n", - " \n", - " \n", - "
            \n", - " \n", - "
              \n", - " \n", - " \n", - " \n", - "
            • \n", - " 1\n", - " 4584315.0\n", - "
            • \n", - " \n", - " \n", - " \n", - "
            \n", - " \n", - "
          • \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - "
          • \n", - " \n", - " 3\n", - " [] 2 items\n", - " \n", - " \n", - "
              \n", - " \n", - " \n", - " \n", - "
            • \n", - " 0\n", - " 493515.0\n", - "
            • \n", - " \n", - " \n", - " \n", - "
            \n", - " \n", - "
              \n", - " \n", - " \n", - " \n", - "
            • \n", - " 1\n", - " 4346085.0\n", - "
            • \n", - " \n", - " \n", - " \n", - "
            \n", - " \n", - "
          • \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - "
          • \n", - " \n", - " 4\n", - " [] 2 items\n", - " \n", - " \n", - "
              \n", - " \n", - " \n", - " \n", - "
            • \n", - " 0\n", - " 258885.0\n", - "
            • \n", - " \n", - " \n", - " \n", - "
            \n", - " \n", - "
              \n", - " \n", - " \n", - " \n", - "
            • \n", - " 1\n", - " 4346085.0\n", - "
            • \n", - " \n", - " \n", - " \n", - "
            \n", - " \n", - "
          • \n", - " \n", - " \n", - "
          \n", - " \n", - "
        • \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " \n", - " proj:shape\n", - " [] 2 items\n", - " \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 0\n", - " 7821\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " 1\n", - " 7941\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - " \n", - "
    • \n", - " \n", - " \n", - "
    \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " geometry\n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " type\n", - " \"Polygon\"\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " \n", - " coordinates\n", - " [] 1 items\n", - " \n", - " \n", - "
        \n", - " \n", - " \n", - "
      • \n", - " \n", - " 0\n", - " [] 5 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - "
        • \n", - " \n", - " 0\n", - " [] 2 items\n", - " \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 0\n", - " -77.28911976020206\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 1\n", - " 41.40912394323429\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
        • \n", - " \n", - " \n", - "
        \n", - " \n", - "
          \n", - " \n", - " \n", - "
        • \n", - " \n", - " 1\n", - " [] 2 items\n", - " \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 0\n", - " -75.07576783500748\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 1\n", - " 40.97162247589133\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
        • \n", - " \n", - " \n", - "
        \n", - " \n", - "
          \n", - " \n", - " \n", - "
        • \n", - " \n", - " 2\n", - " [] 2 items\n", - " \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 0\n", - " -75.66872631473827\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 1\n", - " 39.23210949585851\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
        • \n", - " \n", - " \n", - "
        \n", - " \n", - "
          \n", - " \n", - " \n", - "
        • \n", - " \n", - " 3\n", - " [] 2 items\n", - " \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 0\n", - " -77.87946700654118\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 1\n", - " 39.67679918442899\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
        • \n", - " \n", - " \n", - "
        \n", - " \n", - "
          \n", - " \n", - " \n", - "
        • \n", - " \n", - " 4\n", - " [] 2 items\n", - " \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 0\n", - " -77.28911976020206\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " 1\n", - " 41.40912394323429\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - " \n", - "
        • \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - " \n", - "
    • \n", - " \n", - " \n", - "
    \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " links\n", - " [] 4 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"self\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-05/LC80150322018141LGN00.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"application/json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 1\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"parent\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"../collection.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 2\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"collection\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"../collection.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 3\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " rel\n", - " \"root\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"../../catalog.json\"\n", - "
      • \n", - " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " assets\n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " index\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/index.html\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"text/html\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"HTML index page\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 0 items\n", - " \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " thumbnail\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_thumb_large.jpg\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"image/jpeg\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"Thumbnail image\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " 0\n", - " \"thumbnail\"\n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " B1\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B1.TIF\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"image/tiff\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"Band 1 (coastal)\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " eo:bands\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " 0\n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " name\n", - " \"B1\"\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " full_width_half_max\n", - " 0.02\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " center_wavelength\n", - " 0.44\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " common_name\n", - " \"coastal\"\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 0 items\n", - " \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " B2\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B2.TIF\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"image/tiff\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"Band 2 (blue)\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " eo:bands\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " 0\n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " name\n", - " \"B2\"\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " full_width_half_max\n", - " 0.06\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " center_wavelength\n", - " 0.48\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " common_name\n", - " \"blue\"\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 0 items\n", - " \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " B3\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B3.TIF\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"image/tiff\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"Band 3 (green)\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " eo:bands\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " 0\n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " name\n", - " \"B3\"\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " full_width_half_max\n", - " 0.06\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " center_wavelength\n", - " 0.56\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " common_name\n", - " \"green\"\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 0 items\n", - " \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " B4\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B4.TIF\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"image/tiff\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"Band 4 (red)\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " eo:bands\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " 0\n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " name\n", - " \"B4\"\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " full_width_half_max\n", - " 0.04\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " center_wavelength\n", - " 0.65\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " common_name\n", - " \"red\"\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 0 items\n", - " \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " B5\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B5.TIF\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"image/tiff\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"Band 5 (nir)\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " eo:bands\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " 0\n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " name\n", - " \"B5\"\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " full_width_half_max\n", - " 0.03\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " center_wavelength\n", - " 0.86\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " common_name\n", - " \"nir\"\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 0 items\n", - " \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " B6\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B6.TIF\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"image/tiff\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"Band 6 (swir16)\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " eo:bands\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " 0\n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " name\n", - " \"B6\"\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " full_width_half_max\n", - " 0.08\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " center_wavelength\n", - " 1.6\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " common_name\n", - " \"swir16\"\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 0 items\n", - " \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " B7\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B7.TIF\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"image/tiff\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"Band 7 (swir22)\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " eo:bands\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " 0\n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " name\n", - " \"B7\"\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " full_width_half_max\n", - " 0.22\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " center_wavelength\n", - " 2.2\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " common_name\n", - " \"swir22\"\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 0 items\n", - " \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " B8\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B8.TIF\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"image/tiff\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"Band 8 (pan)\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " eo:bands\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " 0\n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " name\n", - " \"B8\"\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " full_width_half_max\n", - " 0.18\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " center_wavelength\n", - " 0.59\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " common_name\n", - " \"pan\"\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 0 items\n", - " \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " B9\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B9.TIF\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"image/tiff\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"Band 9 (cirrus)\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " eo:bands\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " 0\n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " name\n", - " \"B9\"\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " full_width_half_max\n", - " 0.02\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " center_wavelength\n", - " 1.37\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " common_name\n", - " \"cirrus\"\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 0 items\n", - " \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " B10\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B10.TIF\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"image/tiff\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"Band 10 (lwir)\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " eo:bands\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " 0\n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " name\n", - " \"B10\"\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " full_width_half_max\n", - " 0.8\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " center_wavelength\n", - " 10.9\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " common_name\n", - " \"lwir11\"\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 0 items\n", - " \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " B11\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B11.TIF\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"image/tiff\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"Band 11 (lwir)\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " eo:bands\n", - " [] 1 items\n", - " \n", - " \n", - "
          \n", - " \n", - " \n", - " \n", - "
        • \n", - " 0\n", - "
            \n", - " \n", - " \n", - " \n", - "
          • \n", - " name\n", - " \"B11\"\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " full_width_half_max\n", - " 1\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " center_wavelength\n", - " 12\n", - "
          • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
          • \n", - " common_name\n", - " \"lwir2\"\n", - "
          • \n", - " \n", - " \n", - " \n", - "
          \n", - "
        • \n", - " \n", - " \n", - " \n", - "
        \n", - " \n", - "
      • \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 0 items\n", - " \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " ANG\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_ANG.txt\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"text/plain\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"Angle coefficients file\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 0 items\n", - " \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " MTL\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_MTL.txt\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"text/plain\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"original metadata file\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 0 items\n", - " \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " BQA\n", - "
        \n", - " \n", - " \n", - " \n", - "
      • \n", - " href\n", - " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_BQA.TIF\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " type\n", - " \"image/tiff\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " title\n", - " \"Band quality data\"\n", - "
      • \n", - " \n", - " \n", - " \n", - " \n", - "
      • \n", - " \n", - " roles\n", - " [] 0 items\n", - " \n", - " \n", - "
      • \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " bbox\n", - " [] 4 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - " -77.88298\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 1\n", - " 39.23073\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 2\n", - " -75.07535\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 3\n", - " 41.41022\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - " \n", - "
  • \n", - " \n", - " stac_extensions\n", - " [] 3 items\n", - " \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 0\n", - " \"https://stac-extensions.github.io/eo/v1.1.0/schema.json\"\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 1\n", - " \"https://stac-extensions.github.io/view/v1.0.0/schema.json\"\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " 2\n", - " \"https://stac-extensions.github.io/projection/v1.1.0/schema.json\"\n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " collection\n", - " \"landsat-8-l1\"\n", - "
  • \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - "
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# open a Landsat item\n", - "item = pystac.read_file(\n", - " \"../example-catalog/landsat-8-l1/2018-05/LC80150322018141LGN00.json\"\n", - ")\n", - "item" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[,\n", - " ,\n", - " ,\n", - " ]" - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.links" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'index': ,\n", - " 'thumbnail': ,\n", - " 'B1': ,\n", - " 'B2': ,\n", - " 'B3': ,\n", - " 'B4': ,\n", - " 'B5': ,\n", - " 'B6': ,\n", - " 'B7': ,\n", - " 'B8': ,\n", - " 'B9': ,\n", - " 'B10': ,\n", - " 'B11': ,\n", - " 'ANG': ,\n", - " 'MTL': ,\n", - " 'BQA': }" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item.assets" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "
\n", - "
\n", - "
    \n", - " \n", - " \n", - " \n", - "
  • \n", - " rel\n", - " \"item\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " href\n", - " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/LC80150322018141LGN00/LC80150322018141LGN00.json\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " type\n", - " \"application/json\"\n", - "
  • \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - "
" - ], - "text/plain": [ - ">" - ] - }, - "execution_count": 34, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# add it to the collection created above\n", - "collection.add_item(item)" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "* \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n", - " * \n" - ] - } - ], - "source": [ - "# now look at the catalog we've created\n", - "mycat.describe()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Currently, this STAC only exists in memory. We can use `normalize_and_save` to save off the STAC with the canonical \"absolute published\" form:" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "metadata": {}, - "outputs": [], - "source": [ - "mycat.normalize_and_save(\n", - " \"pystac-example-absolute\", catalog_type=pystac.CatalogType.ABSOLUTE_PUBLISHED\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Notice now that the 'parent' link of an item is a absolute HREF:" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'/home/jsignell/pystac/docs/tutorials/pystac-example-absolute/mykitten/landsat-8-l1/collection.json'" - ] - }, - "execution_count": 37, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item = next(mycat.get_items(recursive=True))\n", - "item.get_single_link(\"parent\").get_href()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can also normalize and save the catalog to the other types described in the best practices documentation: \"relative published\" and \"self contained\". A self contained catalog contains all relative links, and no self links. Notice how saving a self contained catalog will produce relative links:" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "metadata": {}, - "outputs": [], - "source": [ - "mycat.normalize_and_save(\n", - " \"pystac-example-relative\", catalog_type=pystac.CatalogType.SELF_CONTAINED\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'../collection.json'" - ] - }, - "execution_count": 39, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "item = next(mycat.get_items(recursive=True))\n", - "item.get_single_link(\"parent\").get_href()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.6" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/tutorials/pystac-spacenet-tutorial.ipynb b/docs/tutorials/pystac-spacenet-tutorial.ipynb deleted file mode 100644 index b79a2773d..000000000 --- a/docs/tutorials/pystac-spacenet-tutorial.ipynb +++ /dev/null @@ -1,377 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Create and manipulate SpaceNet Vegas STAC" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This tutorial shows how to create and manipulate STACs using pystac.\n", - "\n", - "- Create (in memory) a pystac catalog of [SpaceNet 2 imagery from the Las Vegas AOI](https://spacenetchallenge.github.io/AOI_Lists/AOI_2_Vegas.html) using data hosted in a public s3 bucket\n", - "- Set relative paths for all STAC object\n", - "- Normalize links from a root directory and save the STAC there" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "\n", - "sys.path.append(\"..\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You may need install the following packages that are not included in the Python 3 standard library. If you do not have any of these installed, you can do do with pip:\n", - "\n", - "[boto3](https://pypi.org/project/boto3/): `pip install boto3` \n", - "[botocore](https://pypi.org/project/botocore/): `pip install botocore` \n", - "[rasterio](https://pypi.org/project/rasterio/): `pip install rasterio` \n", - "[shapely](https://pypi.org/project/Shapely/): `pip install Shapely` \n", - "[rio-cogeo](https://github.com/cogeotiff/rio-cogeo): `pip install rio-cogeo`" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "from datetime import datetime\n", - "from os.path import basename, join\n", - "\n", - "import boto3\n", - "import rasterio\n", - "from shapely.geometry import GeometryCollection, box, mapping, shape\n", - "\n", - "import pystac" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Create SpaceNet Vegas STAC" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Initialize a STAC for the SpaceNet 2 dataset" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "spacenet = pystac.Catalog(id=\"spacenet\", description=\"SpaceNet 2 STAC\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We do not yet know the spatial extent of the Vegas AOI. We will need to determine it when we download all of the images. As a placeholder we will create a spatial extent of null values." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "sp_extent = pystac.SpatialExtent([None, None, None, None])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The capture date for SpaceNet 2 Vegas imagery is October 22, 2015. Create a python datetime object using that date" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [], - "source": [ - "capture_date = datetime.strptime(\"2015-10-22\", \"%Y-%m-%d\")\n", - "tmp_extent = pystac.TemporalExtent([(capture_date, None)])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create an Extent object that will define both the spatial and temporal extents of the Vegas collection" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [], - "source": [ - "extent = pystac.Extent(sp_extent, tmp_extent)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create a collection that will encompass the Vegas data and add to the spacenet catalog" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "
\n", - "
\n", - "
    \n", - " \n", - " \n", - " \n", - "
  • \n", - " rel\n", - " \"child\"\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " href\n", - " None\n", - "
  • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
  • \n", - " type\n", - " \"application/json\"\n", - "
  • \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - "
" - ], - "text/plain": [ - ">" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "vegas = pystac.Collection(\n", - " id=\"vegas\", description=\"Vegas SpaceNet 2 dataset\", extent=extent\n", - ")\n", - "spacenet.add_child(vegas)" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "* \n", - " * \n" - ] - } - ], - "source": [ - "spacenet.describe()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Find the locations of SpaceNet images. In order to make this example quicker, we will limit the number of scenes that we use to 10." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [], - "source": [ - "client = boto3.client(\"s3\")\n", - "scenes = client.list_objects(\n", - " Bucket=\"spacenet-dataset\",\n", - " Prefix=\"spacenet/SN2_buildings/train/AOI_2_Vegas/PS-RGB/\",\n", - " MaxKeys=20,\n", - ")\n", - "scenes = [s[\"Key\"] for s in scenes[\"Contents\"] if s[\"Key\"].endswith(\".tif\")][0:10]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For each scene, create and item with a defined bounding box. Each item will include the geotiff as an asset. We will add labels in the next section." - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [], - "source": [ - "for scene in scenes:\n", - " uri = join(\"s3://spacenet-dataset/\", scene)\n", - " params = {}\n", - " params[\"id\"] = basename(uri).split(\".\")[0]\n", - " with rasterio.open(uri) as src:\n", - " params[\"bbox\"] = list(src.bounds)\n", - " params[\"geometry\"] = mapping(box(*params[\"bbox\"]))\n", - " params[\"datetime\"] = capture_date\n", - " params[\"properties\"] = {}\n", - " i = pystac.Item(**params)\n", - " i.add_asset(\n", - " key=\"image\",\n", - " asset=pystac.Asset(\n", - " href=uri, title=\"Geotiff\", media_type=pystac.MediaType.GEOTIFF\n", - " ),\n", - " )\n", - " vegas.add_item(i)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now reset the spatial extent of the Vegas collection using the geometry objects from from the items we just added." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [], - "source": [ - "bounds = [\n", - " list(\n", - " GeometryCollection(\n", - " [shape(s.geometry) for s in spacenet.get_items(recursive=True)]\n", - " ).bounds\n", - " )\n", - "]\n", - "vegas.extent.spatial = pystac.SpatialExtent(bounds)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Currently, this STAC only exists in memory. We need to set all of the paths based on the root directory we want to save off that catalog too, and then save a \"self contained\" catalog, which will have all links be relative and contain no 'self' links. We can do this by using the `normalize` method to set the HREFs of all of our STAC objects. We'll then validate the catalog, and then save:" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [], - "source": [ - "spacenet.normalize_hrefs(\"spacenet-stac\")" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "10" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "spacenet.validate_all()" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [], - "source": [ - "spacenet.save(catalog_type=pystac.CatalogType.SELF_CONTAINED)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.3" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/img/stac-python.png b/img/stac-python.png new file mode 100644 index 000000000..798c91ea0 Binary files /dev/null and b/img/stac-python.png differ diff --git a/img/stac-python.xcf b/img/stac-python.xcf new file mode 100644 index 000000000..f2b67a870 Binary files /dev/null and b/img/stac-python.xcf differ diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 000000000..a1564b4a5 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,80 @@ +site_name: PySTAC +site_url: https://stac-utils.github.com/pystac/ +repo_name: stac-utils/pystac +repo_url: https://github.com/stac-utils/pystac +theme: + name: material + custom_dir: docs/overrides + logo: img/stac-python.png + favicon: img/stac-python.png + features: + - navigation.instant + - navigation.indexes + palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + primary: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: default + toggle: + icon: material/brightness-4 + name: Switch to system preference + +watch: + - docs + - src + +nav: + - Home: index.md + - pystac-v2.0.md + - API: + - api/general.md + - api/stac_object.md + - api/container.md + - api/catalog.md + - api/collection.md + - api/item.md + - api/asset.md + - api/render.md + - api/io.md + - Extensions: + - api/extensions/index.md + - api/extensions/projection.md + - api/validate.md + - api/constants.md + +extra: + alias: true + provider: mike + +extra_css: + - overrides/stylesheets/extra.css + +plugins: + - mkdocstrings: + handlers: + python: + options: + show_bases: true + show_root_heading: true + show_root_full_path: true + show_symbol_type_heading: true + show_symbol_type_toc: true + separate_signature: true + show_signature_annotations: true + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format diff --git a/pyproject.toml b/pyproject.toml index 74bf4d847..14e792d72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,12 @@ [project] name = "pystac" +version = "2.0.0a0" description = "Python library for working with the SpatioTemporal Asset Catalog (STAC) specification" readme = "README.md" authors = [ { name = "Rob Emanuele", email = "rdemanuele@gmail.com" }, { name = "Jon Duckworth", email = "duckontheweb@gmail.com" }, + { name = "Pete Gadomski", email = "pete.gadomski@gmail.com" }, ] maintainers = [{ name = "Pete Gadomski", email = "pete.gadomski@gmail.com" }] keywords = ["pystac", "imagery", "raster", "catalog", "STAC"] @@ -21,98 +23,43 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] requires-python = ">=3.10" -dependencies = ["python-dateutil>=2.7.0"] -dynamic = ["version"] +dependencies = ["python-dateutil>=2.9.0.post0", "typing-extensions>=4.12.2"] [project.optional-dependencies] -jinja2 = ["jinja2<4.0"] -orjson = ["orjson>=3.5"] -urllib3 = ["urllib3>=1.26"] -validation = ["jsonschema~=4.18"] +validate = ["jsonschema>=4.23.0", "referencing>=0.36.2"] +obstore = ["obstore>=0.4.0"] [dependency-groups] dev = [ - "asv>=0.6.4", - "codespell<2.3", - "coverage>=7.6.2", - "doc8>=1.1.2", - "html5lib>=1.1", - "jinja2>=3.1.4", - "jsonschema>=4.23.0", - "mypy>=1.11.2", - "orjson>=3.10.7", - "packaging>=24.1", - "pre-commit>=4.0.1", - "pytest>=8.3.3", - "pytest-cov>=5.0.0", - "pytest-mock>=3.14.0", - "pytest-recording>=0.13.2", - "requests-mock>=1.12.1", - "ruff>=0.6.9", - "types-html5lib>=1.1.11.20240806", - "types-jsonschema>=4.23.0.20240813", - "types-orjson>=3.6.2", - "types-python-dateutil>=2.9.0.20241003", - "types-urllib3>=1.26.25.14", - "virtualenv>=20.26.6", -] -docs = [ - "boto3>=1.35.39", - "ipython>=8.28.0", - "jinja2>=3.1.4", - "jupyter>=1.1.1", - "nbsphinx>=0.9.5", - "pydata-sphinx-theme>=0.15.4", - "rasterio>=1.4.1", - "shapely>=2.0.6", - "sphinx>=8.1.1", - "sphinx-autobuild>=2024.10.3", - "sphinx-design>=0.6.1", - "sphinxcontrib-fulltoc>=1.2.0", + "mypy>=1.15.0", + "pytest>=8.3.4", + "ruff>=0.9.6", + "types-jsonschema>=4.23.0.20241208", + "types-python-dateutil>=2.9.0.20241206", ] +bench = ["asv>=0.6.4"] +docs = ["mike>=2.1.3", "mkdocs-material>=9.6.3", "mkdocstrings-python>=1.14.6"] [project.urls] -Documentation = "https://pystac.readthedocs.io" +Documentation = "https://stac-utils.github.io/pystac" Repository = "https://github.com/stac-utils/pystac" Issues = "https://github.com/stac-utils/pystac/issues" Changelog = "https://github.com/stac-utils/pystac/blob/main/CHANGELOG.md" Discussions = "https://github.com/radiantearth/stac-spec/discussions/categories/stac-software" -[tool.coverage.run] -branch = true -source = ["pystac"] -omit = ["pystac/extensions/label.py"] - -[tool.coverage.report] -fail_under = 90 -exclude_lines = ["if TYPE_CHECKING:"] - -[tool.doc8] -ignore-path = ["docs/_build", "docs/tutorials"] -max-line-length = 88 - [tool.mypy] show_error_codes = true strict = true - -[[tool.mypy.overrides]] -module = ["jinja2"] -ignore_missing_imports = true +files = "src/pystac/**/*.py,tests/**/*.py" [tool.ruff] -line-length = 88 lint.select = ["E", "F", "I"] [tool.pytest.ini_options] filterwarnings = ["error"] -addopts = "--block-network --record-mode=none" - -[tool.setuptools.packages.find] -include = ["pystac*"] -exclude = ["tests*", "benchmarks*"] -[tool.setuptools.dynamic] -version = { attr = "pystac.version.__version__" } +[tool.uv] +default-groups = ["dev", "docs"] [build-system] requires = ["setuptools>=61.0"] diff --git a/pystac/__init__.py b/pystac/__init__.py deleted file mode 100644 index c2a5b53b1..000000000 --- a/pystac/__init__.py +++ /dev/null @@ -1,239 +0,0 @@ -# isort: skip_file -""" -PySTAC is a library for working with SpatioTemporal Asset Catalogs (STACs) -""" - -__all__ = [ - "__version__", - "TemplateError", - "STACError", - "STACTypeError", - "DuplicateObjectKeyError", - "ExtensionAlreadyExistsError", - "ExtensionNotImplemented", - "ExtensionTypeError", - "RequiredPropertyMissing", - "STACValidationError", - "DeprecatedWarning", - "MediaType", - "RelType", - "StacIO", - "STACObject", - "STACObjectType", - "Link", - "HIERARCHICAL_LINKS", - "Catalog", - "CatalogType", - "Collection", - "Extent", - "SpatialExtent", - "TemporalExtent", - "Summaries", - "CommonMetadata", - "RangeSummary", - "Item", - "Asset", - "ItemAssetDefinition", - "ItemCollection", - "Provider", - "ProviderRole", - "read_file", - "read_dict", - "write_file", - "get_stac_version", - "set_stac_version", -] - -import os -import warnings -from typing import Any - -from pystac.errors import ( - TemplateError, - STACError, - STACTypeError, - DuplicateObjectKeyError, - ExtensionAlreadyExistsError, - ExtensionNotImplemented, - ExtensionTypeError, - RequiredPropertyMissing, - STACValidationError, - DeprecatedWarning, -) - -from pystac.version import ( - __version__, - get_stac_version, - set_stac_version, -) -from pystac.media_type import MediaType -from pystac.rel_type import RelType -from pystac.stac_io import StacIO -from pystac.stac_object import STACObject, STACObjectType -from pystac.link import Link, HIERARCHICAL_LINKS -from pystac.catalog import Catalog, CatalogType -from pystac.collection import ( - Collection, - Extent, - SpatialExtent, - TemporalExtent, -) -from pystac.common_metadata import CommonMetadata -from pystac.summaries import RangeSummary, Summaries -from pystac.asset import Asset -from pystac.item import Item -from pystac.item_assets import ItemAssetDefinition -from pystac.item_collection import ItemCollection -from pystac.provider import ProviderRole, Provider -from pystac.utils import HREF -import pystac.validation - -import pystac.extensions.hooks -import pystac.extensions.classification -import pystac.extensions.datacube -import pystac.extensions.eo -import pystac.extensions.file -import pystac.extensions.grid -import pystac.extensions.item_assets - -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - import pystac.extensions.label -import pystac.extensions.mgrs -import pystac.extensions.pointcloud -import pystac.extensions.projection -import pystac.extensions.raster -import pystac.extensions.sar -import pystac.extensions.sat -import pystac.extensions.scientific -import pystac.extensions.storage -import pystac.extensions.table -import pystac.extensions.timestamps -import pystac.extensions.version -import pystac.extensions.view -import pystac.extensions.xarray_assets - -EXTENSION_HOOKS = pystac.extensions.hooks.RegisteredExtensionHooks( - [ - pystac.extensions.classification.CLASSIFICATION_EXTENSION_HOOKS, - pystac.extensions.datacube.DATACUBE_EXTENSION_HOOKS, - pystac.extensions.eo.EO_EXTENSION_HOOKS, - pystac.extensions.file.FILE_EXTENSION_HOOKS, - pystac.extensions.grid.GRID_EXTENSION_HOOKS, - pystac.extensions.item_assets.ITEM_ASSETS_EXTENSION_HOOKS, - pystac.extensions.label.LABEL_EXTENSION_HOOKS, - pystac.extensions.mgrs.MGRS_EXTENSION_HOOKS, - pystac.extensions.pointcloud.POINTCLOUD_EXTENSION_HOOKS, - pystac.extensions.projection.PROJECTION_EXTENSION_HOOKS, - pystac.extensions.raster.RASTER_EXTENSION_HOOKS, - pystac.extensions.sar.SAR_EXTENSION_HOOKS, - pystac.extensions.sat.SAT_EXTENSION_HOOKS, - pystac.extensions.scientific.SCIENTIFIC_EXTENSION_HOOKS, - pystac.extensions.storage.STORAGE_EXTENSION_HOOKS, - pystac.extensions.table.TABLE_EXTENSION_HOOKS, - pystac.extensions.timestamps.TIMESTAMPS_EXTENSION_HOOKS, - pystac.extensions.version.VERSION_EXTENSION_HOOKS, - pystac.extensions.view.VIEW_EXTENSION_HOOKS, - pystac.extensions.xarray_assets.XARRAY_ASSETS_EXTENSION_HOOKS, - ] -) - - -def read_file(href: HREF, stac_io: StacIO | None = None) -> STACObject: - """Reads a STAC object from a file. - - This method will return either a Catalog, a Collection, or an Item based on what - the file contains. - - This is a convenience method for :meth:`StacIO.read_stac_object - ` - - Args: - href : The HREF to read the object from. - stac_io: Optional :class:`~StacIO` instance to use for I/O operations. If not - provided, will use :meth:`StacIO.default` to create an instance. - - Returns: - The specific STACObject implementation class that is represented - by the JSON read from the file located at HREF. - - Raises: - STACTypeError : If the file at ``href`` does not represent a valid - :class:`~pystac.STACObject`. Note that an :class:`~pystac.ItemCollection` - is not a :class:`~pystac.STACObject` and must be read using - :meth:`ItemCollection.from_file ` - """ - if stac_io is None: - stac_io = StacIO.default() - return stac_io.read_stac_object(href) - - -def write_file( - obj: STACObject, - include_self_link: bool = True, - dest_href: HREF | None = None, - stac_io: StacIO | None = None, -) -> None: - """Writes a STACObject to a file. - - This will write only the Catalog, Collection or Item ``obj``. It will not attempt - to write any other objects that are linked to ``obj``; if you'd like functionality - to save off catalogs recursively see :meth:`Catalog.save `. - - This method will write the JSON of the object to the object's assigned "self" link - or to the dest_href if provided. To set the self link, see - :meth:`STACObject.set_self_href `. - - Convenience method for :meth:`STACObject.from_file ` - - Args: - obj : The STACObject to save. - include_self_link : If ``True``, include the ``"self"`` link with this object. - Otherwise, leave out the self link. - dest_href : Optional HREF to save the file to. If ``None``, the object will be - saved to the object's ``"self"`` href. - stac_io: Optional :class:`~StacIO` instance to use for I/O operations. If not - provided, will use :meth:`StacIO.default` to create an instance. - """ - if stac_io is None: - stac_io = StacIO.default() - dest_href = None if dest_href is None else str(os.fspath(dest_href)) - obj.save_object( - include_self_link=include_self_link, dest_href=dest_href, stac_io=stac_io - ) - - -def read_dict( - d: dict[str, Any], - href: str | None = None, - root: Catalog | None = None, - stac_io: StacIO | None = None, -) -> STACObject: - """Reads a :class:`~STACObject` or :class:`~ItemCollection` from a JSON-like dict - representing a serialized STAC object. - - This method will return either a :class:`~Catalog`, :class:`~Collection`, - or :class`~Item` based on the contents of the dict. - - This is a convenience method for either - :meth:`StacIO.stac_object_from_dict `. - - Args: - d : The dict to parse. - href : Optional href that is the file location of the object being - parsed. - root : Optional root of the catalog for this object. - If provided, the root's resolved object cache can be used to search for - previously resolved instances of the STAC object. - stac_io: Optional :class:`~StacIO` instance to use for reading. If ``None``, - the default instance will be used. - - Raises: - STACTypeError : If the ``d`` dictionary does not represent a valid - :class:`~pystac.STACObject`. Note that an :class:`~pystac.ItemCollection` - is not a :class:`~pystac.STACObject` and must be read using - :meth:`ItemCollection.from_dict ` - """ - if stac_io is None: - stac_io = StacIO.default() - return stac_io.stac_object_from_dict(d, href, root) diff --git a/pystac/asset.py b/pystac/asset.py deleted file mode 100644 index 7fbafead9..000000000 --- a/pystac/asset.py +++ /dev/null @@ -1,393 +0,0 @@ -from __future__ import annotations - -import os -import shutil -from copy import copy, deepcopy -from html import escape -from typing import TYPE_CHECKING, Any, Protocol, TypeVar - -from pystac import MediaType, STACError, common_metadata, utils -from pystac.html.jinja_env import get_jinja_env -from pystac.utils import is_absolute_href, make_absolute_href, make_relative_href - -if TYPE_CHECKING: - from pystac.common_metadata import CommonMetadata - from pystac.extensions.ext import AssetExt - -#: Generalized version of :class:`Asset` -A = TypeVar("A", bound="Asset") - - -class Asset: - """An object that contains a link to data associated with an Item or Collection that - can be downloaded or streamed. - - Args: - href : Link to the asset object. Relative and absolute links are both - allowed. - title : Optional displayed title for clients and users. - description : A 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 : Optional description of the media type. Registered Media Types - are preferred. See :class:`~pystac.MediaType` for common media types. - roles : Optional, Semantic roles (i.e. thumbnail, overview, - data, metadata) of the asset. - extra_fields : Optional, additional fields for this asset. This is used - by extensions as a way to serialize and deserialize properties on asset - object JSON. - """ - - href: str - """Link to the asset object. Relative and absolute links are both allowed.""" - - title: str | None - """Optional displayed title for clients and users.""" - - description: str | None - """A 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: str | None - """Optional description of the media type. Registered Media Types are preferred. - See :class:`~pystac.MediaType` for common media types.""" - - roles: list[str] | None - """Optional, Semantic roles (i.e. thumbnail, overview, data, metadata) of the - asset.""" - - owner: Assets | None - """The :class:`~pystac.Item` or :class:`~pystac.Collection` that this asset belongs - to, or ``None`` if it has no owner.""" - - extra_fields: dict[str, Any] - """Optional, additional fields for this asset. This is used by extensions as a - way to serialize and deserialize properties on asset object JSON.""" - - def __init__( - self, - href: str, - title: str | None = None, - description: str | None = None, - media_type: str | None = None, - roles: list[str] | None = None, - extra_fields: dict[str, Any] | None = None, - ) -> None: - self.href = utils.make_posix_style(href) - self.title = title - self.description = description - self.media_type = media_type - self.roles = roles - self.extra_fields = extra_fields or {} - - # The Item which owns this Asset. - self.owner = None - - def set_owner(self, obj: Assets) -> None: - """Sets the owning item of this Asset. - - The owning item will be used to resolve relative HREFs of this asset. - - Args: - obj: The Collection or Item that owns this asset. - """ - self.owner = obj - - def get_absolute_href(self) -> str | None: - """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``. 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 - be determined. - """ - if utils.is_absolute_href(self.href): - return self.href - else: - if self.owner is not 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]: - """Returns this Asset as a dictionary. - - Returns: - dict: A serialization of the Asset. - """ - - d: dict[str, Any] = {"href": self.href} - - if self.media_type is not None: - d["type"] = self.media_type - - if self.title is not None: - d["title"] = self.title - - if self.description is not None: - d["description"] = self.description - - if self.extra_fields is not None and len(self.extra_fields) > 0: - for k, v in self.extra_fields.items(): - d[k] = v - - if self.roles is not None: - d["roles"] = self.roles - - return d - - def clone(self) -> Asset: - """Clones this asset. Makes a ``deepcopy`` of the - :attr:`~pystac.Asset.extra_fields`. - - Returns: - Asset: The clone of this asset. - """ - cls = self.__class__ - return cls( - href=self.href, - title=self.title, - description=self.description, - media_type=self.media_type, - roles=self.roles, - extra_fields=deepcopy(self.extra_fields), - ) - - def has_role(self, role: str) -> bool: - """Check if a role exists in the Asset role list. - - Args: - role: Role to check for existence. - - Returns: - bool: True if role exists, else False. - """ - if self.roles is None: - return False - else: - return role in self.roles - - @property - def common_metadata(self) -> CommonMetadata: - """Access the asset's common metadata fields as a - :class:`~pystac.CommonMetadata` object.""" - return common_metadata.CommonMetadata(self) - - def __repr__(self) -> str: - return f"" - - def _repr_html_(self) -> str: - jinja_env = get_jinja_env() - if jinja_env: - template = jinja_env.get_template("JSON.jinja2") - return str(template.render(dict=self.to_dict())) - else: - return escape(repr(self)) - - @classmethod - def from_dict(cls: type[A], d: dict[str, Any]) -> A: - """Constructs an Asset from a dict. - - Returns: - Asset: The Asset deserialized from the JSON dict. - """ - d = copy(d) - href = d.pop("href") - media_type = d.pop("type", None) - title = d.pop("title", None) - description = d.pop("description", None) - roles = d.pop("roles", None) - properties = None - if any(d): - properties = d - - return cls( - href=href, - media_type=media_type, - title=title, - description=description, - roles=roles, - extra_fields=properties, - ) - - def move(self, href: str) -> Asset: - """Moves this asset's file to a new location on the local filesystem, - setting the asset href accordingly. - - Modifies the asset in place, and returns the same asset. - - Args: - href: The new asset location. Must be a local path. If relative - it must be relative to the owner object. - - Returns: - Asset: The asset with the updated href. - """ - src = _absolute_href(self.href, self.owner, "move") - dst = _absolute_href(href, self.owner, "move") - shutil.move(src, dst) - self.href = href - return self - - def copy(self, href: str) -> Asset: - """Copies this asset's file to a new location on the local filesystem, - setting the asset href accordingly. - - Modifies the asset in place, and returns the same asset. - - Args: - href: The new asset location. Must be a local path. If relative - it must be relative to the owner object. - - Returns: - Asset: The asset with the updated href. - """ - src = _absolute_href(self.href, self.owner, "copy") - dst = _absolute_href(href, self.owner, "copy") - shutil.copy2(src, dst) - self.href = href - return self - - def delete(self) -> None: - """Delete this asset's file. Does not delete the asset from the item - that owns it. See :meth:`~pystac.Item.delete_asset` for that. - - Does not modify the asset. - """ - href = _absolute_href(self.href, self.owner, "delete") - os.remove(href) - - @property - def ext(self) -> AssetExt: - """Accessor for extension classes on this asset - - Example:: - - asset.ext.proj.code = "EPSG:4326" - """ - from pystac.extensions.ext import AssetExt - - return AssetExt(stac_object=self) - - -class Assets(Protocol): - """Protocol, with functionality, for STAC objects that have assets.""" - - assets: dict[str, Asset] - """The asset dictionary.""" - - def get_assets( - self, - media_type: str | MediaType | None = None, - role: str | None = None, - ) -> dict[str, Asset]: - """Get this object's assets. - - Args: - media_type: If set, filter the assets such that only those with a - matching ``media_type`` are returned. - role: If set, filter the assets such that only those with a matching - ``role`` are returned. - - Returns: - Dict[str, Asset]: A dictionary of assets that match ``media_type`` - and/or ``role`` if set or else all of this object's assets. - """ - return { - k: deepcopy(v) - for k, v in self.assets.items() - if (media_type is None or v.media_type == media_type) - and (role is None or v.has_role(role)) - } - - def add_asset(self, key: str, asset: Asset) -> None: - """Adds an Asset to this object. - - Args: - key : The unique key of this asset. - asset : The Asset to add. - """ - asset.set_owner(self) - self.assets[key] = asset - - def delete_asset(self, key: str) -> None: - """Deletes the asset at the given key, and removes the asset's data - file from the local filesystem. - - It is an error to attempt to delete an asset's file if it is on a - remote filesystem. - - To delete the asset without removing the file, use `del item.assets["key"]`. - - Args: - key: The unique key of this asset. - """ - asset = self.assets[key] - asset.set_owner(self) - asset.delete() - - del self.assets[key] - - def make_asset_hrefs_relative(self) -> Assets: - """Modify each asset's HREF to be relative to this object's self HREF. - - Returns: - Item: self - """ - self_href = self.get_self_href() - for asset in self.assets.values(): - if is_absolute_href(asset.href): - if self_href is None: - raise STACError( - "Cannot make asset HREFs relative if no self_href is set." - ) - asset.href = make_relative_href(asset.href, self_href) - return self - - def make_asset_hrefs_absolute(self) -> Assets: - """Modify each asset's HREF to be absolute. - - Any asset HREFs that are relative will be modified to absolute based on this - item's self HREF. - - Returns: - Assets: self - """ - self_href = self.get_self_href() - for asset in self.assets.values(): - if not is_absolute_href(asset.href): - if self_href is None: - raise STACError( - "Cannot make relative asset HREFs absolute " - "if no self_href is set." - ) - asset.href = make_absolute_href(asset.href, self_href) - return self - - def get_self_href(self) -> str | None: - """Abstract definition of STACObject.get_self_href. - - Needed to make the `make_asset_hrefs_{absolute|relative}` methods pass - type checking. Refactoring out all the link behavior in STACObject to - its own protocol would be too heavy, so we just use this stub instead. - """ - ... - - -def _absolute_href(href: str, owner: Assets | None, action: str = "access") -> str: - if utils.is_absolute_href(href): - return href - else: - item_self = owner.get_self_href() if owner else None - if item_self is None: - raise ValueError( - f"Cannot {action} file if asset href ('{href}') is relative " - "and owner item is not set. Hint: try using " - ":func:`~pystac.Item.make_asset_hrefs_absolute`" - ) - return utils.make_absolute_href(href, item_self) diff --git a/pystac/cache.py b/pystac/cache.py deleted file mode 100644 index 9b409c414..000000000 --- a/pystac/cache.py +++ /dev/null @@ -1,349 +0,0 @@ -from __future__ import annotations - -from collections import ChainMap -from copy import copy -from typing import TYPE_CHECKING, Any, cast - -import pystac - -if TYPE_CHECKING: - from pystac.collection import Collection - from pystac.stac_object import STACObject - - -def get_cache_key(stac_object: STACObject) -> tuple[str, bool]: - """Produce a cache key for the given STAC object. - - If a self href is set, use that as the cache key. - If not, use a key that combines this object's ID with - it's parents' IDs. - - Returns: - Tuple[str, bool]: A tuple with the cache key as the first - element and a boolean that is true if the cache key is - the object's HREF as the second element. - """ - href = stac_object.get_self_href() - if href is not None: - return href, True - else: - ids: list[str] = [] - obj: pystac.STACObject | None = stac_object - while obj is not None: - ids.append(obj.id) - obj = obj.get_parent() - return "/".join(ids), False - - -class ResolvedObjectCache: - """This class tracks resolved objects tied to root catalogs. - A STAC object is 'resolved' when it is a Python Object; a link - to a STAC object such as a Catalog or Item is considered "unresolved" - if it's target is pointed at an HREF of the object. - - Tracking resolved objects allows us to tie together the same instances - when there are loops in the Graph of the STAC catalog (e.g. a LabelItem - can link to a rel:source, and if that STAC Item exists in the same - root catalog they should refer to the same Python object). - - Resolution tracking is important when copying STACs in-memory: In order - for object links to refer to the copy of STAC Objects rather than their - originals, we have to keep track of the resolved STAC Objects and replace - them with their copies. - - Args: - id_keys_to_objects : Existing cache of - a key made up of the STACObject and it's parents IDs mapped - to the cached STACObject. - hrefs_to_objects : STAC Object HREFs matched to - their cached object. - ids_to_collections : Map of collection IDs - to collections. - """ - - id_keys_to_objects: dict[str, STACObject] - """Existing cache of a key made up of the STACObject and it's parents IDs mapped - to the cached STACObject.""" - - hrefs_to_objects: dict[str, STACObject] - """STAC Object HREFs matched to their cached object.""" - - ids_to_collections: dict[str, Collection] - """Map of collection IDs to collections.""" - - _collection_cache: ResolvedObjectCollectionCache | None - - def __init__( - self, - id_keys_to_objects: dict[str, STACObject] | None = None, - hrefs_to_objects: dict[str, STACObject] | None = None, - ids_to_collections: dict[str, Collection] | None = None, - ): - self.id_keys_to_objects = id_keys_to_objects or {} - self.hrefs_to_objects = hrefs_to_objects or {} - self.ids_to_collections = ids_to_collections or {} - - self._collection_cache = None - - def get_or_cache(self, obj: STACObject) -> STACObject: - """Gets the STACObject that is the cached version of the given STACObject; or, - if none exists, sets the cached object to the given object. - - Args: - obj : The given object who's cache key will be checked - against the cache. - - Returns: - STACObject: Either the cached object that has the same cache key as the - given object, or the given object. - """ - key, is_href = get_cache_key(obj) - if is_href: - if key in self.hrefs_to_objects: - return self.hrefs_to_objects[key] - else: - self.cache(obj) - return obj - else: - if key in self.id_keys_to_objects: - return self.id_keys_to_objects[key] - else: - self.cache(obj) - return obj - - def get(self, obj: STACObject) -> STACObject | None: - """Get the cached object that has the same cache key as the given object. - - Args: - obj : The given object who's cache key will be checked against - the cache. - - Returns: - STACObject or None: Either the cached object that has the same cache key as - the given object, or None - """ - key, is_href = get_cache_key(obj) - if is_href: - return self.get_by_href(key) - else: - return self.id_keys_to_objects.get(key) - - def get_by_href(self, href: str) -> STACObject | None: - """Gets the cached object at href. - - Args: - href : The href to use as the key for the cached object. - - Returns: - STACObject or None: Returns the STACObject if cached, otherwise None. - """ - return self.hrefs_to_objects.get(href) - - def get_collection_by_id(self, id: str) -> Collection | None: - """Retrieved a cached Collection by its ID. - - Args: - id : The ID of the collection. - - Returns: - Collection or None: Returns the collection if there is one cached - with the given ID, otherwise None. - """ - return self.ids_to_collections.get(id) - - def cache(self, obj: STACObject) -> None: - """Set the given object into the cache. - - Args: - obj : The object to cache - """ - key, is_href = get_cache_key(obj) - if is_href: - self.hrefs_to_objects[key] = obj - else: - self.id_keys_to_objects[key] = obj - - if isinstance(obj, pystac.Collection): - self.ids_to_collections[obj.id] = obj - - def remove(self, obj: STACObject) -> None: - """Removes any cached object that matches the given object's cache key. - - Args: - obj : The object to remove - """ - key, is_href = get_cache_key(obj) - - if is_href: - self.hrefs_to_objects.pop(key, None) - else: - self.id_keys_to_objects.pop(key, None) - - if obj.STAC_OBJECT_TYPE == pystac.STACObjectType.COLLECTION: - self.id_keys_to_objects.pop(obj.id, None) - - def __contains__(self, obj: STACObject) -> bool: - key, is_href = get_cache_key(obj) - return ( - key in self.hrefs_to_objects if is_href else key in self.id_keys_to_objects - ) - - def contains_collection_id(self, collection_id: str) -> bool: - """Returns True if there is a collection with given collection ID is cached.""" - return collection_id in self.ids_to_collections - - def as_collection_cache(self) -> CollectionCache: - if self._collection_cache is None: - self._collection_cache = ResolvedObjectCollectionCache(self) - return self._collection_cache - - @staticmethod - def merge( - first: ResolvedObjectCache, second: ResolvedObjectCache - ) -> ResolvedObjectCache: - """Merges two ResolvedObjectCache. - - The merged cache will give preference to the first argument; that is, if there - are cached keys that exist in both the first and second cache, the object cached - in the first will be cached in the resulting merged ResolvedObjectCache. - - Args: - first : The first cache to merge. This cache will be - the preferred cache for objects in the case of ID conflicts. - second : The second cache to merge. - - Returns: - ResolvedObjectCache: The resulting merged cache. - """ - merged = ResolvedObjectCache( - id_keys_to_objects=dict( - ChainMap( - copy(first.id_keys_to_objects), copy(second.id_keys_to_objects) - ) - ), - hrefs_to_objects=dict( - ChainMap(copy(first.hrefs_to_objects), copy(second.hrefs_to_objects)) - ), - ids_to_collections=dict( - ChainMap( - copy(first.ids_to_collections), copy(second.ids_to_collections) - ) - ), - ) - - merged._collection_cache = ResolvedObjectCollectionCache.merge( - merged, first._collection_cache, second._collection_cache - ) - - return merged - - -class CollectionCache: - """Cache of collections that can be used to avoid re-reading Collection - JSON in :func:`pystac.serialization.merge_common_properties - `. - The CollectionCache will contain collections as either as dicts or PySTAC - Collections, and will set Collection JSON that it reads in order to merge - in common properties. - """ - - cached_ids: dict[str, Collection | dict[str, Any]] - cached_hrefs: dict[str, Collection | dict[str, Any]] - - def __init__( - self, - cached_ids: dict[str, Collection | dict[str, Any]] | None = None, - cached_hrefs: dict[str, Collection | dict[str, Any]] | None = None, - ): - self.cached_ids = cached_ids or {} - self.cached_hrefs = cached_hrefs or {} - - def get_by_id(self, collection_id: str) -> Collection | dict[str, Any] | None: - return self.cached_ids.get(collection_id) - - def get_by_href(self, href: str) -> Collection | dict[str, Any] | None: - return self.cached_hrefs.get(href) - - def contains_id(self, collection_id: str) -> bool: - return collection_id in self.cached_ids - - def cache( - self, - collection: Collection | dict[str, Any], - href: str | None = None, - ) -> None: - """Caches a collection JSON.""" - if isinstance(collection, pystac.Collection): - self.cached_ids[collection.id] = collection - else: - self.cached_ids[collection["id"]] = collection - - if href is not None: - self.cached_hrefs[href] = collection - - -class ResolvedObjectCollectionCache(CollectionCache): - resolved_object_cache: ResolvedObjectCache - - def __init__( - self, - resolved_object_cache: ResolvedObjectCache, - cached_ids: dict[str, Collection | dict[str, Any]] | None = None, - cached_hrefs: dict[str, Collection | dict[str, Any]] | None = None, - ): - super().__init__(cached_ids, cached_hrefs) - self.resolved_object_cache = resolved_object_cache - - def get_by_id(self, collection_id: str) -> Collection | dict[str, Any] | None: - result = self.resolved_object_cache.get_collection_by_id(collection_id) - if result is None: - return super().get_by_id(collection_id) - else: - return result - - def get_by_href(self, href: str) -> Collection | dict[str, Any] | None: - result = self.resolved_object_cache.get_by_href(href) - if result is None: - return super().get_by_href(href) - else: - return cast(pystac.Collection, result) - - def contains_id(self, collection_id: str) -> bool: - return self.resolved_object_cache.contains_collection_id( - collection_id - ) or super().contains_id(collection_id) - - def cache( - self, - collection: Collection | dict[str, Any], - href: str | None = None, - ) -> None: - super().cache(collection, href) - - @staticmethod - def merge( - resolved_object_cache: ResolvedObjectCache, - first: ResolvedObjectCollectionCache | None, - second: ResolvedObjectCollectionCache | None, - ) -> ResolvedObjectCollectionCache: - first_cached_ids = {} - if first is not None: - first_cached_ids = copy(first.cached_ids) - - second_cached_ids = {} - if second is not None: - second_cached_ids = copy(second.cached_ids) - - first_cached_hrefs = {} - if first is not None: - first_cached_hrefs = copy(first.cached_hrefs) - - second_cached_hrefs = {} - if second is not None: - second_cached_hrefs = copy(second.cached_hrefs) - - return ResolvedObjectCollectionCache( - resolved_object_cache, - cached_ids=dict(ChainMap(first_cached_ids, second_cached_ids)), - cached_hrefs=dict(ChainMap(first_cached_hrefs, second_cached_hrefs)), - ) diff --git a/pystac/catalog.py b/pystac/catalog.py deleted file mode 100644 index ae47787c0..000000000 --- a/pystac/catalog.py +++ /dev/null @@ -1,1302 +0,0 @@ -from __future__ import annotations - -import os -import warnings -from collections.abc import Callable, Iterable, Iterator -from copy import deepcopy -from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - TypeVar, - cast, -) - -import pystac -import pystac.media_type -from pystac.cache import ResolvedObjectCache -from pystac.errors import STACError, STACTypeError -from pystac.layout import ( - APILayoutStrategy, - BestPracticesLayoutStrategy, - HrefLayoutStrategy, - LayoutTemplate, -) -from pystac.link import Link -from pystac.serialization import ( - identify_stac_object, - identify_stac_object_type, - migrate_to_latest, -) -from pystac.stac_object import STACObject, STACObjectType -from pystac.utils import ( - HREF, - StringEnum, - _is_url, - is_absolute_href, - make_absolute_href, - make_relative_href, -) - -if TYPE_CHECKING: - from pystac.asset import Asset - from pystac.collection import Collection - from pystac.extensions.ext import CatalogExt - from pystac.item import Item - -#: Generalized version of :class:`Catalog` -C = TypeVar("C", bound="Catalog") - - -class CatalogType(StringEnum): - SELF_CONTAINED = "SELF_CONTAINED" - """A 'self-contained catalog' is one that is designed for portability. - Users may want to download an online catalog from and be able to use it on their - local computer, so all links need to be relative. - - See: - :stac-spec:`The best practices documentation on self-contained catalogs - ` - """ - - ABSOLUTE_PUBLISHED = "ABSOLUTE_PUBLISHED" - """ - Absolute Published Catalog is a catalog that uses absolute links for everything, - both in the links objects and in the asset hrefs. - - See: - :stac-spec:`The best practices documentation on published catalogs - ` - """ - - RELATIVE_PUBLISHED = "RELATIVE_PUBLISHED" - """ - Relative Published Catalog is a catalog that uses relative links for everything, but - includes an absolute self link at the root catalog, to identify its online location. - - See: - :stac-spec:`The best practices documentation on published catalogs - ` - """ - - @classmethod - def determine_type(cls, stac_json: dict[str, Any]) -> CatalogType | None: - """Determines the catalog type based on a STAC JSON dict. - - Only applies to Catalogs or Collections - - Args: - stac_json : The STAC JSON dict to determine the catalog type - - Returns: - Optional[CatalogType]: The catalog type of the catalog or collection. - Will return None if it cannot be determined. - """ - self_link = None - relative = False - for link in stac_json["links"]: - if link["rel"] == pystac.RelType.SELF: - self_link = link - else: - relative |= not is_absolute_href(link["href"]) - - if self_link: - if relative: - return cls.RELATIVE_PUBLISHED - else: - return cls.ABSOLUTE_PUBLISHED - else: - if relative: - return cls.SELF_CONTAINED - else: - return None - - -class Catalog(STACObject): - """A PySTAC Catalog represents a STAC catalog in memory. - - A Catalog is a :class:`~pystac.STACObject` that may contain children, - which are instances of :class:`~pystac.Catalog` or :class:`~pystac.Collection`, - as well as :class:`~pystac.Item` s. - - Args: - id : Identifier for the catalog. Must be unique within the STAC. - description : Detailed multi-line description to fully explain the catalog. - `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. - href : Optional HREF for this catalog, which be set as the - catalog's self link's HREF. - catalog_type : Optional catalog type for this catalog. Must - be one of the values in :class:`~pystac.CatalogType`. - strategy : The layout strategy to use for setting the - HREFs of the catalog child objects and items. - If not provided, it will default to the strategy of the root and fallback to - :class:`~pystac.layout.BestPracticesLayoutStrategy`. - """ - - catalog_type: CatalogType - """The catalog type. Defaults to :attr:`CatalogType.ABSOLUTE_PUBLISHED`.""" - - description: str - """Detailed multi-line description to fully explain the catalog.""" - - extra_fields: dict[str, Any] - """Extra fields that are part of the top-level JSON properties of the Catalog.""" - - id: str - """Identifier for the catalog.""" - - links: list[Link] - """A list of :class:`~pystac.Link` objects representing all links associated with - this Catalog.""" - - title: str | None - """Optional short descriptive one-line title for the catalog.""" - - stac_extensions: list[str] - """List of extensions the Catalog implements.""" - - _resolved_objects: ResolvedObjectCache - - STAC_OBJECT_TYPE = pystac.STACObjectType.CATALOG - - _stac_io: pystac.StacIO | None = None - """Optional instance of StacIO that will be used by default - for any IO operations on objects contained by this catalog. - Set while reading in a catalog. This is set when a catalog - is read by a StacIO instance.""" - - DEFAULT_FILE_NAME = "catalog.json" - """Default file name that will be given to this STAC object in - a canonical format. - """ - - _fallback_strategy: HrefLayoutStrategy = BestPracticesLayoutStrategy() - """Fallback layout strategy""" - - def __init__( - self, - id: str, - description: str, - title: str | None = None, - stac_extensions: list[str] | None = None, - extra_fields: dict[str, Any] | None = None, - href: str | None = None, - catalog_type: CatalogType = CatalogType.ABSOLUTE_PUBLISHED, - strategy: HrefLayoutStrategy | None = None, - ): - super().__init__(stac_extensions or []) - - self.id = id - self.description = description - self.title = title - if extra_fields is None: - self.extra_fields = {} - else: - self.extra_fields = extra_fields - - self._resolved_objects = ResolvedObjectCache() - - self.add_link(Link.root(self)) - - if href is not None: - self.set_self_href(href) - - self.catalog_type: CatalogType = catalog_type - - self.strategy: HrefLayoutStrategy | None = strategy - - self._resolved_objects.cache(self) - - def __repr__(self) -> str: - return f"" - - def set_root(self, root: Catalog | None) -> None: - STACObject.set_root(self, root) - if root is not None: - root._resolved_objects = ResolvedObjectCache.merge( - root._resolved_objects, self._resolved_objects - ) - - # Walk through resolved object links and update the root - for link in self.links: - if link.rel == pystac.RelType.CHILD or link.rel == pystac.RelType.ITEM: - target = link.target - if isinstance(target, STACObject): - target.set_root(root) - - def is_relative(self) -> bool: - return self.catalog_type in [ - CatalogType.RELATIVE_PUBLISHED, - CatalogType.SELF_CONTAINED, - ] - - def _get_strategy(self, strategy: HrefLayoutStrategy | None) -> HrefLayoutStrategy: - if strategy is not None: - return strategy - elif self.strategy is not None: - return self.strategy - elif root := self.get_root(): - if root.strategy is not None: - return root.strategy - else: - return root._fallback_strategy - else: - return self._fallback_strategy - - def add_child( - self, - child: Catalog | Collection, - title: str | None = None, - strategy: HrefLayoutStrategy | None = None, - set_parent: bool = True, - ) -> Link: - """Adds a link to a child :class:`~pystac.Catalog` or - :class:`~pystac.Collection`. - - This method will set the child's parent to this object and potentially - override its self_link (unless ``set_parent`` is False). - - It will always set its root to this Catalog's root. - - Args: - child : The child to add. - title : Optional title to give to the :class:`~pystac.Link` - strategy : The layout strategy to use for setting the - self href of the child. If not provided, defaults to - the layout strategy of the parent or root and falls back to - :class:`~pystac.layout.BestPracticesLayoutStrategy`. - set_parent : Whether to set the parent on the child as well. - Defaults to True. - - Returns: - Link: The link created for the child - """ - - # Prevent typo confusion - if isinstance(child, pystac.Item): - raise pystac.STACError("Cannot add item as child. Use add_item instead.") - - strategy = self._get_strategy(strategy) - - child.set_root(self.get_root()) - if set_parent: - child.set_parent(self) - else: - child._allow_parent_to_override_href = False - - # set self link - self_href = self.get_self_href() - if self_href and set_parent: - child_href = strategy.get_href(child, self_href) - child.set_self_href(child_href) - - child_link = Link.child(child, title=title) - self.add_link(child_link) - return child_link - - def add_children( - self, - children: Iterable[Catalog | Collection], - strategy: HrefLayoutStrategy | None = None, - ) -> list[Link]: - """Adds links to multiple :class:`~pystac.Catalog` or `~pystac.Collection` - objects. This method will set each child's parent to this object, and their - root to this Catalog's root. - - Args: - children : The children to add. - strategy : The layout strategy to use for setting the - self href of the children. If not provided, defaults to - the layout strategy of the parent or root and falls back to - :class:`~pystac.layout.BestPracticesLayoutStrategy`. - - Returns: - List[Link]: An array of links created for the children - """ - return [self.add_child(child, strategy=strategy) for child in children] - - def add_item( - self, - item: Item, - title: str | None = None, - strategy: HrefLayoutStrategy | None = None, - set_parent: bool = True, - ) -> Link: - """Adds a link to an :class:`~pystac.Item`. - - This method will set the item's parent to this object and potentially - override its self_link (unless ``set_parent`` is False) - - It will always set its root to this Catalog's root. - - Args: - item : The item to add. - title : Optional title to give to the :class:`~pystac.Link` - strategy : The layout strategy to use for setting the - self href of the item. If not provided, defaults to - the layout strategy of the parent or root and falls back to - :class:`~pystac.layout.BestPracticesLayoutStrategy`. - set_parent : Whether to set the parent on the item as well. - Defaults to True. - - Returns: - Link: The link created for the item - """ - - # Prevent typo confusion - if isinstance(item, pystac.Catalog): - raise pystac.STACError("Cannot add catalog as item. Use add_child instead.") - - strategy = self._get_strategy(strategy) - - item.set_root(self.get_root()) - if set_parent: - item.set_parent(self) - else: - item._allow_parent_to_override_href = False - - # set self link - self_href = self.get_self_href() - if self_href and set_parent: - item_href = strategy.get_href(item, self_href) - item.set_self_href(item_href) - - item_link = Link.item(item, title=title) - self.add_link(item_link) - return item_link - - def add_items( - self, - items: Iterable[Item], - strategy: HrefLayoutStrategy | None = None, - ) -> list[Link]: - """Adds links to multiple :class:`Items `. - - This method will set each item's parent to this object, and their root to - this Catalog's root. - - Args: - items : The items to add. - strategy : The layout strategy to use for setting the - self href of the items. If not provided, defaults to - the layout strategy of the parent or root and falls back to - :class:`~pystac.layout.BestPracticesLayoutStrategy`. - - Returns: - List[Link]: A list of links created for the item - """ - return [self.add_item(item, strategy=strategy) for item in items] - - def get_child( - self, id: str, recursive: bool = False, sort_links_by_id: bool = True - ) -> Catalog | Collection | None: - """Gets the child of this catalog with the given ID, if it exists. - - Args: - id : The ID of the child to find. - recursive : If True, search this catalog and all children for the - item; otherwise, only search the children of this catalog. Defaults - to False. - sort_links_by_id : If True, links containing the ID will be checked - first. If links do not contain the ID then setting this to False - will improve performance. Defaults to True. - - Return: - Catalog or Collection or None: The child with the given ID, - or None if not found. - """ - if not recursive: - children: Iterable[pystac.Catalog | pystac.Collection] - if not sort_links_by_id: - children = self.get_children() - else: - - def sort_function(links: list[Link]) -> list[Link]: - return sorted( - links, - key=lambda x: (href := x.get_href()) is None or id not in href, - ) - - children = map( - lambda x: cast(pystac.Catalog | pystac.Collection, x), - self.get_stac_objects( - pystac.RelType.CHILD, modify_links=sort_function - ), - ) - return next((c for c in children if c.id == id), None) - else: - for root, _, _ in self.walk(): - child = root.get_child(id, recursive=False) - if child is not None: - return child - return None - - def get_children(self) -> Iterable[Catalog | Collection]: - """Return all children of this catalog. - - Return: - Iterable[Catalog or Collection]: Iterable of children who's parent - is this catalog. - """ - return map( - lambda x: cast(pystac.Catalog | pystac.Collection, x), - self.get_stac_objects(pystac.RelType.CHILD), - ) - - def get_collections(self) -> Iterable[Collection]: - """Return all children of this catalog that are :class:`~pystac.Collection` - instances.""" - return map( - lambda x: cast(pystac.Collection, x), - self.get_stac_objects(pystac.RelType.CHILD, pystac.Collection), - ) - - def get_all_collections(self) -> Iterable[Collection]: - """Get all collections from this catalog and all subcatalogs. Will traverse - any subcatalogs recursively.""" - yield from self.get_collections() - for child in self.get_children(): - yield from child.get_all_collections() - - def get_child_links(self) -> list[Link]: - """Return all child links of this catalog. - - Return: - List[Link]: List of links of this catalog with ``rel == 'child'`` - """ - return self.get_links( - rel=pystac.RelType.CHILD, - media_type=pystac.media_type.STAC_JSON, - ) - - def clear_children(self) -> None: - """Removes all children from this catalog. - - Return: - Catalog: Returns ``self`` - """ - child_ids = [child.id for child in self.get_children()] - for child_id in child_ids: - self.remove_child(child_id) - - def remove_child(self, child_id: str) -> None: - """Removes an child from this catalog. - - Args: - child_id : The ID of the child to remove. - """ - new_links: list[pystac.Link] = [] - root = self.get_root() - for link in self.links: - if link.rel != pystac.RelType.CHILD: - new_links.append(link) - else: - link.resolve_stac_object(root=root) - child = cast("Catalog", link.target) - if child.id != child_id: - new_links.append(link) - else: - child.set_parent(None) - child.set_root(None) - self.links = new_links - - def get_item(self, id: str, recursive: bool = False) -> Item | None: - """ - DEPRECATED. - - .. deprecated:: 1.8 - Use ``next(pystac.Catalog.get_items(id), None)`` instead. - - Returns an item with a given ID. - - Args: - id : The ID of the item to find. - recursive : If True, search this catalog and all children for the - item; otherwise, only search the items of this catalog. Defaults - to False. - - Return: - Item or None: The item with the given ID, or None if not found. - """ - warnings.warn( - "get_item is deprecated and will be removed in v2. " - "Use next(self.get_items(id), None) instead", - DeprecationWarning, - ) - if not recursive: - return next((i for i in self.get_items() if i.id == id), None) - else: - for root, _, _ in self.walk(): - item = root.get_item(id, recursive=False) - if item is not None: - return item - return None - - def get_items(self, *ids: str, recursive: bool = False) -> Iterator[Item]: - """Return all items or specific items of this catalog. - - Args: - *ids : The IDs of the items to include. - recursive : If True, search this catalog and all children for the - item; otherwise, only search the items of this catalog. Defaults - to False. - - Return: - Iterator[Item]: Generator of items whose parent is this catalog, and - (if recursive) all catalogs or collections connected to this catalog - through child links. - """ - items: Iterator[Item] - if not recursive: - items = map( - lambda x: cast(pystac.Item, x), - self.get_stac_objects(pystac.RelType.ITEM), - ) - else: - items = chain( - self.get_items(recursive=False), - *(child.get_items(recursive=True) for child in self.get_children()), - ) - if ids: - yield from (i for i in items if i.id in ids) - else: - yield from items - - def clear_items(self) -> None: - """Removes all items from this catalog. - - Return: - Catalog: Returns ``self`` - """ - for link in self.get_item_links(): - if link.is_resolved(): - item = cast(pystac.Item, link.target) - item.set_parent(None) - item.set_root(None) - - self.links = [link for link in self.links if link.rel != pystac.RelType.ITEM] - - def remove_item(self, item_id: str) -> None: - """Removes an item from this catalog. - - Args: - item_id : The ID of the item to remove. - """ - new_links: list[pystac.Link] = [] - root = self.get_root() - for link in self.links: - if link.rel != pystac.RelType.ITEM: - new_links.append(link) - else: - link.resolve_stac_object(root=root) - item = cast(pystac.Item, link.target) - if item.id != item_id: - new_links.append(link) - else: - item.set_parent(None) - item.set_root(None) - self.links = new_links - - def get_all_items(self) -> Iterator[Item]: - """ - DEPRECATED. - - .. deprecated:: 1.8 - Use ``pystac.Catalog.get_items(recursive=True)`` instead. - - Get all items from this catalog and all subcatalogs. Will traverse - any subcatalogs recursively. - - Returns: - Generator[Item]: All items that belong to this catalog, and all - catalogs or collections connected to this catalog through - child links. - """ - warnings.warn( - "get_all_items is deprecated and will be removed in v2", - DeprecationWarning, - ) - return chain( - self.get_items(), - *(child.get_items(recursive=True) for child in self.get_children()), - ) - - def get_item_links(self) -> list[Link]: - """Return all item links of this catalog. - - Return: - List[Link]: List of links of this catalog with ``rel == 'item'`` - """ - return self.get_links( - rel=pystac.RelType.ITEM, media_type=pystac.media_type.STAC_JSON - ) - - def to_dict( - self, include_self_link: bool = True, transform_hrefs: bool = True - ) -> dict[str, Any]: - links = [ - x - for x in self.links - if x.rel != pystac.RelType.ROOT or x.get_href(transform_hrefs) is not None - ] - if not include_self_link: - links = [x for x in links if x.rel != pystac.RelType.SELF] - - d: dict[str, Any] = { - "type": self.STAC_OBJECT_TYPE.value.title(), - "id": self.id, - "stac_version": pystac.get_stac_version(), - "description": self.description, - "links": [link.to_dict(transform_href=transform_hrefs) for link in links], - } - - if self.stac_extensions: - d["stac_extensions"] = self.stac_extensions - - for key in self.extra_fields: - d[key] = self.extra_fields[key] - - if self.title is not None: - d["title"] = self.title - - return d - - def clone(self) -> Catalog: - cls = self.__class__ - clone = cls( - id=self.id, - description=self.description, - title=self.title, - stac_extensions=self.stac_extensions.copy(), - extra_fields=deepcopy(self.extra_fields), - catalog_type=self.catalog_type, - ) - clone._resolved_objects.cache(clone) - - for link in self.links: - if link.rel == pystac.RelType.ROOT: - # Catalog __init__ sets correct root to clone; don't reset - # if the root link points to self - root_is_self = link.is_resolved() and link.target is self - if not root_is_self: - clone.set_root(None) - clone.add_link(link.clone()) - else: - clone.add_link(link.clone()) - - return clone - - def make_all_asset_hrefs_relative(self) -> None: - """Recursively makes all the HREFs of assets in this catalog relative""" - for item in self.get_items(recursive=True): - item.make_asset_hrefs_relative() - for collection in self.get_all_collections(): - collection.make_asset_hrefs_relative() - - def make_all_asset_hrefs_absolute(self) -> None: - """Recursively makes all the HREFs of assets in this catalog absolute""" - for item in self.get_items(recursive=True): - item.make_asset_hrefs_absolute() - for collection in self.get_all_collections(): - collection.make_asset_hrefs_absolute() - - def normalize_and_save( - self, - root_href: str, - catalog_type: CatalogType | None = None, - strategy: HrefLayoutStrategy | None = None, - stac_io: pystac.StacIO | None = None, - skip_unresolved: bool = False, - ) -> None: - """Normalizes link HREFs to the given root_href, and saves the catalog. - - This is a convenience method that simply calls :func:`Catalog.normalize_hrefs - ` and :func:`Catalog.save ` - in sequence. - - Args: - root_href : The absolute HREF that all links will be normalized - against. - catalog_type : The catalog type that dictates the structure of - the catalog to save. Use a member of :class:`~pystac.CatalogType`. - Defaults to the root catalog.catalog_type or the current catalog - catalog_type if there is no root catalog. - strategy : The layout strategy to use in setting the - HREFS for this catalog. If not provided, defaults to - the layout strategy of the parent or root and falls back to - :class:`~pystac.layout.BestPracticesLayoutStrategy` - stac_io : Optional instance of :class:`~pystac.StacIO` to use. If not - provided, will use the instance set while reading in the catalog, - or the default instance if this is not available. - skip_unresolved : Skip unresolved links when normalizing the tree. - Defaults to False. Because unresolved links are not saved, this - argument can be used to normalize and save only newly-added - objects. - """ - self.normalize_hrefs( - root_href, strategy=strategy, skip_unresolved=skip_unresolved - ) - self.save(catalog_type, stac_io=stac_io) - - def normalize_hrefs( - self, - root_href: str, - strategy: HrefLayoutStrategy | None = None, - skip_unresolved: bool = False, - ) -> None: - """Normalize HREFs will regenerate all link HREFs based on - an absolute root_href and the canonical catalog layout as specified - in the STAC specification's best practices. - - This method mutates the entire catalog tree, unless ``skip_unresolved`` - is True, in which case only resolved links are modified. This is useful - in the case when you have loaded a large catalog and you've added a few - items/children, and you only want to update those newly-added objects, - not the whole tree. - - Args: - root_href : The absolute HREF that all links will be normalized against. - strategy : The layout strategy to use in setting the HREFS - for this catalog. If not provided, defaults to - the layout strategy of the parent or root and falls back to - :class:`~pystac.layout.BestPracticesLayoutStrategy` - skip_unresolved : Skip unresolved links when normalizing the tree. - Defaults to False. - - See: - :stac-spec:`STAC best practices document ` - for the canonical layout of a STAC. - """ - - _strategy = self._get_strategy(strategy) - - # Normalizing requires an absolute path - if not is_absolute_href(root_href): - root_href = make_absolute_href(root_href, os.getcwd(), start_is_dir=True) - - if isinstance(_strategy, APILayoutStrategy) and not _is_url(root_href): - raise STACError("When using APILayoutStrategy the root_href must be a URL") - - def process_item( - item: Item, _root_href: str, is_root: bool, parent: Catalog | None - ) -> Callable[[], None] | None: - if not skip_unresolved: - item.resolve_links() - - # Abort as the intended parent is not the actual parent - # https://github.com/stac-utils/pystac/issues/1116 - if parent is not None and item.get_parent() != parent: - return None - - new_self_href = _strategy.get_href(item, _root_href, is_root) - - def fn() -> None: - item.set_self_href(new_self_href) - - return fn - - def process_catalog( - cat: Catalog, - _root_href: str, - is_root: bool, - parent: Catalog | None = None, - ) -> list[Callable[[], None]]: - setter_funcs: list[Callable[[], None]] = [] - - if not skip_unresolved: - cat.resolve_links() - - # Abort as the intended parent is not the actual parent - # https://github.com/stac-utils/pystac/issues/1116 - if parent is not None and cat.get_parent() != parent: - return setter_funcs - - new_self_href = _strategy.get_href(cat, _root_href, is_root) - new_root = new_self_href - - for link in cat.get_links(): - if skip_unresolved and not link.is_resolved(): - continue - elif link.rel == pystac.RelType.ITEM: - link.resolve_stac_object(root=self.get_root()) - item_fn = process_item( - cast(pystac.Item, link.target), new_root, is_root, cat - ) - if item_fn is not None: - setter_funcs.append(item_fn) - elif link.rel == pystac.RelType.CHILD: - link.resolve_stac_object(root=self.get_root()) - setter_funcs.extend( - process_catalog( - cast(pystac.Catalog | pystac.Collection, link.target), - new_root, - is_root=False, - parent=cat, - ) - ) - - def fn() -> None: - cat.set_self_href(new_self_href) - - setter_funcs.append(fn) - - return setter_funcs - - # Collect functions that will actually mutate the objects. - # Delay mutation as setting hrefs while walking the catalog - # can result in bad links. - setter_funcs = process_catalog(self, root_href, is_root=True) - - for fn in setter_funcs: - fn() - - def generate_subcatalogs( - self, - template: str, - defaults: dict[str, Any] | None = None, - parent_ids: list[str] | None = None, - ) -> list[Catalog]: - """Walks through the catalog and generates subcatalogs - for items based on the template string. - - See :class:`~pystac.layout.LayoutTemplate` - for details on the construction of template strings. This template string - will be applied to the items, and subcatalogs will be created that separate - and organize the items based on template values. - - Args: - template : A template string that - can be consumed by a :class:`~pystac.layout.LayoutTemplate` - defaults : Default values for the template variables - that will be used if the property cannot be found on - the item. - parent_ids : Optional list of the parent catalogs' - identifiers. If the bottom-most subcatalogs already match the - template, no subcatalog is added. - - Returns: - list[Catalog]: List of new catalogs created - """ - result: list[Catalog] = [] - parent_ids = parent_ids or list() - parent_ids.append(self.id) - for child in self.get_children(): - result.extend( - child.generate_subcatalogs( - template, defaults=defaults, parent_ids=parent_ids.copy() - ) - ) - - layout_template = LayoutTemplate(template, defaults=defaults) - - keep_item_links: list[Link] = [] - item_links = [lk for lk in self.links if lk.rel == pystac.RelType.ITEM] - for link in item_links: - link.resolve_stac_object(root=self.get_root()) - item = cast(pystac.Item, link.target) - subcat_ids = layout_template.substitute(item).split("/") - id_iter = reversed(parent_ids) - if all([f"{id}" == next(id_iter, None) for id in reversed(subcat_ids)]): - # Skip items for which the sub-catalog structure already - # matches the template. The list of parent IDs can include more - # elements on the root side, so compare the reversed sequences. - keep_item_links.append(link) - continue - curr_parent = self - for subcat_id in subcat_ids: - subcat = curr_parent.get_child(subcat_id) - if subcat is None: - subcat_desc = "Catalog of items from {} with id {}".format( - curr_parent.id, subcat_id - ) - subcat = pystac.Catalog(id=subcat_id, description=subcat_desc) - curr_parent.add_child(subcat) - result.append(subcat) - curr_parent = subcat - - # resolve collection link so when added back points to correct location - col_link = item.get_single_link(pystac.RelType.COLLECTION) - if col_link is not None: - col_link.resolve_stac_object() - - curr_parent.add_item(item) - - # keep only non-item links and item links that have not been moved elsewhere - self.links = [ - lk for lk in self.links if lk.rel != pystac.RelType.ITEM - ] + keep_item_links - - return result - - def save( - self, - catalog_type: CatalogType | None = None, - dest_href: str | None = None, - stac_io: pystac.StacIO | None = None, - ) -> None: - """Save this catalog and all it's children/item to files determined by the - object's self link HREF or a specified path. - - Args: - catalog_type : The catalog type that dictates the structure of - the catalog to save. Use a member of :class:`~pystac.CatalogType`. - If not supplied, the catalog_type of this catalog will be used. - If that attribute is not set, an exception will be raised. - dest_href : The location where the catalog is to be saved. - If not supplied, the catalog's self link HREF is used to determine - the location of the catalog file and children's files. - stac_io : Optional instance of :class:`~pystac.StacIO` to use. If not - provided, will use the instance set while reading in the catalog, - or the default instance if this is not available. - Note: - If the catalog type is ``CatalogType.ABSOLUTE_PUBLISHED``, - all self links will be included, and hierarchical links be absolute URLs. - If the catalog type is ``CatalogType.RELATIVE_PUBLISHED``, this catalog's - self link will be included, but no child catalog will have self links, and - hierarchical links will be relative URLs - If the catalog type is ``CatalogType.SELF_CONTAINED``, no self links will - be included and hierarchical links will be relative URLs. - """ - - root = self.get_root() - if root is None: - raise Exception("There is no root catalog") - - if catalog_type is not None: - root.catalog_type = catalog_type - - items_include_self_link = root.catalog_type in [CatalogType.ABSOLUTE_PUBLISHED] - - for child_link in self.get_child_links(): - if child_link.is_resolved(): - child = cast(Catalog, child_link.target) - if dest_href is not None: - rel_href = make_relative_href(child.self_href, self.self_href) - child_dest_href = make_absolute_href( - rel_href, dest_href, start_is_dir=True - ) - child.save( - dest_href=os.path.dirname(child_dest_href), - stac_io=stac_io, - ) - else: - child.save(stac_io=stac_io) - - for item_link in self.get_item_links(): - if item_link.is_resolved(): - item = cast(pystac.Item, item_link.target) - if dest_href is not None: - rel_href = make_relative_href(item.self_href, self.self_href) - item_dest_href = make_absolute_href( - rel_href, dest_href, start_is_dir=True - ) - item.save_object( - include_self_link=items_include_self_link, - dest_href=item_dest_href, - stac_io=stac_io, - ) - else: - item.save_object( - include_self_link=items_include_self_link, stac_io=stac_io - ) - - include_self_link = False - # include a self link if this is the root catalog - # or if ABSOLUTE_PUBLISHED catalog - if root.catalog_type == CatalogType.ABSOLUTE_PUBLISHED: - include_self_link = True - elif root.catalog_type != CatalogType.SELF_CONTAINED: - root_link = self.get_root_link() - if root_link and root_link.get_absolute_href() == self.get_self_href(): - include_self_link = True - - catalog_dest_href = None - if dest_href is not None: - rel_href = make_relative_href(self.self_href, self.self_href) - catalog_dest_href = make_absolute_href( - rel_href, dest_href, start_is_dir=True - ) - self.save_object( - include_self_link=include_self_link, - dest_href=catalog_dest_href, - stac_io=stac_io, - ) - if catalog_type is not None: - self.catalog_type = catalog_type - - def walk( - self, - ) -> Iterable[tuple[Catalog, Iterable[Catalog], Iterable[Item]]]: - """Walks through children and items of catalogs. - - For each catalog in the STAC's tree rooted at this catalog (including this - catalog itself), it yields a 3-tuple (root, subcatalogs, items). The root in - that 3-tuple refers to the current catalog being walked, the subcatalogs are any - catalogs or collections for which the root is a parent, and items represents - any items that have the root as a parent. - - This has similar functionality to Python's :func:`os.walk`. - - Returns: - Generator[(Catalog, Generator[Catalog], Generator[Item])]: A generator that - yields a 3-tuple (parent_catalog, children, items). - """ - children = self.get_children() - items = self.get_items() - - yield self, children, items - for child in self.get_children(): - yield from child.walk() - - def fully_resolve(self) -> None: - """Resolves every link in this catalog. - - Useful if, e.g., you'd like to read a catalog from a filesystem, upgrade - every object in the catalog to the latest STAC version, and save it back - to the filesystem. By default, :py:meth:`~pystac.Catalog.save` skips - unresolved links. - """ - for _, _, items in self.walk(): - # items is a generator, so we need to consume it to resolve the - # items - for item in items: - pass - - def validate_all(self, max_items: int | None = None, recursive: bool = True) -> int: - """Validates each catalog, collection, item contained within this catalog. - - Walks through the children and items of the catalog and validates each - stac object. - - Args: - max_items : The maximum number of STAC items to validate. Default - is None which means, validate them all. - recursive : Whether to validate catalog, collections, and items contained - within child objects. - - Returns: - int : Number of STAC items validated. - - Raises: - STACValidationError: Raises this error on any item that is invalid. - Will raise on the first invalid stac object encountered. - """ - n = 0 - self.validate() - for child in self.get_children(): - if recursive: - inner_max_items = None if max_items is None else max_items - n - n += child.validate_all(max_items=inner_max_items, recursive=True) - else: - child.validate() - for item in self.get_items(): - if max_items is not None and n >= max_items: - break - item.validate() - n += 1 - return n - - def _object_links(self) -> list[str | pystac.RelType]: - return [ - pystac.RelType.CHILD, - pystac.RelType.ITEM, - *pystac.EXTENSION_HOOKS.get_extended_object_links(self), - ] - - def map_items( - self, - item_mapper: Callable[[Item], Item | list[Item]], - ) -> Catalog: - """Creates a copy of a catalog, with each item passed through the - item_mapper function. - - Args: - item_mapper : A function that takes in an item, and returns - either an item or list of items. The item that is passed into the - item_mapper is a copy, so the method can mutate it safely. - - Returns: - Catalog: A full copy of this catalog, with items manipulated according - to the item_mapper function. - """ - - new_cat = self.full_copy() - - def process_catalog(catalog: Catalog) -> None: - for child in catalog.get_children(): - process_catalog(child) - - item_links: list[Link] = [] - for item_link in catalog.get_item_links(): - item_link.resolve_stac_object(root=self.get_root()) - mapped = item_mapper(cast(pystac.Item, item_link.target)) - if mapped is None: - raise Exception("item_mapper cannot return None.") - if isinstance(mapped, pystac.Item): - item_link.target = mapped - item_links.append(item_link) - else: - for i in mapped: - new_link = item_link.clone() - new_link.target = i - item_links.append(new_link) - catalog.clear_items() - catalog.add_links(item_links) - - process_catalog(new_cat) - return new_cat - - def map_assets( - self, - asset_mapper: Callable[ - [str, Asset], - Asset | tuple[str, Asset] | dict[str, Asset], - ], - ) -> Catalog: - """Creates a copy of a catalog, with each Asset for each Item passed - through the asset_mapper function. - - Args: - asset_mapper : A function that takes in an key and an Asset, and - returns either an Asset, a (key, Asset), or a dictionary of Assets with - unique keys. The Asset that is passed into the item_mapper is a copy, - so the method can mutate it safely. - - Returns: - Catalog: A full copy of this catalog, with assets manipulated according - to the asset_mapper function. - """ - - def apply_asset_mapper( - tup: tuple[str, Asset], - ) -> list[tuple[str, pystac.Asset]]: - k, v = tup - result = asset_mapper(k, v) - if result is None: - raise Exception("asset_mapper cannot return None.") - if isinstance(result, pystac.Asset): - return [(k, result)] - elif isinstance(result, tuple): - return [result] - else: - assets = list(result.items()) - if len(assets) < 1: - raise Exception("asset_mapper must return a non-empty list") - return assets - - def item_mapper(item: pystac.Item) -> pystac.Item: - new_assets = [ - x - for result in map(apply_asset_mapper, item.assets.items()) - for x in result - ] - item.assets = dict(new_assets) - return item - - return self.map_items(item_mapper) - - def describe(self, include_hrefs: bool = False, _indent: int = 0) -> None: - """Prints out information about this Catalog and all contained - STACObjects. - - Args: - include_hrefs (bool) - If True, print out each object's self link - HREF along with the object ID. - """ - s = "{}* {}".format(" " * _indent, self) - if include_hrefs: - s += f" {self.get_self_href()}" - print(s) - for child in self.get_children(): - child.describe(include_hrefs=include_hrefs, _indent=_indent + 4) - for item in self.get_items(): - s = "{}* {}".format(" " * (_indent + 2), item) - if include_hrefs: - s += f" {item.get_self_href()}" - print(s) - - @classmethod - def from_dict( - cls: type[C], - d: dict[str, Any], - href: str | None = None, - root: Catalog | None = None, - migrate: bool = True, - preserve_dict: bool = True, - ) -> C: - if migrate: - info = identify_stac_object(d) - d = migrate_to_latest(d, info) - - if not cls.matches_object_type(d): - raise STACTypeError(d, cls) - - catalog_type = CatalogType.determine_type(d) - - if preserve_dict: - d = deepcopy(d) - - id = d.pop("id") - description = d.pop("description") - title = d.pop("title", None) - stac_extensions = d.pop("stac_extensions", None) - links = d.pop("links") - - d.pop("stac_version") - - cat = cls( - id=id, - description=description, - title=title, - stac_extensions=stac_extensions, - extra_fields=d, - href=href, - catalog_type=catalog_type or CatalogType.ABSOLUTE_PUBLISHED, - ) - - for link in links: - if link["rel"] == pystac.RelType.ROOT: - # Remove the link that's generated in Catalog's constructor. - cat.remove_links(pystac.RelType.ROOT) - - if link["rel"] != pystac.RelType.SELF or href is None: - cat.add_link(Link.from_dict(link)) - - if root: - cat.set_root(root) - - return cat - - def full_copy( - self, root: Catalog | None = None, parent: Catalog | None = None - ) -> Catalog: - return cast(Catalog, super().full_copy(root, parent)) - - @classmethod - def from_file(cls: type[C], href: HREF, stac_io: pystac.StacIO | None = None) -> C: - if stac_io is None: - stac_io = pystac.StacIO.default() - - result = super().from_file(href, stac_io) - result._stac_io = stac_io - - return result - - @classmethod - def matches_object_type(cls, d: dict[str, Any]) -> bool: - return identify_stac_object_type(d) == STACObjectType.CATALOG - - @property - def ext(self) -> CatalogExt: - """Accessor for extension classes on this catalog - - Example:: - - print(collection.ext.version) - """ - from pystac.extensions.ext import CatalogExt - - return CatalogExt(stac_object=self) diff --git a/pystac/client.py b/pystac/client.py deleted file mode 100644 index f429bf10b..000000000 --- a/pystac/client.py +++ /dev/null @@ -1,23 +0,0 @@ -# mypy: ignore-errors - -_import_error_message = ( - "pystac-client is not installed.\n\n" - "Please install pystac-client:\n\n" - " pip install pystac-client" -) - -try: - from pystac_client import * # noqa: F403 -except ImportError as e: - if e.msg == "No module named 'pystac_client'": - raise ImportError(_import_error_message) from e - else: - raise - - -def __getattr__(value: str): - try: - import pystac_client - except ImportError as e: - raise ImportError(_import_error_message) from e - return getattr(pystac_client, value) diff --git a/pystac/collection.py b/pystac/collection.py deleted file mode 100644 index 200b2f10d..000000000 --- a/pystac/collection.py +++ /dev/null @@ -1,886 +0,0 @@ -from __future__ import annotations - -import warnings -from collections.abc import Iterable -from copy import deepcopy -from datetime import datetime, timezone -from typing import ( - TYPE_CHECKING, - Any, - Optional, - TypeVar, - cast, -) - -from dateutil import tz - -import pystac -from pystac import CatalogType, STACObjectType -from pystac.asset import Asset, Assets -from pystac.catalog import Catalog -from pystac.errors import DeprecatedWarning, ExtensionNotImplemented, STACTypeError -from pystac.item_assets import ItemAssetDefinition, _ItemAssets -from pystac.layout import HrefLayoutStrategy -from pystac.link import Link -from pystac.provider import Provider -from pystac.serialization import ( - identify_stac_object, - identify_stac_object_type, - migrate_to_latest, -) -from pystac.summaries import Summaries -from pystac.utils import ( - datetime_to_str, - str_to_datetime, -) - -if TYPE_CHECKING: - from pystac.extensions.ext import CollectionExt - from pystac.item import Item - -#: Generalized version of :class:`Collection` -C = TypeVar("C", bound="Collection") - -Bboxes = list[list[float | int]] -TemporalIntervals = list[list[datetime]] | list[list[Optional[datetime]]] -TemporalIntervalsLike = TemporalIntervals | list[datetime] | list[Optional[datetime]] - - -class SpatialExtent: - """Describes the spatial extent of a Collection. - - Args: - bboxes : A list of bboxes that represent the spatial - extent of the collection. Each bbox can be 2D or 3D. The length of the bbox - array must be 2*n where n is the number of dimensions. For example, a - 2D Collection with only one bbox would be [[xmin, ymin, xmax, ymax]] - - extra_fields : Dictionary containing additional top-level fields defined on the - Spatial Extent object. - """ - - bboxes: Bboxes - """A list of bboxes that represent the spatial - extent of the collection. Each bbox can be 2D or 3D. The length of the bbox - array must be 2*n where n is the number of dimensions. For example, a - 2D Collection with only one bbox would be [[xmin, ymin, xmax, ymax]]""" - - extra_fields: dict[str, Any] - """Dictionary containing additional top-level fields defined on the Spatial - Extent object.""" - - def __init__( - self, - bboxes: Bboxes | list[float | int], - extra_fields: dict[str, Any] | None = None, - ) -> None: - if not isinstance(bboxes, list): - raise TypeError("bboxes must be a list") - - # A common mistake is to pass in a single bbox instead of a list of bboxes. - # Account for this by transforming the input in that case. - if isinstance(bboxes[0], (float, int)): - self.bboxes = [cast(list[float | int], bboxes)] - else: - self.bboxes = cast(Bboxes, bboxes) - - self.extra_fields = extra_fields or {} - - def to_dict(self) -> dict[str, Any]: - """Returns this spatial extent as a dictionary. - - Returns: - dict: A serialization of the SpatialExtent. - """ - d = {"bbox": self.bboxes, **self.extra_fields} - return d - - def clone(self) -> SpatialExtent: - """Clones this object. - - Returns: - SpatialExtent: The clone of this object. - """ - cls = self.__class__ - return cls( - bboxes=deepcopy(self.bboxes), extra_fields=deepcopy(self.extra_fields) - ) - - @staticmethod - def from_dict(d: dict[str, Any]) -> SpatialExtent: - """Constructs a SpatialExtent from a dict. - - Returns: - SpatialExtent: The SpatialExtent deserialized from the JSON dict. - """ - return SpatialExtent( - bboxes=d["bbox"], extra_fields={k: v for k, v in d.items() if k != "bbox"} - ) - - @staticmethod - def from_coordinates( - coordinates: list[Any], extra_fields: dict[str, Any] | None = None - ) -> SpatialExtent: - """Constructs a SpatialExtent from a set of coordinates. - - This method will only produce a single bbox that covers all points - in the coordinate set. - - Args: - coordinates : Coordinates to derive the bbox from. - extra_fields : Dictionary containing additional top-level fields defined on - the SpatialExtent object. - - Returns: - SpatialExtent: A SpatialExtent with a single bbox that covers the - given coordinates. - """ - - def process_coords( - coord_lists: list[Any], - xmin: float | None = None, - ymin: float | None = None, - xmax: float | None = None, - ymax: float | None = None, - ) -> tuple[float | None, float | None, float | None, float | None]: - for coord in coord_lists: - if isinstance(coord[0], list): - xmin, ymin, xmax, ymax = process_coords( - coord, xmin, ymin, xmax, ymax - ) - else: - x, y = coord - if xmin is None or x < xmin: - xmin = x - elif xmax is None or xmax < x: - xmax = x - if ymin is None or y < ymin: - ymin = y - elif ymax is None or ymax < y: - ymax = y - return xmin, ymin, xmax, ymax - - xmin, ymin, xmax, ymax = process_coords(coordinates) - - if xmin is None or ymin is None or xmax is None or ymax is None: - raise ValueError( - f"Could not determine bounds from coordinate sequence {coordinates}" - ) - - return SpatialExtent( - bboxes=[[xmin, ymin, xmax, ymax]], extra_fields=extra_fields - ) - - -class TemporalExtent: - """Describes the temporal extent of a Collection. - - Args: - intervals : A list of two datetimes wrapped in a list, - representing the temporal extent of a Collection. Open date ranges are - supported by setting either the start (the first element of the interval) - or the end (the second element of the interval) to None. - - extra_fields : Dictionary containing additional top-level fields defined on the - Temporal Extent object. - Note: - Datetimes are required to be in UTC. - """ - - intervals: TemporalIntervals - """A list of two datetimes wrapped in a list, - representing the temporal extent of a Collection. Open date ranges are - represented by either the start (the first element of the interval) or the - end (the second element of the interval) being None.""" - - extra_fields: dict[str, Any] - """Dictionary containing additional top-level fields defined on the Temporal - Extent object.""" - - def __init__( - self, - intervals: TemporalIntervals | list[datetime | None], - extra_fields: dict[str, Any] | None = None, - ): - if not isinstance(intervals, list): - raise TypeError("intervals must be a list") - # A common mistake is to pass in a single interval instead of a - # list of intervals. Account for this by transforming the input - # in that case. - if isinstance(intervals[0], datetime) or intervals[0] is None: - self.intervals = [cast(list[Optional[datetime]], intervals)] - else: - self.intervals = cast(TemporalIntervals, intervals) - - self.extra_fields = extra_fields or {} - - def to_dict(self) -> dict[str, Any]: - """Returns this temporal extent as a dictionary. - - Returns: - dict: A serialization of the TemporalExtent. - """ - encoded_intervals: list[list[str | None]] = [] - for i in self.intervals: - start = None - end = None - - if i[0] is not None: - start = datetime_to_str(i[0]) - - if i[1] is not None: - end = datetime_to_str(i[1]) - - encoded_intervals.append([start, end]) - - d = {"interval": encoded_intervals, **self.extra_fields} - return d - - def clone(self) -> TemporalExtent: - """Clones this object. - - Returns: - TemporalExtent: The clone of this object. - """ - cls = self.__class__ - return cls( - intervals=deepcopy(self.intervals), extra_fields=deepcopy(self.extra_fields) - ) - - @staticmethod - def from_dict(d: dict[str, Any]) -> TemporalExtent: - """Constructs an TemporalExtent from a dict. - - Returns: - TemporalExtent: The TemporalExtent deserialized from the JSON dict. - """ - parsed_intervals: list[list[datetime | None]] = [] - for i in d["interval"]: - if isinstance(i, str): - # d["interval"] is a list of strings, so we correct the list and - # try again - # https://github.com/stac-utils/pystac/issues/1221 - warnings.warn( - "A collection's temporal extent should be a list of lists, but " - "is instead a " - "list of strings. pystac is fixing this issue and continuing " - "deserialization, but note that the source " - "collection is invalid STAC.", - UserWarning, - ) - d["interval"] = [d["interval"]] - return TemporalExtent.from_dict(d) - start = None - end = None - - if i[0]: - start = str_to_datetime(i[0]) - if i[1]: - end = str_to_datetime(i[1]) - parsed_intervals.append([start, end]) - - return TemporalExtent( - intervals=parsed_intervals, - extra_fields={k: v for k, v in d.items() if k != "interval"}, - ) - - @staticmethod - def from_now() -> TemporalExtent: - """Constructs an TemporalExtent with a single open interval that has - the start time as the current time. - - Returns: - TemporalExtent: The resulting TemporalExtent. - """ - return TemporalExtent( - intervals=[[datetime.now(timezone.utc).replace(microsecond=0), None]] - ) - - -class Extent: - """Describes the spatiotemporal extents of a Collection. - - Args: - spatial : Potential spatial extent covered by the collection. - temporal : Potential temporal extent covered by the collection. - extra_fields : Dictionary containing additional top-level fields defined on the - Extent object. - """ - - spatial: SpatialExtent - """Potential spatial extent covered by the collection.""" - - temporal: TemporalExtent - """Potential temporal extent covered by the collection.""" - - extra_fields: dict[str, Any] - """Dictionary containing additional top-level fields defined on the Extent - object.""" - - def __init__( - self, - spatial: SpatialExtent, - temporal: TemporalExtent, - extra_fields: dict[str, Any] | None = None, - ): - self.spatial = spatial - self.temporal = temporal - self.extra_fields = extra_fields or {} - - def to_dict(self) -> dict[str, Any]: - """Returns this extent as a dictionary. - - Returns: - dict: A serialization of the Extent. - """ - d = { - "spatial": self.spatial.to_dict(), - "temporal": self.temporal.to_dict(), - **self.extra_fields, - } - - return d - - def clone(self) -> Extent: - """Clones this object. - - Returns: - Extent: The clone of this extent. - """ - cls = self.__class__ - return cls( - spatial=self.spatial.clone(), - temporal=self.temporal.clone(), - extra_fields=deepcopy(self.extra_fields), - ) - - @staticmethod - def from_dict(d: dict[str, Any]) -> Extent: - """Constructs an Extent from a dict. - - Returns: - Extent: The Extent deserialized from the JSON dict. - """ - return Extent( - spatial=SpatialExtent.from_dict(d["spatial"]), - temporal=TemporalExtent.from_dict(d["temporal"]), - extra_fields={ - k: v for k, v in d.items() if k not in {"spatial", "temporal"} - }, - ) - - @staticmethod - def from_items( - items: Iterable[Item], extra_fields: dict[str, Any] | None = None - ) -> Extent: - """Create an Extent based on the datetimes and bboxes of a list of items. - - Args: - items : A list of items to derive the extent from. - extra_fields : Optional dictionary containing additional top-level fields - defined on the Extent object. - - Returns: - Extent: An Extent that spatially and temporally covers all of the - given items. - """ - bounds_values: list[list[float]] = [ - [float("inf")], - [float("inf")], - [float("-inf")], - [float("-inf")], - ] - datetimes: list[datetime] = [] - starts: list[datetime] = [] - ends: list[datetime] = [] - - for item in items: - if item.bbox is not None: - for i in range(0, 4): - bounds_values[i].append(item.bbox[i]) - if item.datetime is not None: - datetimes.append(item.datetime) - if item.common_metadata.start_datetime is not None: - starts.append(item.common_metadata.start_datetime) - if item.common_metadata.end_datetime is not None: - ends.append(item.common_metadata.end_datetime) - - if not any(datetimes + starts): - start_timestamp = None - else: - start_timestamp = min( - [ - dt if dt.tzinfo else dt.replace(tzinfo=tz.UTC) - for dt in datetimes + starts - ] - ) - if not any(datetimes + ends): - end_timestamp = None - else: - end_timestamp = max( - [ - dt if dt.tzinfo else dt.replace(tzinfo=tz.UTC) - for dt in datetimes + ends - ] - ) - - spatial = SpatialExtent( - [ - [ - min(bounds_values[0]), - min(bounds_values[1]), - max(bounds_values[2]), - max(bounds_values[3]), - ] - ] - ) - temporal = TemporalExtent([[start_timestamp, end_timestamp]]) - - return Extent(spatial=spatial, temporal=temporal, extra_fields=extra_fields) - - -class Collection(Catalog, Assets): - """A Collection extends the Catalog spec with additional metadata that helps - enable discovery. - - Args: - id : Identifier for the collection. Must be unique within the STAC. - description : Detailed multi-line description to fully explain the - 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. - title : Optional short descriptive one-line title for the - collection. - stac_extensions : Optional list of extensions the Collection - implements. - href : Optional HREF for this collection, which be set as the - collection's self link's HREF. - catalog_type : Optional catalog type for this catalog. Must - be one of the values in :class`~pystac.CatalogType`. - license : Collection's license(s) as a - `SPDX License identifier `_, - or `other`. If collection includes data with multiple - different licenses, use `other` and add a link for - each. The licenses `various` and `proprietary` are deprecated. - Defaults to 'other'. - keywords : Optional list of keywords describing the collection. - providers : Optional list of providers of this Collection. - summaries : An optional map of property summaries, - 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. - strategy : The layout strategy to use for setting the - HREFs of the catalog child objects and items. - If not provided, it will default to strategy of the parent and fallback to - :class:`~pystac.layout.BestPracticesLayoutStrategy`. - """ - - description: str - """Detailed multi-line description to fully explain the collection.""" - - extent: Extent - """Spatial and temporal extents that describe the bounds of all items contained - within this Collection.""" - - id: str - """Identifier for the collection.""" - - stac_extensions: list[str] - """List of extensions the Collection implements.""" - - title: str | None - """Optional short descriptive one-line title for the collection.""" - - keywords: list[str] | None - """Optional list of keywords describing the collection.""" - - providers: list[Provider] | None - """Optional list of providers of this Collection.""" - - summaries: Summaries - """A map of property summaries, either a set of values or statistics such as a - range.""" - - links: list[Link] - """A list of :class:`~pystac.Link` objects representing all links associated with - this Collection.""" - - extra_fields: dict[str, Any] - """Extra fields that are part of the top-level JSON properties of the Collection.""" - - STAC_OBJECT_TYPE = STACObjectType.COLLECTION - - DEFAULT_FILE_NAME = "collection.json" - """Default file name that will be given to this STAC object - in a canonical format.""" - - def __init__( - self, - id: str, - description: str, - extent: Extent, - title: str | None = None, - stac_extensions: list[str] | None = None, - href: str | None = None, - extra_fields: dict[str, Any] | None = None, - catalog_type: CatalogType | None = None, - license: str = "other", - keywords: list[str] | None = None, - providers: list[Provider] | None = None, - summaries: Summaries | None = None, - assets: dict[str, Asset] | None = None, - strategy: HrefLayoutStrategy | None = None, - ): - super().__init__( - id, - description, - title, - stac_extensions, - extra_fields, - href, - catalog_type or CatalogType.ABSOLUTE_PUBLISHED, - strategy, - ) - self.extent = extent - self.license = license - - self.stac_extensions: list[str] = stac_extensions or [] - self.keywords = keywords - self.providers = providers - self.summaries = summaries or Summaries.empty() - self._item_assets: _ItemAssets | None = None - - self.assets = {} - if assets is not None: - for k, asset in assets.items(): - self.add_asset(k, asset) - - def __repr__(self) -> str: - return f"" - - def add_item( - self, - item: Item, - title: str | None = None, - strategy: HrefLayoutStrategy | None = None, - set_parent: bool = True, - ) -> Link: - link = super().add_item(item, title, strategy, set_parent) - item.set_collection(self) - return link - - def to_dict( - self, include_self_link: bool = True, transform_hrefs: bool = True - ) -> dict[str, Any]: - d = super().to_dict( - include_self_link=include_self_link, transform_hrefs=transform_hrefs - ) - d["extent"] = self.extent.to_dict() - d["license"] = self.license - if self.stac_extensions: - d["stac_extensions"] = self.stac_extensions - if self.keywords: - d["keywords"] = self.keywords - if self.providers: - d["providers"] = list(map(lambda x: x.to_dict(), self.providers)) - if not self.summaries.is_empty(): - d["summaries"] = self.summaries.to_dict() - if any(self.assets): - d["assets"] = {k: v.to_dict() for k, v in self.assets.items()} - - return d - - def clone(self) -> Collection: - cls = self.__class__ - clone = cls( - id=self.id, - description=self.description, - extent=self.extent.clone(), - title=self.title, - stac_extensions=self.stac_extensions.copy(), - extra_fields=deepcopy(self.extra_fields), - catalog_type=self.catalog_type, - license=self.license, - 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) - - for link in self.links: - if link.rel == pystac.RelType.ROOT: - # Collection __init__ sets correct root to clone; don't reset - # if the root link points to self - root_is_self = link.is_resolved() and link.target is self - if not root_is_self: - clone.set_root(None) - clone.add_link(link.clone()) - else: - clone.add_link(link.clone()) - - return clone - - @classmethod - def from_dict( - cls: type[C], - d: dict[str, Any], - href: str | None = None, - root: Catalog | None = None, - migrate: bool = True, - preserve_dict: bool = True, - ) -> C: - from pystac.extensions.version import CollectionVersionExtension - - if migrate: - info = identify_stac_object(d) - d = migrate_to_latest(d, info) - - if not cls.matches_object_type(d): - raise STACTypeError(d, cls) - - catalog_type = CatalogType.determine_type(d) - - if preserve_dict: - d = deepcopy(d) - - id = d.pop("id") - description = d.pop("description") - license = d.pop("license") - extent = Extent.from_dict(d.pop("extent")) - title = d.pop("title", None) - stac_extensions = d.pop("stac_extensions", None) - keywords = d.pop("keywords", None) - providers = d.pop("providers", None) - if providers is not None: - providers = list(map(lambda x: pystac.Provider.from_dict(x), providers)) - summaries = d.pop("summaries", None) - if summaries is not None: - summaries = Summaries(summaries) - - assets = d.pop("assets", None) - if assets: - assets = {k: Asset.from_dict(v) for k, v in assets.items()} - links = d.pop("links") - - d.pop("stac_version") - - collection = cls( - id=id, - description=description, - extent=extent, - title=title, - stac_extensions=stac_extensions, - extra_fields=d, - license=license, - keywords=keywords, - providers=providers, - summaries=summaries, - href=href, - catalog_type=catalog_type, - assets=assets, - ) - - for link in links: - if link["rel"] == pystac.RelType.ROOT: - # Remove the link that's generated in Catalog's constructor. - collection.remove_links(pystac.RelType.ROOT) - - if link["rel"] != pystac.RelType.SELF or href is None: - collection.add_link(Link.from_dict(link)) - - if root: - collection.set_root(root) - - try: - version = CollectionVersionExtension.ext(collection) - if version.deprecated: - warnings.warn( - f"The collection '{collection.id}' is deprecated.", - DeprecatedWarning, - ) - # Collection asset deprecation checks pending version extension support - except ExtensionNotImplemented: - pass - - return collection - - @classmethod - def from_items( - cls: type[Collection], - items: Iterable[Item] | pystac.ItemCollection, - *, - id: str | None = None, - strategy: HrefLayoutStrategy | None = None, - ) -> Collection: - """Create a :class:`Collection` from iterable of items or an - :class:`~pystac.ItemCollection`. - - Will try to pull collection attributes from - :attr:`~pystac.ItemCollection.extra_fields` and items when possible. - - Args: - items : Iterable of :class:`~pystac.Item` instances to include in the - :class:`Collection`. This can be a :class:`~pystac.ItemCollection`. - id : Identifier for the collection. If not set, must be available on the - items and they must all match. - strategy : The layout strategy to use for setting the - HREFs of the catalog child objects and items. - If not provided, it will default to strategy of the parent and fallback - to :class:`~pystac.layout.BestPracticesLayoutStrategy`. - """ - - def extract(attr: str) -> Any: - """Extract attrs from items or item.properties as long as they all match""" - value = None - values = {getattr(item, attr, None) for item in items} - if len(values) == 1: - value = next(iter(values)) - if value is None: - values = {item.properties.get(attr, None) for item in items} - if len(values) == 1: - value = next(iter(values)) - return value - - if isinstance(items, pystac.ItemCollection): - extra_fields = deepcopy(items.extra_fields) - links = extra_fields.pop("links", {}) - providers = extra_fields.pop("providers", None) - if providers is not None: - providers = [pystac.Provider.from_dict(p) for p in providers] - else: - extra_fields = {} - links = {} - providers = [] - - id = id or extract("collection_id") - if id is None: - raise ValueError( - "Collection id must be defined. Either by specifying collection_id " - "on every item, or as a keyword argument to this function." - ) - - collection = cls( - id=id, - description=extract("description"), - extent=Extent.from_items(items), - title=extract("title"), - providers=providers, - extra_fields=extra_fields, - strategy=strategy, - ) - collection.add_items(items) - - for link in links: - collection.add_link(Link.from_dict(link)) - - return collection - - def get_item(self, id: str, recursive: bool = False) -> Item | None: - """Returns an item with a given ID. - - Args: - id : The ID of the item to find. - recursive : If True, search this collection and all children for the - item; otherwise, only search the items of this collection. Defaults - to False. - - Return: - Item or None: The item with the given ID, or None if not found. - """ - try: - return next(self.get_items(id, recursive=recursive), None) - except TypeError as e: - if any("recursive" in arg for arg in e.args): - # For inherited classes that do not yet support recursive - # See https://github.com/stac-utils/pystac-client/issues/485 - return super().get_item(id, recursive=recursive) - raise e - - @property - def item_assets(self) -> dict[str, ItemAssetDefinition]: - """Accessor for `item_assets - `__ - on this collection. - - Example:: - - .. code-block:: python - - >>> print(collection.item_assets) - {'thumbnail': , - 'metadata': , - 'B5': , - 'B6': , - 'B7': , - 'B8': } - >>> collection.item_assets["thumbnail"].title - 'Thumbnail' - - Set attributes on :class:`~pystac.ItemAssetDefinition` objects - - .. code-block:: python - - >>> collection.item_assets["thumbnail"].title = "New Title" - - Add to the ``item_assets`` dict: - - .. code-block:: python - - >>> collection.item_assets["B4"] = { - 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', - 'eo:bands': [{'name': 'B4', 'common_name': 'red'}] - } - >>> collection.item_assets["B4"].owner == collection - True - """ - if self._item_assets is None: - self._item_assets = _ItemAssets(self) - return self._item_assets - - @item_assets.setter - def item_assets( - self, item_assets: dict[str, ItemAssetDefinition | dict[str, Any]] | None - ) -> None: - # clear out the cached value - self._item_assets = None - - if item_assets is None: - self.extra_fields.pop("item_assets") - else: - self.extra_fields["item_assets"] = { - k: v if isinstance(v, dict) else v.to_dict() - for k, v in item_assets.items() - } - - def update_extent_from_items(self) -> None: - """ - Update datetime and bbox based on all items to a single bbox and time window. - """ - self.extent = Extent.from_items(self.get_items(recursive=True)) - - def full_copy( - self, root: Catalog | None = None, parent: Catalog | None = None - ) -> Collection: - return cast(Collection, super().full_copy(root, parent)) - - @classmethod - def matches_object_type(cls, d: dict[str, Any]) -> bool: - return identify_stac_object_type(d) == STACObjectType.COLLECTION - - @property - def ext(self) -> CollectionExt: - """Accessor for extension classes on this collection - - Example:: - - print(collection.ext.xarray) - """ - from pystac.extensions.ext import CollectionExt - - return CollectionExt(stac_object=self) diff --git a/pystac/common_metadata.py b/pystac/common_metadata.py deleted file mode 100644 index 4dac97dae..000000000 --- a/pystac/common_metadata.py +++ /dev/null @@ -1,251 +0,0 @@ -from __future__ import annotations - -from datetime import datetime -from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast - -import pystac -from pystac import utils -from pystac.errors import STACError - -if TYPE_CHECKING: - from pystac.asset import Asset - from pystac.item import Item - from pystac.provider import Provider - - -P = TypeVar("P") - - -class CommonMetadata: - """Object containing fields that are not included in core item schema but - are still commonly used. All attributes are defined within the properties of - this item and are optional - - Args: - properties : Dictionary of attributes that is the Item's properties - """ - - object: Asset | Item - """The object from which common metadata is obtained.""" - - def __init__(self, object: Asset | Item): - self.object = object - - def _set_field(self, prop_name: str, v: Any | None) -> None: - if hasattr(self.object, prop_name): - setattr(self.object, prop_name, v) - elif hasattr(self.object, "properties"): - item = cast(pystac.Item, self.object) - if v is None: - item.properties.pop(prop_name, None) - else: - item.properties[prop_name] = v - elif hasattr(self.object, "extra_fields") and isinstance( - self.object.extra_fields, dict - ): - if v is None: - self.object.extra_fields.pop(prop_name, None) - else: - self.object.extra_fields[prop_name] = v - else: - raise pystac.STACError(f"Cannot set field {prop_name} on {self}.") - - def _get_field(self, prop_name: str, _typ: type[P]) -> P | None: - if hasattr(self.object, prop_name): - return cast(Optional[P], getattr(self.object, prop_name)) - elif hasattr(self.object, "properties"): - item = cast(pystac.Item, self.object) - return item.properties.get(prop_name) - elif hasattr(self.object, "extra_fields") and isinstance( - self.object.extra_fields, dict - ): - return self.object.extra_fields.get(prop_name) - else: - raise STACError(f"Cannot get field {prop_name} from {self}.") - - # Basics - @property - def title(self) -> str | None: - """Gets or set the object's title.""" - return self._get_field("title", str) - - @title.setter - def title(self, v: str | None) -> None: - self._set_field("title", v) - - @property - def description(self) -> str | None: - """Gets or set the object's description.""" - return self._get_field("description", str) - - @description.setter - def description(self, v: str | None) -> None: - if v == "": - raise ValueError("description cannot be an empty string") - self._set_field("description", v) - - # Date and Time Range - @property - def start_datetime(self) -> datetime | None: - """Get or set the object's start_datetime. - - Note: - ``start_datetime`` is an inclusive datetime. - """ - return utils.map_opt( - utils.str_to_datetime, self._get_field("start_datetime", str) - ) - - @start_datetime.setter - def start_datetime(self, v: datetime | None) -> None: - self._set_field("start_datetime", utils.map_opt(utils.datetime_to_str, v)) - - @property - def end_datetime(self) -> datetime | None: - """Get or set the item's end_datetime. - - Note: - ``end_datetime`` is an inclusive datetime. - """ - return utils.map_opt( - utils.str_to_datetime, self._get_field("end_datetime", str) - ) - - @end_datetime.setter - def end_datetime(self, v: datetime | None) -> None: - self._set_field("end_datetime", utils.map_opt(utils.datetime_to_str, v)) - - # License - @property - def license(self) -> str | None: - """Get or set the current license. License should be provided - as a `SPDX License identifier `_, - or `other`. If object includes data with multiple - different licenses, use `other` and add a link for - each. - - Note: - The licenses `various` and `proprietary` are deprecated. - """ - return self._get_field("license", str) - - @license.setter - def license(self, v: str | None) -> None: - self._set_field("license", v) - - # Providers - @property - def providers(self) -> list[Provider] | None: - """Get or set a list of the object's providers.""" - return utils.map_opt( - lambda providers: [pystac.Provider.from_dict(d) for d in providers], - self._get_field("providers", list[dict[str, Any]]), - ) - - @providers.setter - def providers(self, v: list[Provider] | None) -> None: - self._set_field( - "providers", - utils.map_opt(lambda providers: [p.to_dict() for p in providers], v), - ) - - # Instrument - @property - def platform(self) -> str | None: - """Gets or set the object's platform attribute.""" - return self._get_field("platform", str) - - @platform.setter - def platform(self, v: str | None) -> None: - self._set_field("platform", v) - - @property - def instruments(self) -> list[str] | None: - """Gets or sets the names of the instruments used.""" - return self._get_field("instruments", list[str]) - - @instruments.setter - def instruments(self, v: list[str] | None) -> None: - self._set_field("instruments", v) - - @property - def constellation(self) -> str | None: - """Gets or set the name of the constellation associate with an object.""" - return self._get_field("constellation", str) - - @constellation.setter - def constellation(self, v: str | None) -> None: - self._set_field("constellation", v) - - @property - def mission(self) -> str | None: - """Gets or set the name of the mission associated with an object.""" - return self._get_field("mission", str) - - @mission.setter - def mission(self, v: str | None) -> None: - self._set_field("mission", v) - - @property - def gsd(self) -> float | None: - """Gets or sets the Ground Sample Distance at the sensor.""" - return self._get_field("gsd", float) - - @gsd.setter - def gsd(self, v: float | None) -> None: - self._set_field("gsd", v) - - # Metadata - @property - def created(self) -> datetime | None: - """Get or set the metadata file's creation date. All datetime attributes have - setters that can take either a string or a datetime, but always stores - the attribute as a string. - - Note: - ``created`` has a different meaning depending on the type of STAC object. - On an :class:`~pystac.Item`, it refers to the creation time of the - metadata. On an :class:`~pystac.Asset`, it refers to the creation time of - the actual data linked to in :attr:`~pystac.Asset.href`. - """ - return utils.map_opt(utils.str_to_datetime, self._get_field("created", str)) - - @created.setter - def created(self, v: datetime | None) -> None: - self._set_field("created", utils.map_opt(utils.datetime_to_str, v)) - - @property - def updated(self) -> datetime | None: - """Get or set the metadata file's update date. All datetime attributes have - setters that can take either a string or a datetime, but always stores - the attribute as a string - - Note: - ``updated`` has a different meaning depending on the type of STAC object. - On an :class:`~pystac.Item`, it refers to the update time of the - metadata. On an :class:`~pystac.Asset`, it refers to the update time of - the actual data linked to in :attr:`~pystac.Asset.href`. - """ - return utils.map_opt(utils.str_to_datetime, self._get_field("updated", str)) - - @updated.setter - def updated(self, v: datetime | None) -> None: - self._set_field("updated", utils.map_opt(utils.datetime_to_str, v)) - - @property - def keywords(self) -> list[str] | None: - """Get or set the keywords describing the STAC entity.""" - return self._get_field("keywords", list[str]) - - @keywords.setter - def keywords(self, v: list[str] | None) -> None: - self._set_field("keywords", v) - - @property - def roles(self) -> list[str] | None: - """Get or set the semantic roles of the entity.""" - return self._get_field("roles", list[str]) - - @roles.setter - def roles(self, v: list[str] | None) -> None: - self._set_field("roles", v) diff --git a/pystac/errors.py b/pystac/errors.py deleted file mode 100644 index d6f322a72..000000000 --- a/pystac/errors.py +++ /dev/null @@ -1,119 +0,0 @@ -from typing import Any - - -class TemplateError(Exception): - """Exception thrown when an error occurs during converting a template - string into data for :class:`~pystac.layout.LayoutTemplate` - """ - - pass - - -class STACError(Exception): - """A STACError is raised for errors relating to STAC, e.g. for - invalid formats or trying to operate on a STAC that does not have - the required information available. - """ - - pass - - -class STACTypeError(Exception): - """A STACTypeError is raised when encountering a representation of - a STAC entity that is not correct for the context; for example, if - a Catalog JSON was read in as an Item. - """ - - def __init__( - self, - bad_dict: dict[str, Any], - expected: type, - extra_message: str | None = "", - ): - """ - Construct an exception with an appropriate error message from bad_dict and the - expected that it didn't align with. - - Args: - bad_dict: Dictionary that did not match the expected type - expected: The expected type. - extra_message: message that will be appended to the exception message. - """ - message = ( - f"JSON (id = {bad_dict.get('id', 'unknown')}) does not represent" - f" a {expected.__name__} instance." - ) - if extra_message: - message += " " + extra_message - super().__init__(message) - - -class DuplicateObjectKeyError(Exception): - """Raised when deserializing a JSON object containing a duplicate key.""" - - pass - - -class ExtensionTypeError(Exception): - """An ExtensionTypeError is raised when an extension is used against - an object that the extension does not apply to - """ - - pass - - -class ExtensionAlreadyExistsError(Exception): - """An ExtensionAlreadyExistsError is raised when extension hooks - are registered with PySTAC if there are already hooks registered - for an extension with the same ID.""" - - pass - - -class ExtensionNotImplemented(Exception): - """Attempted to extend a STAC object that does not implement the given - extension.""" - - -class RequiredPropertyMissing(Exception): - """This error is raised when a required value was expected - to be there but was missing or None. This will happen, for example, - in an extension that has required properties, where the required - property is missing from the extended object - - Args: - obj: Description of the object that will have a property missing. - Should include a __repr__ that identifies the object for the - error message, or be a string that describes the object. - prop: The property that is missing - """ - - def __init__(self, obj: str | Any, prop: str, msg: str | None = None) -> None: - msg = msg or f"{repr(obj)} does not have required property {prop}" - super().__init__(msg) - - -class STACLocalValidationError(Exception): - """Schema not available locally""" - - -class STACValidationError(Exception): - """Represents a validation error. Thrown by validation calls if the STAC JSON - is invalid. - - Args: - source : Source of the exception. Type will be determined by the - validation implementation. For the default JsonSchemaValidator this will a - the ``jsonschema.ValidationError``. - """ - - def __init__(self, message: str, source: Any | None = None): - super().__init__(message) - self.source = source - - -class DeprecatedWarning(FutureWarning): - """Issued when converting a dictionary to a STAC Item or Collection and the - version extension ``deprecated`` field is present and set to ``True``.""" - - pass diff --git a/pystac/extensions/__init__.py b/pystac/extensions/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/pystac/extensions/base.py b/pystac/extensions/base.py deleted file mode 100644 index 356851ecb..000000000 --- a/pystac/extensions/base.py +++ /dev/null @@ -1,270 +0,0 @@ -from __future__ import annotations - -import re -import warnings -from abc import ABC, abstractmethod -from collections.abc import Iterable -from typing import ( - Any, - Generic, - TypeVar, - cast, -) - -import pystac -from pystac.stac_object import S - -VERSION_REGEX = re.compile("/v[0-9].[0-9].*/") - - -class SummariesExtension: - """Base class for extending the properties in :attr:`pystac.Collection.summaries` - to include properties defined by a STAC Extension. - - This class should generally not be instantiated directly. Instead, create an - extension-specific class that inherits from this class and instantiate that. See - :class:`~pystac.extensions.eo.SummariesEOExtension` for an example.""" - - summaries: pystac.Summaries - """The summaries for the :class:`~pystac.Collection` being extended.""" - - def __init__(self, collection: pystac.Collection) -> None: - self.summaries = collection.summaries - - def _set_summary( - self, - prop_key: str, - v: list[Any] | pystac.RangeSummary[Any] | dict[str, Any] | None, - ) -> None: - if v is None: - self.summaries.remove(prop_key) - else: - self.summaries.add(prop_key, v) - - -P = TypeVar("P") - - -class PropertiesExtension(ABC): - """Abstract base class for extending the properties of an :class:`~pystac.Item` - to include properties defined by a STAC Extension. - - This class should not be instantiated directly. Instead, create an - extension-specific class that inherits from this class and instantiate that. See - :class:`~pystac.extensions.eo.EOExtension` for an example. - """ - - properties: dict[str, Any] - """The properties that this extension wraps. - - The extension which implements PropertiesExtension can use ``_get_property`` and - ``_set_property`` to get and set values on this instance. Note that _set_properties - mutates the properties directly.""" - - additional_read_properties: Iterable[dict[str, Any]] | None = None - """Additional read-only properties accessible from the extended object. - - These are used when extending an :class:`~pystac.Asset` to give access to the - properties of the owning :class:`~pystac.Item`. If a property exists in both - ``additional_read_properties`` and ``properties``, the value in - ``additional_read_properties`` will take precedence. - """ - - def _get_property(self, prop_name: str, _typ: type[P]) -> P | None: - maybe_property: P | None = self.properties.get(prop_name) - if maybe_property is not None: - return maybe_property - if self.additional_read_properties is not None: - for props in self.additional_read_properties: - maybe_additional_property: P | None = props.get(prop_name) - if maybe_additional_property is not None: - return maybe_additional_property - return None - - def _set_property( - self, prop_name: str, v: Any | None, pop_if_none: bool = True - ) -> None: - if v is None and pop_if_none: - self.properties.pop(prop_name, None) - elif isinstance(v, list): - self.properties[prop_name] = [ - x.to_dict() if hasattr(x, "to_dict") else x for x in v - ] - else: - self.properties[prop_name] = v - - -class ExtensionManagementMixin(Generic[S], ABC): - """Abstract base class with methods for adding and removing extensions from STAC - Objects. This class is generic over the type of object being extended (e.g. - :class:`~pystac.Item`). - - Concrete extension implementations should inherit from this class and either - provide a concrete type or a bounded type variable. - - See :class:`~pystac.extensions.eo.EOExtension` for an example implementation. - """ - - @classmethod - @abstractmethod - def get_schema_uri(cls) -> str: - """Gets the schema URI associated with this extension.""" - raise NotImplementedError - - @classmethod - def get_schema_uris(cls) -> list[str]: - """Gets a list of schema URIs associated with this extension.""" - warnings.warn( - "get_schema_uris is deprecated and will be removed in v2", - DeprecationWarning, - ) - return [cls.get_schema_uri()] - - @classmethod - def add_to(cls, obj: S) -> None: - """Add the schema URI for this extension to the - :attr:`~pystac.STACObject.stac_extensions` list for the given object, if it is - not already present.""" - if obj.stac_extensions is None: - obj.stac_extensions = [cls.get_schema_uri()] - elif not cls.has_extension(obj): - obj.stac_extensions.append(cls.get_schema_uri()) - - @classmethod - def remove_from(cls, obj: S) -> None: - """Remove the schema URI for this extension from the - :attr:`pystac.STACObject.stac_extensions` list for the given object.""" - if obj.stac_extensions is not None: - obj.stac_extensions = [ - uri for uri in obj.stac_extensions if uri != cls.get_schema_uri() - ] - - @classmethod - def has_extension(cls, obj: S) -> bool: - """Check if the given object implements this extension by checking - :attr:`pystac.STACObject.stac_extensions` for this extension's schema URI.""" - schema_startswith = VERSION_REGEX.split(cls.get_schema_uri())[0] + "/" - - return obj.stac_extensions is not None and any( - uri.startswith(schema_startswith) for uri in obj.stac_extensions - ) - - @classmethod - def validate_owner_has_extension( - cls, - asset: pystac.Asset | pystac.ItemAssetDefinition, - add_if_missing: bool = False, - ) -> None: - """ - DEPRECATED - - .. deprecated:: 1.9 - Use :meth:`ensure_owner_has_extension` instead. - - Given an :class:`~pystac.Asset`, checks if the asset's owner has this - extension's schema URI in its :attr:`~pystac.STACObject.stac_extensions` list. - If ``add_if_missing`` is ``True``, the schema URI will be added to the owner. - - Args: - asset : The asset whose owner should be validated. - add_if_missing : Whether to add the schema URI to the owner if it is - not already present. Defaults to False. - - Raises: - STACError : If ``add_if_missing`` is ``True`` and ``asset.owner`` is - ``None``. - """ - warnings.warn( - "ensure_owner_has_extension is deprecated. " - "Use ensure_owner_has_extension instead", - DeprecationWarning, - ) - return cls.ensure_owner_has_extension(asset, add_if_missing) - - @classmethod - def ensure_owner_has_extension( - cls, - asset_or_link: pystac.Asset | pystac.ItemAssetDefinition | pystac.Link, - add_if_missing: bool = False, - ) -> None: - """Given an :class:`~pystac.Asset`, checks if the asset's owner has this - extension's schema URI in its :attr:`~pystac.STACObject.stac_extensions` list. - If ``add_if_missing`` is ``True``, the schema URI will be added to the owner. - - Args: - asset : The asset whose owner should be validated. - add_if_missing : Whether to add the schema URI to the owner if it is - not already present. Defaults to False. - - Raises: - STACError : If ``add_if_missing`` is ``True`` and ``asset.owner`` is - ``None``. - """ - if asset_or_link.owner is None: - if add_if_missing: - raise pystac.STACError( - f"Attempted to use add_if_missing=True for a {type(asset_or_link)} " - "with no owner. Use .set_owner or set add_if_missing=False." - ) - else: - return - return cls.ensure_has_extension(cast(S, asset_or_link.owner), add_if_missing) - - @classmethod - def validate_has_extension(cls, obj: S, add_if_missing: bool = False) -> None: - """ - DEPRECATED - - .. deprecated:: 1.9 - Use :meth:`ensure_has_extension` instead. - - Given a :class:`~pystac.STACObject`, checks if the object has this - extension's schema URI in its :attr:`~pystac.STACObject.stac_extensions` list. - If ``add_if_missing`` is ``True``, the schema URI will be added to the object. - - Args: - obj : The object to validate. - add_if_missing : Whether to add the schema URI to the object if it is - not already present. Defaults to False. - """ - warnings.warn( - "validate_has_extension is deprecated. Use ensure_has_extension instead", - DeprecationWarning, - ) - - return cls.ensure_has_extension(obj, add_if_missing) - - @classmethod - def ensure_has_extension(cls, obj: S, add_if_missing: bool = False) -> None: - """Given a :class:`~pystac.STACObject`, checks if the object has this - extension's schema URI in its :attr:`~pystac.STACObject.stac_extensions` list. - If ``add_if_missing`` is ``True``, the schema URI will be added to the object. - - Args: - obj : The object to validate. - add_if_missing : Whether to add the schema URI to the object if it is - not already present. Defaults to False. - """ - if add_if_missing: - cls.add_to(obj) - - if not cls.has_extension(obj): - name = getattr(cls, "name", cls.__name__) - suggestion = ( - f"``obj.ext.add('{name}')" - if hasattr(cls, "name") - else f"``{name}.add_to(obj)``" - ) - - raise pystac.ExtensionNotImplemented( - f"Extension '{name}' is not implemented on object." - f"STAC producers can add the extension using {suggestion}" - ) - - @classmethod - def _ext_error_message(cls, obj: Any) -> str: - contents = [f"{cls.__name__} does not apply to type '{type(obj).__name__}'"] - if hasattr(cls, "summaries") and isinstance(obj, pystac.Collection): - hint = f"Hint: Did you mean to use `{cls.__name__}.summaries` instead?" - contents.append(hint) - return ". ".join(contents) diff --git a/pystac/extensions/classification.py b/pystac/extensions/classification.py deleted file mode 100644 index b7c23e3d3..000000000 --- a/pystac/extensions/classification.py +++ /dev/null @@ -1,747 +0,0 @@ -"""Implements the :stac-ext:`Classification `.""" - -from __future__ import annotations - -import re -import warnings -from collections.abc import Iterable -from re import Pattern -from typing import ( - Any, - Generic, - Literal, - TypeVar, - cast, -) - -import pystac -from pystac.extensions.base import ( - ExtensionManagementMixin, - PropertiesExtension, - SummariesExtension, -) -from pystac.extensions.hooks import ExtensionHooks -from pystac.extensions.raster import RasterBand -from pystac.serialization.identify import STACJSONDescription, STACVersionID -from pystac.utils import get_required, map_opt - -#: Generalized version of :class:`~pystac.Item`, :class:`~pystac.Asset`, -#: :class:`~pystac.ItemAssetDefinition` or :class:`~pystac.extensions.raster.RasterBand` -T = TypeVar("T", pystac.Item, pystac.Asset, pystac.ItemAssetDefinition, RasterBand) - -SCHEMA_URI_PATTERN: str = ( - "https://stac-extensions.github.io/classification/v{version}/schema.json" -) -DEFAULT_VERSION: str = "2.0.0" -SUPPORTED_VERSIONS: list[str] = ["2.0.0", "1.1.0", "1.0.0"] - -# Field names -PREFIX: str = "classification:" -BITFIELDS_PROP: str = PREFIX + "bitfields" -CLASSES_PROP: str = PREFIX + "classes" -RASTER_BANDS_PROP: str = "raster:bands" - -COLOR_HINT_PATTERN: Pattern[str] = re.compile("^([0-9A-Fa-f]{6})$") - - -class Classification: - """Represents a single category of a classification. - - Use Classification.create to create a new Classification. - """ - - properties: dict[str, Any] - - def __init__(self, properties: dict[str, Any]) -> None: - self.properties = properties - - def apply( - self, - value: int, - description: str | None = None, - name: str | None = None, - color_hint: str | None = None, - nodata: bool | None = None, - percentage: float | None = None, - count: int | None = None, - ) -> None: - """ - Set the properties for a new Classification. - - Args: - value: The integer value corresponding to this class - description: The description of this class - name: Short name of the class for machine readability. Must consist only - of letters, numbers, -, and _ characters. Required as of v2.0 of - this extension. - color_hint: An optional hexadecimal string-encoded representation of the - RGB color that is suggested to represent this class (six hexadecimal - characters, all capitalized) - nodata: If set to true classifies a value as a no-data value. - percentage: The percentage of data values that belong to this class - in comparison to all data values, in percent (0-100). - count: The number of data values that belong to this class. - """ - self.value = value - # TODO pystac v2.0: make `name` non-optional, move it before - # `description` in the arg list, and remove this check - if name is None: - raise Exception( - "As of v2.0.0 of the classification extension, 'name' is required" - ) - self.name = name - self.description = description - self.color_hint = color_hint - self.nodata = nodata - self.percentage = percentage - self.count = count - - if color_hint is not None: - match = COLOR_HINT_PATTERN.match(color_hint) - assert ( - color_hint is None or match is not None and match.group() == color_hint - ), "Must format color hints as '^([0-9A-F]{6})$'" - - @classmethod - def create( - cls, - value: int, - description: str | None = None, - name: str | None = None, - color_hint: str | None = None, - nodata: bool | None = None, - percentage: float | None = None, - count: int | None = None, - ) -> Classification: - """ - Create a new Classification. - - Args: - value: The integer value corresponding to this class - description: The optional long-form description of this class - name: Short name of the class for machine readability. Must consist only - of letters, numbers, -, and _ characters. Required as of v2.0 of - this extension. - color_hint: An optional hexadecimal string-encoded representation of the - RGB color that is suggested to represent this class (six hexadecimal - characters, all capitalized) - nodata: If set to true classifies a value as a no-data value. - percentage: The percentage of data values that belong to this class - in comparison to all data values, in percent (0-100). - count: The number of data values that belong to this class. - """ - c = cls({}) - c.apply( - value=value, - name=name, - description=description, - color_hint=color_hint, - nodata=nodata, - percentage=percentage, - count=count, - ) - return c - - @property - def value(self) -> int: - """Get or set the class value - - Returns: - int - """ - return get_required(self.properties.get("value"), self, "value") - - @value.setter - def value(self, v: int) -> None: - self.properties["value"] = v - - @property - def description(self) -> str | None: - """Get or set the description of the class - - Returns: - str - """ - return self.properties.get("description") - - @description.setter - def description(self, v: str | None) -> None: - if v is not None: - self.properties["description"] = v - else: - self.properties.pop("description", None) - - @property - def name(self) -> str: - """Get or set the name of the class - - Returns: - Optional[str] - """ - return get_required(self.properties.get("name"), self, "name") - - @name.setter - def name(self, v: str) -> None: - if v is None: - raise Exception( - "`name` was converted to a required attribute in classification" - " version v2.0, so cannot be set to None" - ) - self.properties["name"] = v - - @property - def color_hint(self) -> str | None: - """Get or set the optional color hint for this class. - - The color hint must be a six-character string of capitalized hexadecimal - characters ([0-9A-F]). - - Returns: - Optional[str] - """ - return self.properties.get("color_hint") - - @color_hint.setter - def color_hint(self, v: str | None) -> None: - if v is not None: - match = COLOR_HINT_PATTERN.match(v) - assert ( - v is None or match is not None and match.group() == v - ), "Must format color hints as '^([0-9A-F]{6})$'" - self.properties["color_hint"] = v - else: - self.properties.pop("color_hint", None) - - @property - def nodata(self) -> bool | None: - """Get or set the nodata value for this class. - - Returns: - bool | None - """ - return self.properties.get("nodata") - - @nodata.setter - def nodata(self, v: bool | None) -> None: - if v is not None: - self.properties["nodata"] = v - else: - self.properties.pop("nodata", None) - - @property - def percentage(self) -> float | None: - """Get or set the percentage value for this class. - - Returns: - Optional[float] - """ - return self.properties.get("percentage") - - @percentage.setter - def percentage(self, v: float | None) -> None: - if v is not None: - self.properties["percentage"] = v - else: - self.properties.pop("percentage", None) - - @property - def count(self) -> int | None: - """Get or set the count value for this class. - - Returns: - Optional[int] - """ - return self.properties.get("count") - - @count.setter - def count(self, v: int | None) -> None: - if v is not None: - self.properties["count"] = v - else: - self.properties.pop("count", None) - - def to_dict(self) -> dict[str, Any]: - """Returns the dictionary encoding of this class - - Returns: - dict: The serialization of the Classification - """ - return self.properties - - def __eq__(self, other: object) -> bool: - if not isinstance(other, Classification): - raise NotImplementedError - return ( - self.value == other.value - and self.description == other.description - and self.name == other.name - and self.color_hint == other.color_hint - ) - - def __repr__(self) -> str: - return f"" - - -class Bitfield: - """Encodes the representation of values as bits in an integer. - - Use Bitfield.create to create a new Bitfield. - """ - - properties: dict[str, Any] - - def __init__(self, properties: dict[str, Any]): - self.properties = properties - - def apply( - self, - offset: int, - length: int, - classes: list[Classification], - roles: list[str] | None = None, - description: str | None = None, - name: str | None = None, - ) -> None: - """Sets the properties for this Bitfield. - - Args: - offset: describes the position of the least significant bit captured - by this bitfield, with zero indicating the least significant binary - digit - length: the number of bits described by this bitfield - classes: a list of Classification objects describing the various levels - captured by this bitfield - roles: the optional role of this bitfield (see `Asset Roles - `) - description: an optional short description of the classification - name: the optional name of the class for machine readability - """ - self.offset = offset - self.length = length - self.classes = classes - self.roles = roles - self.description = description - self.name = name - - assert offset >= 0, "Non-negative offsets only" - assert length >= 1, "Positive field lengths only" - assert len(classes) > 0, "Must specify at least one class" - assert ( - roles is None or len(roles) > 0 - ), "When set, roles must contain at least one item" - - @classmethod - def create( - cls, - offset: int, - length: int, - classes: list[Classification], - roles: list[str] | None = None, - description: str | None = None, - name: str | None = None, - ) -> Bitfield: - """Sets the properties for this Bitfield. - - Args: - offset: describes the position of the least significant bit captured - by this bitfield, with zero indicating the least significant binary - digit - length: the number of bits described by this bitfield - classes: a list of Classification objects describing the various levels - captured by this bitfield - roles: the optional role of this bitfield (see `Asset Roles - `) - description: an optional short description of the classification - name: the optional name of the class for machine readability - """ - b = cls({}) - b.apply( - offset=offset, - length=length, - classes=classes, - roles=roles, - description=description, - name=name, - ) - return b - - @property - def offset(self) -> int: - """Get or set the offset of the bitfield. - - Describes the position of the least significant bit captured by this - bitfield, with zero indicating the least significant binary digit - - Returns: - int - """ - return get_required(self.properties.get("offset"), self, "offset") - - @offset.setter - def offset(self, v: int) -> None: - self.properties["offset"] = v - - @property - def length(self) -> int: - """Get or set the length (number of bits) of the bitfield - - Returns: - int - """ - return get_required(self.properties.get("length"), self, "length") - - @length.setter - def length(self, v: int) -> None: - self.properties["length"] = v - - @property - def classes(self) -> list[Classification]: - """Get or set the class definitions for the bitfield - - Returns: - List[Classification] - """ - - return [ - Classification(d) - for d in cast( - list[dict[str, Any]], - get_required( - self.properties.get("classes"), - self, - "classes", - ), - ) - ] - - @classes.setter - def classes(self, v: list[Classification]) -> None: - self.properties["classes"] = [c.to_dict() for c in v] - - @property - def roles(self) -> list[str] | None: - """Get or set the role of the bitfield. - - See `Asset Roles - ` - - Returns: - Optional[List[str]] - """ - return self.properties.get("roles") - - @roles.setter - def roles(self, v: list[str] | None) -> None: - if v is not None: - self.properties["roles"] = v - else: - self.properties.pop("roles", None) - - @property - def description(self) -> str | None: - """Get or set the optional description of a bitfield. - - Returns: - Optional[str] - """ - return self.properties.get("description") - - @description.setter - def description(self, v: str | None) -> None: - if v is not None: - self.properties["description"] = v - else: - self.properties.pop("description", None) - - @property - def name(self) -> str | None: - """Get or set the optional name of the bitfield. - - Returns: - Optional[str] - """ - return self.properties.get("name") - - @name.setter - def name(self, v: str | None) -> None: - if v is not None: - self.properties["name"] = v - else: - self.properties.pop("name", None) - - def __repr__(self) -> str: - return ( - f"" - ) - - def to_dict(self) -> dict[str, Any]: - """Returns the dictionary encoding of this bitfield - - Returns: - dict: The serialization of the Bitfield - """ - return self.properties - - -class ClassificationExtension( - Generic[T], - PropertiesExtension, - ExtensionManagementMixin[pystac.Item | pystac.Collection], -): - """An abstract class that can be used to extend the properties of - :class:`~pystac.Item`, :class:`~pystac.Asset`, - :class:`~pystac.extensions.raster.RasterBand`, or - :class:`~pystac.ItemAssetDefinition` with properties from the - :stac-ext:`Classification Extension `. This class is generic - over the type of STAC object being extended. - - This class is not to be instantiated directly. One can either directly use the - subclass corresponding to the object you are extending, or the `ext` class - method can be used to construct the proper class for you. - """ - - name: Literal["classification"] = "classification" - properties: dict[str, Any] - """The :class:`~pystac.Asset` fields, including extension properties.""" - - def apply( - self, - classes: list[Classification] | None = None, - bitfields: list[Bitfield] | None = None, - ) -> None: - """Applies the classification extension fields to the extended object. - - Note: one may set either the classes or bitfields objects, but not both. - - Args: - classes: a list of - :class:`~pystac.extensions.classification.Classification` objects - describing the various classes in the classification - """ - assert ( - classes is None - and bitfields is not None - or bitfields is None - and classes is not None - ), "Must set exactly one of `classes` or `bitfields`" - if classes: - self.classes = classes - if bitfields: - self.bitfields = bitfields - - @property - def classes(self) -> list[Classification] | None: - """Get or set the classes for the base object - - Note: Setting the classes will clear the object's bitfields if they are - not None - - Returns: - Optional[List[Classification]] - """ - return self._get_classes() - - @classes.setter - def classes(self, v: list[Classification] | None) -> None: - if self._get_bitfields() is not None: - self.bitfields = None - self._set_property( - CLASSES_PROP, map_opt(lambda classes: [c.to_dict() for c in classes], v) - ) - - def _get_classes(self) -> list[Classification] | None: - return map_opt( - lambda classes: [Classification(c) for c in classes], - self._get_property(CLASSES_PROP, list[dict[str, Any]]), - ) - - @property - def bitfields(self) -> list[Bitfield] | None: - """Get or set the bitfields for the base object - - Note: Setting the bitfields will clear the object's classes if they are - not None - - Returns: - Optional[List[Bitfield]] - """ - return self._get_bitfields() - - @bitfields.setter - def bitfields(self, v: list[Bitfield] | None) -> None: - if self._get_classes() is not None: - self.classes = None - self._set_property( - BITFIELDS_PROP, - map_opt(lambda bitfields: [b.to_dict() for b in bitfields], v), - ) - - def _get_bitfields(self) -> list[Bitfield] | None: - return map_opt( - lambda bitfields: [Bitfield(b) for b in bitfields], - self._get_property(BITFIELDS_PROP, list[dict[str, Any]]), - ) - - @classmethod - def get_schema_uri(cls) -> str: - return SCHEMA_URI_PATTERN.format(version=DEFAULT_VERSION) - - @classmethod - def get_schema_uris(cls) -> list[str]: - warnings.warn( - "get_schema_uris is deprecated and will be removed in v2", - DeprecationWarning, - ) - return [SCHEMA_URI_PATTERN.format(version=v) for v in SUPPORTED_VERSIONS] - - @classmethod - def ext(cls, obj: T, add_if_missing: bool = False) -> ClassificationExtension[T]: - """Extends the given STAC object with propertied from the - :stac-ext:`Classification Extension ` - - This extension can be applied to instances of :class:`~pystac.Item`, - :class:`~pystac.Asset`, - :class:`~pystac.ItemAssetDefinition`, or - :class:`~pystac.extensions.raster.RasterBand`. - - Raises: - pystac.ExtensionTypeError : If an invalid object type is passed - """ - if isinstance(obj, pystac.Item): - cls.ensure_has_extension(obj, add_if_missing) - return cast(ClassificationExtension[T], ItemClassificationExtension(obj)) - elif isinstance(obj, pystac.Asset): - cls.ensure_owner_has_extension(obj, add_if_missing) - return cast(ClassificationExtension[T], AssetClassificationExtension(obj)) - elif isinstance(obj, pystac.ItemAssetDefinition): - cls.ensure_owner_has_extension(obj, add_if_missing) - return cast( - ClassificationExtension[T], ItemAssetsClassificationExtension(obj) - ) - elif isinstance(obj, RasterBand): - return cast( - ClassificationExtension[T], RasterBandClassificationExtension(obj) - ) - else: - raise pystac.ExtensionTypeError(cls._ext_error_message(obj)) - - @classmethod - def summaries( - cls, obj: pystac.Collection, add_if_missing: bool = False - ) -> SummariesClassificationExtension: - cls.ensure_has_extension(obj, add_if_missing) - return SummariesClassificationExtension(obj) - - -class ItemClassificationExtension(ClassificationExtension[pystac.Item]): - item: pystac.Item - - properties: dict[str, Any] - - def __init__(self, item: pystac.Item): - self.item = item - self.properties = item.properties - - def __repr__(self) -> str: - return f"" - - -class AssetClassificationExtension(ClassificationExtension[pystac.Asset]): - asset: pystac.Asset - asset_href: str - properties: dict[str, Any] - additional_read_properties: Iterable[dict[str, Any]] | None - - def __init__(self, asset: pystac.Asset): - self.asset = asset - self.asset_href = asset.href - self.properties = asset.extra_fields - if asset.owner and isinstance(asset.owner, pystac.Item): - self.additional_read_properties = [asset.owner.properties] - - def __repr__(self) -> str: - return f"" - - -class ItemAssetsClassificationExtension( - ClassificationExtension[pystac.ItemAssetDefinition] -): - properties: dict[str, Any] - asset_defn: pystac.ItemAssetDefinition - - def __init__(self, item_asset: pystac.ItemAssetDefinition): - self.asset_defn = item_asset - self.properties = item_asset.properties - - def __repr__(self) -> str: - return ( - f" str: - return f"" - - -class SummariesClassificationExtension(SummariesExtension): - @property - def classes(self) -> list[Classification] | None: - return map_opt( - lambda classes: [Classification(c) for c in classes], - self.summaries.get_list(CLASSES_PROP), - ) - - @classes.setter - def classes(self, v: list[Classification] | None) -> None: - self._set_summary(CLASSES_PROP, map_opt(lambda x: [c.to_dict() for c in x], v)) - - @property - def bitfields(self) -> list[Bitfield] | None: - return map_opt( - lambda bitfields: [Bitfield(b) for b in bitfields], - self.summaries.get_list(BITFIELDS_PROP), - ) - - @bitfields.setter - def bitfields(self, v: list[Bitfield] | None) -> None: - self._set_summary( - BITFIELDS_PROP, map_opt(lambda x: [b.to_dict() for b in x], v) - ) - - -class ClassificationExtensionHooks(ExtensionHooks): - schema_uri: str = SCHEMA_URI_PATTERN.format(version=DEFAULT_VERSION) - prev_extension_ids = { - SCHEMA_URI_PATTERN.format(version=v) - for v in SUPPORTED_VERSIONS - if v != DEFAULT_VERSION - } - stac_object_types = {pystac.STACObjectType.ITEM} - - def migrate( - self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription - ) -> None: - if SCHEMA_URI_PATTERN.format(version="1.0.0") in info.extensions: - for asset in obj.get("assets", {}).values(): - classification_classes = asset.get(CLASSES_PROP, None) - if classification_classes is None or not isinstance( - classification_classes, list - ): - continue - for class_object in classification_classes: - if "color-hint" in class_object: - class_object["color_hint"] = class_object["color-hint"] - del class_object["color-hint"] - super().migrate(obj, version, info) - - -CLASSIFICATION_EXTENSION_HOOKS: ExtensionHooks = ClassificationExtensionHooks() diff --git a/pystac/extensions/datacube.py b/pystac/extensions/datacube.py deleted file mode 100644 index 77623848a..000000000 --- a/pystac/extensions/datacube.py +++ /dev/null @@ -1,718 +0,0 @@ -"""Implements the :stac-ext:`Datacube Extension `.""" - -from __future__ import annotations - -from abc import ABC -from typing import Any, Generic, Literal, TypeVar, cast - -import pystac -from pystac.extensions.base import ExtensionManagementMixin, PropertiesExtension -from pystac.extensions.hooks import ExtensionHooks -from pystac.utils import StringEnum, get_required, map_opt - -#: Generalized version of :class:`~pystac.Collection`, `:class:`~pystac.Item`, -#: :class:`~pystac.Asset`, or :class:`~pystac.ItemAssetDefinition` -T = TypeVar( - "T", pystac.Collection, pystac.Item, pystac.Asset, pystac.ItemAssetDefinition -) - -SCHEMA_URI = "https://stac-extensions.github.io/datacube/v2.2.0/schema.json" - -PREFIX: str = "cube:" -DIMENSIONS_PROP = PREFIX + "dimensions" -VARIABLES_PROP = PREFIX + "variables" - -# Dimension properties -DIM_TYPE_PROP = "type" -DIM_DESC_PROP = "description" -DIM_AXIS_PROP = "axis" -DIM_EXTENT_PROP = "extent" -DIM_VALUES_PROP = "values" -DIM_STEP_PROP = "step" -DIM_REF_SYS_PROP = "reference_system" -DIM_UNIT_PROP = "unit" - -# Variable properties -VAR_TYPE_PROP = "type" -VAR_DESC_PROP = "description" -VAR_EXTENT_PROP = "extent" -VAR_VALUES_PROP = "values" -VAR_DIMENSIONS_PROP = "dimensions" -VAR_UNIT_PROP = "unit" - - -class DimensionType(StringEnum): - """Dimension object types for spatial and temporal Dimension Objects.""" - - SPATIAL = "spatial" - GEOMETRIES = "geometries" - TEMPORAL = "temporal" - - -class HorizontalSpatialDimensionAxis(StringEnum): - """Allowed values for ``axis`` field of :class:`HorizontalSpatialDimension` - object.""" - - X = "x" - Y = "y" - - -class VerticalSpatialDimensionAxis(StringEnum): - """Allowed values for ``axis`` field of :class:`VerticalSpatialDimension` - object.""" - - Z = "z" - - -class Dimension(ABC): - """Object representing a dimension of the datacube. The fields contained in - Dimension Object vary by ``type``. See the :stac-ext:`Datacube Dimension Object - ` docs for details. - """ - - properties: dict[str, Any] - - def __init__(self, properties: dict[str, Any]) -> None: - self.properties = properties - - @property - def dim_type(self) -> DimensionType | str: - """The type of the dimension. Must be ``"spatial"`` for - :stac-ext:`Horizontal Spatial Dimension Objects - ` or - :stac-ext:`Vertical Spatial Dimension Objects - `, ``geometries`` for - :stac-ext:`Spatial Vector Dimension Objects - ` ``"temporal"`` for - :stac-ext:`Temporal Dimension Objects - `. May be an arbitrary string for - :stac-ext:`Additional Dimension Objects - `.""" - return get_required( - self.properties.get(DIM_TYPE_PROP), "cube:dimension", DIM_TYPE_PROP - ) - - @dim_type.setter - def dim_type(self, v: DimensionType | str) -> None: - self.properties[DIM_TYPE_PROP] = v - - @property - def description(self) -> str | None: - """Detailed multi-line description to explain the dimension. `CommonMark 0.29 - `__ syntax MAY be used for rich text representation.""" - return self.properties.get(DIM_DESC_PROP) - - @description.setter - def description(self, v: str | None) -> None: - if v is None: - self.properties.pop(DIM_DESC_PROP, None) - else: - self.properties[DIM_DESC_PROP] = v - - def to_dict(self) -> dict[str, Any]: - return self.properties - - @staticmethod - def from_dict(d: dict[str, Any]) -> Dimension: - dim_type: str = get_required( - d.get(DIM_TYPE_PROP), "cube_dimension", DIM_TYPE_PROP - ) - if dim_type == DimensionType.SPATIAL: - axis: str = get_required( - d.get(DIM_AXIS_PROP), "cube_dimension", DIM_AXIS_PROP - ) - if axis == "z": - return VerticalSpatialDimension(d) - else: - return HorizontalSpatialDimension(d) - elif dim_type == DimensionType.GEOMETRIES: - return VectorSpatialDimension(d) - elif dim_type == DimensionType.TEMPORAL: - # The v1.0.0 spec says that AdditionalDimensions can have - # type 'temporal', but it is unclear how to differentiate that - # from a temporal dimension. Just key off of type for now. - # See https://github.com/stac-extensions/datacube/issues/5 - return TemporalDimension(d) - else: - return AdditionalDimension(d) - - -class SpatialDimension(Dimension): - @property - def extent(self) -> list[float]: - """Extent (lower and upper bounds) of the dimension as two-dimensional array. - Open intervals with ``None`` are not allowed.""" - return get_required( - self.properties.get(DIM_EXTENT_PROP), "cube:dimension", DIM_EXTENT_PROP - ) - - @extent.setter - def extent(self, v: list[float]) -> None: - self.properties[DIM_EXTENT_PROP] = v - - @property - def values(self) -> list[float] | None: - """Optional set of all potential values.""" - return self.properties.get(DIM_VALUES_PROP) - - @values.setter - def values(self, v: list[float] | None) -> None: - if v is None: - self.properties.pop(DIM_VALUES_PROP, None) - else: - self.properties[DIM_VALUES_PROP] = v - - @property - def step(self) -> float | None: - """The space between the values. Use ``None`` for irregularly spaced steps.""" - return self.properties.get(DIM_STEP_PROP) - - @step.setter - def step(self, v: float | None) -> None: - self.properties[DIM_STEP_PROP] = v - - def clear_step(self) -> None: - """Setting step to None sets it to the null value, - which means irregularly spaced steps. Use clear_step - to remove it from the properties.""" - self.properties.pop(DIM_STEP_PROP, None) - - @property - def reference_system(self) -> str | float | dict[str, Any] | None: - """The spatial reference system for the data, specified as `numerical EPSG code - `__, `WKT2 (ISO 19162) string - `__ or `PROJJSON - object `__. - Defaults to EPSG code 4326.""" - return self.properties.get(DIM_REF_SYS_PROP) - - @reference_system.setter - def reference_system(self, v: str | float | dict[str, Any] | None) -> None: - if v is None: - self.properties.pop(DIM_REF_SYS_PROP, None) - else: - self.properties[DIM_REF_SYS_PROP] = v - - -class HorizontalSpatialDimension(SpatialDimension): - @property - def axis(self) -> HorizontalSpatialDimensionAxis: - """Axis of the spatial dimension. Must be one of ``"x"`` or ``"y"``.""" - return get_required( - self.properties.get(DIM_AXIS_PROP), "cube:dimension", DIM_AXIS_PROP - ) - - @axis.setter - def axis(self, v: HorizontalSpatialDimensionAxis) -> None: - self.properties[DIM_AXIS_PROP] = v - - -class VerticalSpatialDimension(SpatialDimension): - @property - def axis(self) -> VerticalSpatialDimensionAxis: - """Axis of the spatial dimension. Must be ``"z"``.""" - return get_required( - self.properties.get(DIM_AXIS_PROP), "cube:dimension", DIM_AXIS_PROP - ) - - @axis.setter - def axis(self, v: VerticalSpatialDimensionAxis) -> None: - self.properties[DIM_AXIS_PROP] = v - - @property - def unit(self) -> str | None: - """The unit of measurement for the data, preferably compliant to `UDUNITS-2 - `__ units (singular).""" - return self.properties.get(DIM_UNIT_PROP) - - @unit.setter - def unit(self, v: str | None) -> None: - if v is None: - self.properties.pop(DIM_UNIT_PROP, None) - else: - self.properties[DIM_UNIT_PROP] = v - - -class VectorSpatialDimension(Dimension): - @property - def axes(self) -> list[str] | None: - """Axes of the vector dimension as an ordered set of `x`, `y` and `z`.""" - return self.properties.get("axes") - - @axes.setter - def axes(self, v: list[str]) -> None: - if v is None: - self.properties.pop("axes", None) - else: - self.properties["axes"] = v - - @property - def bbox(self) -> list[float]: - """A single bounding box of the geometries as defined for STAC - Collections but not nested.""" - return get_required(self.properties.get("bbox"), "cube:bbox", "bbox") - - @bbox.setter - def bbox(self, v: list[float]) -> None: - self.properties["bbox"] = v - - @property - def values(self) -> list[str] | None: - """Optionally, a representation of the geometries. This could be a list - of WKT strings or other identifiers.""" - return self.properties.get(DIM_VALUES_PROP) - - @values.setter - def values(self, v: list[str] | None) -> None: - if v is None: - self.properties.pop(DIM_VALUES_PROP, None) - else: - self.properties[DIM_VALUES_PROP] = v - - @property - def geometry_types(self) -> list[str] | None: - """A set of geometry types. If not present, mixed geometry types must be - assumed.""" - return self.properties.get("geometry_types") - - @geometry_types.setter - def geometry_types(self, v: list[str] | None) -> None: - if v is None: - self.properties.pop("geometry_types", None) - else: - self.properties["geometry_types"] = v - - @property - def reference_system(self) -> str | float | dict[str, Any] | None: - """The reference system for the data.""" - return self.properties.get(DIM_REF_SYS_PROP) - - @reference_system.setter - def reference_system(self, v: str | float | dict[str, Any] | None) -> None: - if v is None: - self.properties.pop(DIM_REF_SYS_PROP, None) - else: - self.properties[DIM_REF_SYS_PROP] = v - - -class TemporalDimension(Dimension): - @property - def extent(self) -> list[str | None] | None: - """Extent (lower and upper bounds) of the dimension as two-dimensional array. - The dates and/or times must be strings compliant to `ISO 8601 - `__. ``None`` is allowed for open date - ranges.""" - return self.properties.get(DIM_EXTENT_PROP) - - @extent.setter - def extent(self, v: list[str | None] | None) -> None: - if v is None: - self.properties.pop(DIM_EXTENT_PROP, None) - else: - self.properties[DIM_EXTENT_PROP] = v - - @property - def values(self) -> list[str] | None: - """If the dimension consists of set of specific values they can be listed here. - The dates and/or times must be strings compliant to `ISO 8601 - `__.""" - return self.properties.get(DIM_VALUES_PROP) - - @values.setter - def values(self, v: list[str] | None) -> None: - if v is None: - self.properties.pop(DIM_VALUES_PROP, None) - else: - self.properties[DIM_VALUES_PROP] = v - - @property - def step(self) -> str | None: - """The space between the temporal instances as `ISO 8601 duration - `__, e.g. P1D. Use null for - irregularly spaced steps.""" - return self.properties.get(DIM_STEP_PROP) - - @step.setter - def step(self, v: str | None) -> None: - self.properties[DIM_STEP_PROP] = v - - def clear_step(self) -> None: - """Setting step to None sets it to the null value, - which means irregularly spaced steps. Use clear_step - to remove it from the properties.""" - self.properties.pop(DIM_STEP_PROP, None) - - -class AdditionalDimension(Dimension): - @property - def extent(self) -> list[float | None] | None: - """If the dimension consists of `ordinal - `__ values, - the extent (lower and upper bounds) of the values as two-dimensional array. Use - null for open intervals.""" - return self.properties.get(DIM_EXTENT_PROP) - - @extent.setter - def extent(self, v: list[float | None] | None) -> None: - if v is None: - self.properties.pop(DIM_EXTENT_PROP, None) - else: - self.properties[DIM_EXTENT_PROP] = v - - @property - def values(self) -> list[str] | list[float] | None: - """A set of all potential values, especially useful for `nominal - `__ values.""" - return self.properties.get(DIM_VALUES_PROP) - - @values.setter - def values(self, v: list[str] | list[float] | None) -> None: - if v is None: - self.properties.pop(DIM_VALUES_PROP, None) - else: - self.properties[DIM_VALUES_PROP] = v - - @property - def step(self) -> float | None: - """If the dimension consists of `interval - `__ values, - the space between the values. Use null for irregularly spaced steps.""" - return self.properties.get(DIM_STEP_PROP) - - @step.setter - def step(self, v: float | None) -> None: - self.properties[DIM_STEP_PROP] = v - - def clear_step(self) -> None: - """Setting step to None sets it to the null value, - which means irregularly spaced steps. Use clear_step - to remove it from the properties.""" - self.properties.pop(DIM_STEP_PROP, None) - - @property - def unit(self) -> str | None: - """The unit of measurement for the data, preferably compliant to `UDUNITS-2 - units `__ (singular).""" - return self.properties.get(DIM_UNIT_PROP) - - @unit.setter - def unit(self, v: str | None) -> None: - if v is None: - self.properties.pop(DIM_UNIT_PROP, None) - else: - self.properties[DIM_UNIT_PROP] = v - - @property - def reference_system(self) -> str | float | dict[str, Any] | None: - """The reference system for the data.""" - return self.properties.get(DIM_REF_SYS_PROP) - - @reference_system.setter - def reference_system(self, v: str | float | dict[str, Any] | None) -> None: - if v is None: - self.properties.pop(DIM_REF_SYS_PROP, None) - else: - self.properties[DIM_REF_SYS_PROP] = v - - -class VariableType(StringEnum): - """Variable object types""" - - DATA = "data" - AUXILIARY = "auxiliary" - - -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: - self.properties = properties - - @property - def dimensions(self) -> list[str]: - """The dimensions of the variable. Should refer to keys in the - ``cube:dimensions`` object or be an empty list if the variable has no - dimensions - """ - return get_required( - self.properties.get(VAR_DIMENSIONS_PROP), - "cube:variable", - VAR_DIMENSIONS_PROP, - ) - - @dimensions.setter - def dimensions(self, v: list[str]) -> None: - self.properties[VAR_DIMENSIONS_PROP] = v - - @property - def var_type(self) -> VariableType | str: - """Type of the variable, either ``data`` or ``auxiliary``""" - return get_required( - self.properties.get(VAR_TYPE_PROP), "cube:variable", VAR_TYPE_PROP - ) - - @var_type.setter - def var_type(self, v: VariableType | str) -> None: - self.properties[VAR_TYPE_PROP] = v - - @property - def description(self) -> str | None: - """Detailed multi-line description to explain the variable. `CommonMark 0.29 - `__ syntax MAY be used for rich text representation.""" - return self.properties.get(VAR_DESC_PROP) - - @description.setter - def description(self, v: str | None) -> None: - if v is None: - self.properties.pop(VAR_DESC_PROP, None) - else: - self.properties[VAR_DESC_PROP] = v - - @property - def extent(self) -> list[float | str | None]: - """If the variable consists of `ordinal values - `, the extent - (lower and upper bounds) of the values as two-dimensional array. Use ``None`` - for open intervals""" - return get_required( - self.properties.get(VAR_EXTENT_PROP), "cube:variable", VAR_EXTENT_PROP - ) - - @extent.setter - def extent(self, v: list[float | str | None]) -> None: - self.properties[VAR_EXTENT_PROP] = v - - @property - def values(self) -> list[float | str] | None: - """A set of all potential values, especially useful for `nominal values - `.""" - return self.properties.get(VAR_VALUES_PROP) - - @values.setter - def values(self, v: list[float | str] | None) -> None: - if v is None: - self.properties.pop(VAR_VALUES_PROP) - else: - self.properties[VAR_VALUES_PROP] = v - - @property - def unit(self) -> str | None: - """The unit of measurement for the data, preferably compliant to `UDUNITS-2 - ` units (singular)""" - return self.properties.get(VAR_UNIT_PROP) - - @unit.setter - def unit(self, v: str | None) -> None: - if v is None: - self.properties.pop(VAR_UNIT_PROP) - else: - self.properties[VAR_UNIT_PROP] = v - - @staticmethod - def from_dict(d: dict[str, Any]) -> Variable: - return Variable(d) - - def to_dict(self) -> dict[str, Any]: - return self.properties - - -class DatacubeExtension( - Generic[T], - PropertiesExtension, - ExtensionManagementMixin[pystac.Item | pystac.Collection], -): - """An abstract class that can be used to extend the properties of a - :class:`~pystac.Collection`, :class:`~pystac.Item`, or :class:`~pystac.Asset` with - properties from the :stac-ext:`Datacube Extension `. This class is - generic over the type of STAC Object to be extended (e.g. :class:`~pystac.Item`, - :class:`~pystac.Asset`). - - To create a concrete instance of :class:`DatacubeExtension`, use the - :meth:`DatacubeExtension.ext` method. For example: - - .. code-block:: python - - >>> item: pystac.Item = ... - >>> dc_ext = DatacubeExtension.ext(item) - """ - - name: Literal["cube"] = "cube" - - def apply( - self, - dimensions: dict[str, Dimension], - variables: dict[str, Variable] | None = None, - ) -> None: - """Applies Datacube Extension properties to the extended - :class:`~pystac.Collection`, :class:`~pystac.Item` or :class:`~pystac.Asset`. - - Args: - dimensions : Dictionary mapping dimension name to :class:`Dimension` - objects. - variables : Dictionary mapping variable name to a :class:`Variable` - object. - """ - self.dimensions = dimensions - self.variables = variables - - @property - def dimensions(self) -> dict[str, Dimension]: - """A dictionary where each key is the name of a dimension and each - value is a :class:`~Dimension` object. - """ - result = get_required( - self._get_property(DIMENSIONS_PROP, dict[str, Any]), self, DIMENSIONS_PROP - ) - return {k: Dimension.from_dict(v) for k, v in result.items()} - - @dimensions.setter - def dimensions(self, v: dict[str, Dimension]) -> None: - self._set_property(DIMENSIONS_PROP, {k: dim.to_dict() for k, dim in v.items()}) - - @property - def variables(self) -> dict[str, Variable] | None: - """A dictionary where each key is the name of a variable and each - value is a :class:`~Variable` object. - """ - result = self._get_property(VARIABLES_PROP, dict[str, Any]) - - if result is None: - return None - return {k: Variable.from_dict(v) for k, v in result.items()} - - @variables.setter - def variables(self, v: dict[str, Variable] | None) -> None: - self._set_property( - VARIABLES_PROP, - map_opt( - lambda variables: {k: var.to_dict() for k, var in variables.items()}, v - ), - ) - - @classmethod - def get_schema_uri(cls) -> str: - return SCHEMA_URI - - @classmethod - def ext(cls, obj: T, add_if_missing: bool = False) -> DatacubeExtension[T]: - """Extends the given STAC Object with properties from the :stac-ext:`Datacube - Extension `. - - This extension can be applied to instances of :class:`~pystac.Collection`, - :class:`~pystac.Item` or :class:`~pystac.Asset`. - - Raises: - - pystac.ExtensionTypeError : If an invalid object type is passed. - """ - if isinstance(obj, pystac.Collection): - cls.ensure_has_extension(obj, add_if_missing) - return cast(DatacubeExtension[T], CollectionDatacubeExtension(obj)) - if isinstance(obj, pystac.Item): - cls.ensure_has_extension(obj, add_if_missing) - return cast(DatacubeExtension[T], ItemDatacubeExtension(obj)) - elif isinstance(obj, pystac.Asset): - cls.ensure_owner_has_extension(obj, add_if_missing) - return cast(DatacubeExtension[T], AssetDatacubeExtension(obj)) - elif isinstance(obj, pystac.ItemAssetDefinition): - cls.ensure_owner_has_extension(obj, add_if_missing) - return cast(DatacubeExtension[T], ItemAssetsDatacubeExtension(obj)) - else: - raise pystac.ExtensionTypeError(cls._ext_error_message(obj)) - - -class CollectionDatacubeExtension(DatacubeExtension[pystac.Collection]): - """A concrete implementation of :class:`DatacubeExtension` on an - :class:`~pystac.Collection` that extends the properties of the Item to include - properties defined in the :stac-ext:`Datacube Extension `. - - This class should generally not be instantiated directly. Instead, call - :meth:`DatacubeExtension.ext` on an :class:`~pystac.Collection` to extend it. - """ - - collection: pystac.Collection - properties: dict[str, Any] - - def __init__(self, collection: pystac.Collection): - self.collection = collection - self.properties = collection.extra_fields - - def __repr__(self) -> str: - return f"" - - -class ItemDatacubeExtension(DatacubeExtension[pystac.Item]): - """A concrete implementation of :class:`DatacubeExtension` on an - :class:`~pystac.Item` that extends the properties of the Item to include properties - defined in the :stac-ext:`Datacube Extension `. - - This class should generally not be instantiated directly. Instead, call - :meth:`DatacubeExtension.ext` on an :class:`~pystac.Item` to extend it. - """ - - item: pystac.Item - properties: dict[str, Any] - - def __init__(self, item: pystac.Item): - self.item = item - self.properties = item.properties - - def __repr__(self) -> str: - return f"" - - -class AssetDatacubeExtension(DatacubeExtension[pystac.Asset]): - """A concrete implementation of :class:`DatacubeExtension` on an - :class:`~pystac.Asset` that extends the Asset fields to include properties defined - in the :stac-ext:`Datacube Extension `. - - This class should generally not be instantiated directly. Instead, call - :meth:`DatacubeExtension.ext` on an :class:`~pystac.Asset` to extend it. - """ - - asset_href: str - properties: dict[str, Any] - additional_read_properties: list[dict[str, Any]] | None - - def __init__(self, asset: pystac.Asset): - self.asset_href = asset.href - self.properties = asset.extra_fields - if asset.owner and isinstance(asset.owner, pystac.Item): - self.additional_read_properties = [asset.owner.properties] - else: - self.additional_read_properties = None - - def __repr__(self) -> str: - return f"" - - -class ItemAssetsDatacubeExtension(DatacubeExtension[pystac.ItemAssetDefinition]): - properties: dict[str, Any] - asset_defn: pystac.ItemAssetDefinition - - def __init__(self, item_asset: pystac.ItemAssetDefinition): - self.asset_defn = item_asset - self.properties = item_asset.properties - - -class DatacubeExtensionHooks(ExtensionHooks): - schema_uri: str = SCHEMA_URI - prev_extension_ids = { - "datacube", - "https://stac-extensions.github.io/datacube/v1.0.0/schema.json", - "https://stac-extensions.github.io/datacube/v2.0.0/schema.json", - "https://stac-extensions.github.io/datacube/v2.1.0/schema.json", - } - stac_object_types = { - pystac.STACObjectType.COLLECTION, - pystac.STACObjectType.ITEM, - } - - -DATACUBE_EXTENSION_HOOKS: ExtensionHooks = DatacubeExtensionHooks() diff --git a/pystac/extensions/eo.py b/pystac/extensions/eo.py deleted file mode 100644 index 7d77d8fa8..000000000 --- a/pystac/extensions/eo.py +++ /dev/null @@ -1,708 +0,0 @@ -"""Implements the :stac-ext:`Electro-Optical Extension `.""" - -from __future__ import annotations - -import warnings -from collections.abc import Iterable -from typing import ( - Any, - Generic, - Literal, - TypeVar, - cast, -) - -import pystac -from pystac.extensions import projection, view -from pystac.extensions.base import ( - ExtensionManagementMixin, - PropertiesExtension, - SummariesExtension, -) -from pystac.extensions.hooks import ExtensionHooks -from pystac.serialization.identify import STACJSONDescription, STACVersionID -from pystac.summaries import RangeSummary -from pystac.utils import get_required, map_opt - -#: Generalized version of :class:`~pystac.Item`, :class:`~pystac.Asset`, -#: pr :class:`~pystac.ItemAssetDefinition` -T = TypeVar("T", pystac.Item, pystac.Asset, pystac.ItemAssetDefinition) - -SCHEMA_URI: str = "https://stac-extensions.github.io/eo/v1.1.0/schema.json" -SCHEMA_URIS: list[str] = [ - "https://stac-extensions.github.io/eo/v1.0.0/schema.json", - SCHEMA_URI, -] -PREFIX: str = "eo:" - -# Field names -BANDS_PROP: str = PREFIX + "bands" -CLOUD_COVER_PROP: str = PREFIX + "cloud_cover" -SNOW_COVER_PROP: str = PREFIX + "snow_cover" - - -def validated_percentage(v: float | None) -> float | None: - if v is not None and not isinstance(v, (float, int)) or isinstance(v, bool): - raise ValueError(f"Invalid percentage: {v} must be number") - if v is not None and not 0 <= v <= 100: - raise ValueError(f"Invalid percentage: {v} must be between 0 and 100") - return v - - -class Band: - """Represents Band information attached to an Item that implements the eo extension. - - Use :meth:`Band.create` to create a new Band. - """ - - properties: dict[str, Any] - - def __init__(self, properties: dict[str, Any]) -> None: - self.properties = properties - - def apply( - self, - name: str, - common_name: str | None = None, - description: str | None = None, - center_wavelength: float | None = None, - full_width_half_max: float | None = None, - solar_illumination: float | None = None, - ) -> None: - """ - Sets the properties for this Band. - - Args: - name : The name of the band (e.g., "B01", "B02", "B1", "B5", "QA"). - common_name : The name commonly used to refer to the band to make it - easier to search for bands across instruments. See the :stac-ext:`list - of accepted common names `. - description : Description to fully explain the band. - center_wavelength : The center wavelength of the band, in micrometers (μm). - full_width_half_max : Full width at half maximum (FWHM). The width of the - band, as measured at half the maximum transmission, in micrometers (μm). - solar_illumination: The solar illumination of the band, - as measured at half the maximum transmission, in W/m2/micrometers. - """ - self.name = name - self.common_name = common_name - self.description = description - self.center_wavelength = center_wavelength - self.full_width_half_max = full_width_half_max - self.solar_illumination = solar_illumination - - @classmethod - def create( - cls, - name: str, - common_name: str | None = None, - description: str | None = None, - center_wavelength: float | None = None, - full_width_half_max: float | None = None, - solar_illumination: float | None = None, - ) -> Band: - """ - Creates a new band. - - Args: - name : The name of the band (e.g., "B01", "B02", "B1", "B5", "QA"). - common_name : The name commonly used to refer to the band to make it easier - to search for bands across instruments. See the :stac-ext:`list of - accepted common names `. - description : Description to fully explain the band. - center_wavelength : The center wavelength of the band, in micrometers (μm). - full_width_half_max : Full width at half maximum (FWHM). The width of the - band, as measured at half the maximum transmission, in micrometers (μm). - solar_illumination: The solar illumination of the band, - as measured at half the maximum transmission, in W/m2/micrometers. - """ - b = cls({}) - b.apply( - name=name, - common_name=common_name, - description=description, - center_wavelength=center_wavelength, - full_width_half_max=full_width_half_max, - solar_illumination=solar_illumination, - ) - return b - - @property - def name(self) -> str: - """Get or sets the name of the band (e.g., "B01", "B02", "B1", "B5", "QA"). - - Returns: - str - """ - return get_required(self.properties.get("name"), self, "name") - - @name.setter - def name(self, v: str) -> None: - self.properties["name"] = v - - @property - def common_name(self) -> str | None: - """Get or sets the name commonly used to refer to the band to make it easier - to search for bands across instruments. See the :stac-ext:`list of accepted - common names `. - - Returns: - Optional[str] - """ - return self.properties.get("common_name") - - @common_name.setter - def common_name(self, v: str | None) -> None: - if v is not None: - self.properties["common_name"] = v - else: - self.properties.pop("common_name", None) - - @property - def description(self) -> str | None: - """Get or sets the description to fully explain the band. CommonMark 0.29 - syntax MAY be used for rich text representation. - - Returns: - str - """ - return self.properties.get("description") - - @description.setter - def description(self, v: str | None) -> None: - if v is not None: - self.properties["description"] = v - else: - self.properties.pop("description", None) - - @property - def center_wavelength(self) -> float | None: - """Get or sets the center wavelength of the band, in micrometers (μm). - - Returns: - float - """ - return self.properties.get("center_wavelength") - - @center_wavelength.setter - def center_wavelength(self, v: float | None) -> None: - if v is not None: - self.properties["center_wavelength"] = v - else: - self.properties.pop("center_wavelength", None) - - @property - def full_width_half_max(self) -> float | None: - """Get or sets the full width at half maximum (FWHM). The width of the band, - as measured at half the maximum transmission, in micrometers (μm). - - Returns: - [float] - """ - return self.properties.get("full_width_half_max") - - @full_width_half_max.setter - def full_width_half_max(self, v: float | None) -> None: - if v is not None: - self.properties["full_width_half_max"] = v - else: - self.properties.pop("full_width_half_max", None) - - @property - def solar_illumination(self) -> float | None: - """Get or sets the solar illumination of the band, - as measured at half the maximum transmission, in W/m2/micrometers. - - Returns: - [float] - """ - return self.properties.get("solar_illumination") - - @solar_illumination.setter - def solar_illumination(self, v: float | None) -> None: - if v is not None: - self.properties["solar_illumination"] = v - else: - self.properties.pop("solar_illumination", None) - - def __repr__(self) -> str: - return f"" - - def to_dict(self) -> dict[str, Any]: - """Returns this band as a dictionary. - - Returns: - dict: The serialization of this Band. - """ - return self.properties - - @staticmethod - def band_range(common_name: str) -> tuple[float, float] | None: - """Gets the band range for a common band name. - - Args: - common_name : The common band name. Must be one of the :stac-ext:`list of - accepted common names `. - - Returns: - Tuple[float, float] or None: The band range for this name as (min, max), or - None if this is not a recognized common name. - """ - name_to_range = { - "coastal": (0.40, 0.45), - "blue": (0.45, 0.50), - "green": (0.50, 0.60), - "red": (0.60, 0.70), - "yellow": (0.58, 0.62), - "pan": (0.50, 0.70), - "rededge": (0.70, 0.75), - "nir": (0.75, 1.00), - "nir08": (0.75, 0.90), - "nir09": (0.85, 1.05), - "cirrus": (1.35, 1.40), - "swir16": (1.55, 1.75), - "swir22": (2.10, 2.30), - "lwir": (10.5, 12.5), - "lwir11": (10.5, 11.5), - "lwir12": (11.5, 12.5), - } - - return name_to_range.get(common_name) - - @staticmethod - def band_description(common_name: str) -> str | None: - """Returns a description of the band for one with a common name. - - Args: - common_name : The common band name. Must be one of the :stac-ext:`list of - accepted common names `. - - Returns: - str or None: If a recognized common name, returns a description including - the band range. Otherwise returns None. - """ - r = Band.band_range(common_name) - if r is not None: - return f"Common name: {common_name}, Range: {r[0]} to {r[1]}" - return None - - -class EOExtension( - Generic[T], - PropertiesExtension, - ExtensionManagementMixin[pystac.Item | pystac.Collection], -): - """An abstract class that can be used to extend the properties of an - :class:`~pystac.Item` or :class:`~pystac.Asset` with properties from the - :stac-ext:`Electro-Optical Extension `. This class is generic over the type of - STAC Object to be extended (e.g. :class:`~pystac.Item`, - :class:`~pystac.Asset`). - - To create a concrete instance of :class:`EOExtension`, use the - :meth:`EOExtension.ext` method. For example: - - .. code-block:: python - - >>> item: pystac.Item = ... - >>> eo_ext = EOExtension.ext(item) - """ - - name: Literal["eo"] = "eo" - - def apply( - self, - bands: list[Band] | None = None, - cloud_cover: float | None = None, - snow_cover: float | None = None, - ) -> None: - """Applies Electro-Optical Extension properties to the extended - :class:`~pystac.Item` or :class:`~pystac.Asset`. - - Args: - bands : A list of available bands where each item is a :class:`~Band` - object. If given, requires at least one band. - cloud_cover : The estimate of cloud cover as a percentage - (0-100) of the entire scene. If not available the field should not - be provided. - snow_cover : The estimate of snow cover as a percentage - (0-100) of the entire scene. If not available the field should not - be provided. - """ - self.bands = bands - self.cloud_cover = validated_percentage(cloud_cover) - self.snow_cover = validated_percentage(snow_cover) - - @property - def bands(self) -> list[Band] | None: - """Gets or sets a list of available bands where each item is a :class:`~Band` - object (or ``None`` if no bands have been set). If not available the field - should not be provided. - """ - return self._get_bands() - - @bands.setter - def bands(self, v: list[Band] | None) -> None: - self._set_property( - BANDS_PROP, map_opt(lambda bands: [b.to_dict() for b in bands], v) - ) - - def _get_bands(self) -> list[Band] | None: - return map_opt( - lambda bands: [Band(b) for b in bands], - self._get_property(BANDS_PROP, list[dict[str, Any]]), - ) - - @property - def cloud_cover(self) -> float | None: - """Get or sets the estimate of cloud cover as a percentage - (0-100) of the entire scene. If not available the field should not be provided. - - Returns: - float or None - """ - return self._get_property(CLOUD_COVER_PROP, float) - - @cloud_cover.setter - def cloud_cover(self, v: float | None) -> None: - self._set_property(CLOUD_COVER_PROP, validated_percentage(v), pop_if_none=True) - - @property - def snow_cover(self) -> float | None: - """Get or sets the estimate of snow cover as a percentage - (0-100) of the entire scene. If not available the field should not be provided. - - Returns: - float or None - """ - return self._get_property(SNOW_COVER_PROP, float) - - @snow_cover.setter - def snow_cover(self, v: float | None) -> None: - self._set_property(SNOW_COVER_PROP, validated_percentage(v), pop_if_none=True) - - @classmethod - def get_schema_uri(cls) -> str: - return SCHEMA_URI - - @classmethod - def get_schema_uris(cls) -> list[str]: - warnings.warn( - "get_schema_uris is deprecated and will be removed in v2", - DeprecationWarning, - ) - return SCHEMA_URIS - - @classmethod - def ext(cls, obj: T, add_if_missing: bool = False) -> EOExtension[T]: - """Extends the given STAC Object with properties from the - :stac-ext:`Electro-Optical Extension `. - - This extension can be applied to instances of :class:`~pystac.Item` or - :class:`~pystac.Asset`. - - Raises: - - pystac.ExtensionTypeError : If an invalid object type is passed. - """ - if isinstance(obj, pystac.Item): - cls.ensure_has_extension(obj, add_if_missing) - return cast(EOExtension[T], ItemEOExtension(obj)) - elif isinstance(obj, pystac.Asset): - cls.ensure_owner_has_extension(obj, add_if_missing) - return cast(EOExtension[T], AssetEOExtension(obj)) - elif isinstance(obj, pystac.ItemAssetDefinition): - cls.ensure_owner_has_extension(obj, add_if_missing) - return cast(EOExtension[T], ItemAssetsEOExtension(obj)) - else: - raise pystac.ExtensionTypeError(cls._ext_error_message(obj)) - - @classmethod - def summaries( - cls, obj: pystac.Collection, add_if_missing: bool = False - ) -> SummariesEOExtension: - """Returns the extended summaries object for the given collection.""" - cls.ensure_has_extension(obj, add_if_missing) - return SummariesEOExtension(obj) - - -class ItemEOExtension(EOExtension[pystac.Item]): - """A concrete implementation of :class:`EOExtension` on an :class:`~pystac.Item` - that extends the properties of the Item to include properties defined in the - :stac-ext:`Electro-Optical Extension `. - - This class should generally not be instantiated directly. Instead, call - :meth:`EOExtension.ext` on an :class:`~pystac.Item` to extend it. - """ - - 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 _get_bands(self) -> list[Band] | None: - """Get or sets a list of :class:`~pystac.Band` objects that represent - the available bands. - """ - bands = self._get_property(BANDS_PROP, list[dict[str, Any]]) - - # get assets with eo:bands even if not in item - if bands is None: - asset_bands: list[dict[str, Any]] = [] - for _, value in self.item.get_assets().items(): - if BANDS_PROP in value.extra_fields: - asset_bands.extend( - cast(list[dict[str, Any]], value.extra_fields.get(BANDS_PROP)) - ) - if any(asset_bands): - bands = asset_bands - - if bands is not None: - return [Band(b) for b in bands] - return None - - def get_assets( - self, - name: str | None = None, - common_name: str | None = None, - ) -> dict[str, pystac.Asset]: - """Get the item's assets where eo:bands are defined. - - Args: - name: If set, filter the assets such that only those with a - matching ``eo:band.name`` are returned. - common_name: If set, filter the assets such that only those with a matching - ``eo:band.common_name`` are returned. - - Returns: - Dict[str, Asset]: A dictionary of assets that match ``name`` - and/or ``common_name`` if set or else all of this item's assets were - eo:bands are defined. - """ - kwargs = {"name": name, "common_name": common_name} - return { - key: asset - for key, asset in self.item.get_assets().items() - if BANDS_PROP in asset.extra_fields - and all( - v is None or any(v == b.get(k) for b in asset.extra_fields[BANDS_PROP]) - for k, v in kwargs.items() - ) - } - - def __repr__(self) -> str: - return f"" - - -class AssetEOExtension(EOExtension[pystac.Asset]): - """A concrete implementation of :class:`EOExtension` on an :class:`~pystac.Asset` - that extends the Asset fields to include properties defined in the - :stac-ext:`Electro-Optical Extension `. - - This class should generally not be instantiated directly. Instead, call - :meth:`EOExtension.ext` on an :class:`~pystac.Asset` to extend it. - """ - - asset_href: str - """The ``href`` value of the :class:`~pystac.Asset` being extended.""" - - properties: dict[str, Any] - """The :class:`~pystac.Asset` fields, including extension properties.""" - - additional_read_properties: Iterable[dict[str, Any]] | None = None - """If present, this will be a list containing 1 dictionary representing the - properties of the owning :class:`~pystac.Item`.""" - - def _get_bands(self) -> list[Band] | None: - if BANDS_PROP not in self.properties: - return None - return list( - map( - lambda band: Band(band), - cast(list[dict[str, Any]], self.properties.get(BANDS_PROP)), - ) - ) - - def __init__(self, asset: pystac.Asset): - self.asset_href = asset.href - self.properties = asset.extra_fields - if asset.owner and isinstance(asset.owner, pystac.Item): - self.additional_read_properties = [asset.owner.properties] - - def __repr__(self) -> str: - return f"" - - -class ItemAssetsEOExtension(EOExtension[pystac.ItemAssetDefinition]): - properties: dict[str, Any] - asset_defn: pystac.ItemAssetDefinition - - def _get_bands(self) -> list[Band] | None: - if BANDS_PROP not in self.properties: - return None - return list( - map( - lambda band: Band(band), - cast(list[dict[str, Any]], self.properties.get(BANDS_PROP)), - ) - ) - - def __init__(self, item_asset: pystac.ItemAssetDefinition): - self.asset_defn = item_asset - self.properties = item_asset.properties - - -class SummariesEOExtension(SummariesExtension): - """A concrete implementation of :class:`~pystac.extensions.base.SummariesExtension` - that extends the ``summaries`` field of a :class:`~pystac.Collection` to include - properties defined in the :stac-ext:`Electro-Optical Extension `. - """ - - @property - def bands(self) -> list[Band] | None: - """Get or sets the summary of :attr:`EOExtension.bands` values - for this Collection. - """ - - return map_opt( - lambda bands: [Band(b) for b in bands], - self.summaries.get_list(BANDS_PROP), - ) - - @bands.setter - def bands(self, v: list[Band] | None) -> None: - self._set_summary(BANDS_PROP, map_opt(lambda x: [b.to_dict() for b in x], v)) - - @property - def cloud_cover(self) -> RangeSummary[float] | None: - """Get or sets the summary of :attr:`EOExtension.cloud_cover` values - for this Collection. - """ - return self.summaries.get_range(CLOUD_COVER_PROP) - - @cloud_cover.setter - def cloud_cover(self, v: RangeSummary[float] | None) -> None: - self._set_summary(CLOUD_COVER_PROP, v) - - @property - def snow_cover(self) -> RangeSummary[float] | None: - """Get or sets the summary of :attr:`EOExtension.snow_cover` values - for this Collection. - """ - return self.summaries.get_range(SNOW_COVER_PROP) - - @snow_cover.setter - def snow_cover(self, v: RangeSummary[float] | None) -> None: - self._set_summary(SNOW_COVER_PROP, v) - - -class EOExtensionHooks(ExtensionHooks): - schema_uri: str = SCHEMA_URI - prev_extension_ids = { - "eo", - *[uri for uri in SCHEMA_URIS if uri != SCHEMA_URI], - } - stac_object_types = {pystac.STACObjectType.ITEM} - - def migrate( - self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription - ) -> None: - if version < "0.9": - # Some eo fields became common_metadata - if ( - "eo:platform" in obj["properties"] - and "platform" not in obj["properties"] - ): - obj["properties"]["platform"] = obj["properties"]["eo:platform"] - del obj["properties"]["eo:platform"] - - if ( - "eo:instrument" in obj["properties"] - and "instruments" not in obj["properties"] - ): - obj["properties"]["instruments"] = [obj["properties"]["eo:instrument"]] - del obj["properties"]["eo:instrument"] - - if ( - "eo:constellation" in obj["properties"] - and "constellation" not in obj["properties"] - ): - obj["properties"]["constellation"] = obj["properties"][ - "eo:constellation" - ] - del obj["properties"]["eo:constellation"] - - # Some eo fields became view extension fields - eo_to_view_fields = [ - "off_nadir", - "azimuth", - "incidence_angle", - "sun_azimuth", - "sun_elevation", - ] - - for field in eo_to_view_fields: - if f"eo:{field}" in obj["properties"]: - if "stac_extensions" not in obj: - obj["stac_extensions"] = [] - if view.SCHEMA_URI not in obj["stac_extensions"]: - obj["stac_extensions"].append(view.SCHEMA_URI) - if f"view:{field}" not in obj["properties"]: - obj["properties"][f"view:{field}"] = obj["properties"][ - f"eo:{field}" - ] - del obj["properties"][f"eo:{field}"] - - # eo:epsg became proj:epsg in Projection Extension <2.0.0 and became - # proj:code in Projection Extension 2.0.0 - eo_epsg = PREFIX + "epsg" - proj_epsg = projection.PREFIX + "epsg" - proj_code = projection.PREFIX + "code" - if ( - eo_epsg in obj["properties"] - and proj_epsg not in obj["properties"] - and proj_code not in obj["properties"] - ): - obj["stac_extensions"] = obj.get("stac_extensions", []) - if set(obj["stac_extensions"]).intersection( - projection.ProjectionExtensionHooks.pre_2 - ): - obj["properties"][proj_epsg] = obj["properties"].pop(eo_epsg) - else: - obj["properties"][proj_code] = ( - f"EPSG:{obj['properties'].pop(eo_epsg)}" - ) - if not projection.ProjectionExtensionHooks().has_extension(obj): - obj["stac_extensions"].append( - projection.ProjectionExtension.get_schema_uri() - ) - - if not any(prop.startswith(PREFIX) for prop in obj["properties"]): - obj["stac_extensions"].remove(EOExtension.get_schema_uri()) - - if version < "1.0.0-beta.1" and info.object_type == pystac.STACObjectType.ITEM: - # gsd moved from eo to common metadata - if "eo:gsd" in obj["properties"]: - obj["properties"]["gsd"] = obj["properties"]["eo:gsd"] - del obj["properties"]["eo:gsd"] - - # The way bands were declared in assets changed. - # In 1.0.0-beta.1 they are inlined into assets as - # opposed to having indices back into a property-level array. - if "eo:bands" in obj["properties"]: - bands = obj["properties"]["eo:bands"] - for asset in obj["assets"].values(): - if "eo:bands" in asset: - new_bands: list[dict[str, Any]] = [] - for band_index in asset["eo:bands"]: - new_bands.append(bands[band_index]) - asset["eo:bands"] = new_bands - - super().migrate(obj, version, info) - - -EO_EXTENSION_HOOKS: ExtensionHooks = EOExtensionHooks() diff --git a/pystac/extensions/ext.py b/pystac/extensions/ext.py deleted file mode 100644 index 5cad55b85..000000000 --- a/pystac/extensions/ext.py +++ /dev/null @@ -1,420 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any, Generic, Literal, TypeVar, cast - -from pystac import ( - Asset, - Catalog, - Collection, - Item, - ItemAssetDefinition, - Link, - STACError, -) -from pystac.extensions.classification import ClassificationExtension -from pystac.extensions.datacube import DatacubeExtension -from pystac.extensions.eo import EOExtension -from pystac.extensions.file import FileExtension -from pystac.extensions.grid import GridExtension -from pystac.extensions.item_assets import ItemAssetsExtension -from pystac.extensions.mgrs import MgrsExtension -from pystac.extensions.pointcloud import PointcloudExtension -from pystac.extensions.projection import ProjectionExtension -from pystac.extensions.raster import RasterExtension -from pystac.extensions.render import Render, RenderExtension -from pystac.extensions.sar import SarExtension -from pystac.extensions.sat import SatExtension -from pystac.extensions.scientific import ScientificExtension -from pystac.extensions.storage import StorageExtension -from pystac.extensions.table import TableExtension -from pystac.extensions.timestamps import TimestampsExtension -from pystac.extensions.version import BaseVersionExtension, VersionExtension -from pystac.extensions.view import ViewExtension -from pystac.extensions.xarray_assets import XarrayAssetsExtension - -#: Generalized version of :class:`~pystac.Asset`, -#: :class:`~pystac.ItemAssetDefinition`, or :class:`~pystac.Link` -T = TypeVar("T", Asset, ItemAssetDefinition, Link) -#: Generalized version of :class:`~pystac.Asset` or -#: :class:`~pystac.ItemAssetDefinition` -U = TypeVar("U", Asset, ItemAssetDefinition) - -EXTENSION_NAMES = Literal[ - "classification", - "cube", - "eo", - "file", - "grid", - "item_assets", - "mgrs", - "pc", - "proj", - "raster", - "render", - "sar", - "sat", - "sci", - "storage", - "table", - "timestamps", - "version", - "view", - "xarray", -] - -EXTENSION_NAME_MAPPING: dict[EXTENSION_NAMES, Any] = { - ClassificationExtension.name: ClassificationExtension, - DatacubeExtension.name: DatacubeExtension, - EOExtension.name: EOExtension, - FileExtension.name: FileExtension, - GridExtension.name: GridExtension, - ItemAssetsExtension.name: ItemAssetsExtension, - MgrsExtension.name: MgrsExtension, - PointcloudExtension.name: PointcloudExtension, - ProjectionExtension.name: ProjectionExtension, - RasterExtension.name: RasterExtension, - RenderExtension.name: RenderExtension, - SarExtension.name: SarExtension, - SatExtension.name: SatExtension, - ScientificExtension.name: ScientificExtension, - StorageExtension.name: StorageExtension, - TableExtension.name: TableExtension, - TimestampsExtension.name: TimestampsExtension, - VersionExtension.name: VersionExtension, - ViewExtension.name: ViewExtension, - XarrayAssetsExtension.name: XarrayAssetsExtension, -} - - -def _get_class_by_name(name: str) -> Any: - try: - return EXTENSION_NAME_MAPPING[cast(EXTENSION_NAMES, name)] - except KeyError as e: - raise KeyError( - f"Extension '{name}' is not a valid extension. " - f"Options are {list(EXTENSION_NAME_MAPPING)}" - ) from e - - -@dataclass -class CatalogExt: - """Supporting the :attr:`~pystac.Catalog.ext` accessor for interacting - with extension classes - """ - - stac_object: Catalog - - def has(self, name: EXTENSION_NAMES) -> bool: - """Whether the given extension is enabled on this STAC object - - Args: - name : Extension identifier (eg: 'eo') - - Returns: - bool: ``True`` if extension is enabled, otherwise ``False`` - """ - return cast(bool, _get_class_by_name(name).has_extension(self.stac_object)) - - def add(self, name: EXTENSION_NAMES) -> None: - """Add the given extension to this STAC object - - Args: - name : Extension identifier (eg: 'eo') - """ - _get_class_by_name(name).add_to(self.stac_object) - - def remove(self, name: EXTENSION_NAMES) -> None: - """Remove the given extension from this STAC object - - Args: - name : Extension identifier (eg: 'eo') - """ - _get_class_by_name(name).remove_from(self.stac_object) - - @property - def version(self) -> VersionExtension[Catalog]: - return VersionExtension.ext(self.stac_object) - - -@dataclass -class CollectionExt(CatalogExt): - """Supporting the :attr:`~pystac.Collection.ext` accessor for interacting - with extension classes - """ - - stac_object: Collection - - @property - def cube(self) -> DatacubeExtension[Collection]: - return DatacubeExtension.ext(self.stac_object) - - @property - def item_assets(self) -> dict[str, ItemAssetDefinition]: - return ItemAssetsExtension.ext(self.stac_object).item_assets - - @property - def render(self) -> dict[str, Render]: - return RenderExtension.ext(self.stac_object).renders - - @property - def sci(self) -> ScientificExtension[Collection]: - return ScientificExtension.ext(self.stac_object) - - @property - def table(self) -> TableExtension[Collection]: - return TableExtension.ext(self.stac_object) - - @property - def xarray(self) -> XarrayAssetsExtension[Collection]: - return XarrayAssetsExtension.ext(self.stac_object) - - -@dataclass -class ItemExt: - """Supporting the :attr:`~pystac.Item.ext` accessor for interacting - with extension classes - """ - - stac_object: Item - - def has(self, name: EXTENSION_NAMES) -> bool: - """Whether the given extension is enabled on this STAC object - - Args: - name : Extension identifier (eg: 'eo') - - Returns: - bool: ``True`` if extension is enabled, otherwise ``False`` - """ - return cast(bool, _get_class_by_name(name).has_extension(self.stac_object)) - - def add(self, name: EXTENSION_NAMES) -> None: - """Add the given extension to this STAC object - - Args: - name : Extension identifier (eg: 'eo') - """ - _get_class_by_name(name).add_to(self.stac_object) - - def remove(self, name: EXTENSION_NAMES) -> None: - """Remove the given extension from this STAC object - - Args: - name : Extension identifier (eg: 'eo') - """ - _get_class_by_name(name).remove_from(self.stac_object) - - @property - def classification(self) -> ClassificationExtension[Item]: - return ClassificationExtension.ext(self.stac_object) - - @property - def cube(self) -> DatacubeExtension[Item]: - return DatacubeExtension.ext(self.stac_object) - - @property - def eo(self) -> EOExtension[Item]: - return EOExtension.ext(self.stac_object) - - @property - def grid(self) -> GridExtension: - return GridExtension.ext(self.stac_object) - - @property - def mgrs(self) -> MgrsExtension: - return MgrsExtension.ext(self.stac_object) - - @property - def pc(self) -> PointcloudExtension[Item]: - return PointcloudExtension.ext(self.stac_object) - - @property - def proj(self) -> ProjectionExtension[Item]: - return ProjectionExtension.ext(self.stac_object) - - @property - def render(self) -> RenderExtension[Item]: - return RenderExtension.ext(self.stac_object) - - @property - def sar(self) -> SarExtension[Item]: - return SarExtension.ext(self.stac_object) - - @property - def sat(self) -> SatExtension[Item]: - return SatExtension.ext(self.stac_object) - - @property - def sci(self) -> ScientificExtension[Item]: - return ScientificExtension.ext(self.stac_object) - - @property - def storage(self) -> StorageExtension[Item]: - return StorageExtension.ext(self.stac_object) - - @property - def table(self) -> TableExtension[Item]: - return TableExtension.ext(self.stac_object) - - @property - def timestamps(self) -> TimestampsExtension[Item]: - return TimestampsExtension.ext(self.stac_object) - - @property - def version(self) -> VersionExtension[Item]: - return VersionExtension.ext(self.stac_object) - - @property - def view(self) -> ViewExtension[Item]: - return ViewExtension.ext(self.stac_object) - - @property - def xarray(self) -> XarrayAssetsExtension[Item]: - return XarrayAssetsExtension.ext(self.stac_object) - - -class _AssetsExt(Generic[T]): - stac_object: T - - def has(self, name: EXTENSION_NAMES) -> bool: - """Whether the given extension is enabled on the owner - - Args: - name : Extension identifier (eg: 'eo') - - Returns: - bool: ``True`` if extension is enabled, otherwise ``False`` - """ - if self.stac_object.owner is None: - raise STACError( - f"Attempted to use `.ext.has('{name}') for an object with no owner. " - "Use `.set_owner` and then try to check the extension again." - ) - else: - return cast( - bool, _get_class_by_name(name).has_extension(self.stac_object.owner) - ) - - def add(self, name: EXTENSION_NAMES) -> None: - """Add the given extension to the owner - - Args: - name : Extension identifier (eg: 'eo') - """ - if self.stac_object.owner is None: - raise STACError( - f"Attempted to add extension='{name}' for an object with no owner. " - "Use `.set_owner` and then try to add the extension again." - ) - else: - _get_class_by_name(name).add_to(self.stac_object.owner) - - def remove(self, name: EXTENSION_NAMES) -> None: - """Remove the given extension from the owner - - Args: - name : Extension identifier (eg: 'eo') - """ - if self.stac_object.owner is None: - raise STACError( - f"Attempted to remove extension='{name}' for an object with no owner. " - "Use `.set_owner` and then try to remove the extension again." - ) - else: - _get_class_by_name(name).remove_from(self.stac_object.owner) - - -class _AssetExt(_AssetsExt[U]): - stac_object: U - - @property - def classification(self) -> ClassificationExtension[U]: - return ClassificationExtension.ext(self.stac_object) - - @property - def cube(self) -> DatacubeExtension[U]: - return DatacubeExtension.ext(self.stac_object) - - @property - def eo(self) -> EOExtension[U]: - return EOExtension.ext(self.stac_object) - - @property - def pc(self) -> PointcloudExtension[U]: - return PointcloudExtension.ext(self.stac_object) - - @property - def proj(self) -> ProjectionExtension[U]: - return ProjectionExtension.ext(self.stac_object) - - @property - def raster(self) -> RasterExtension[U]: - return RasterExtension.ext(self.stac_object) - - @property - def sar(self) -> SarExtension[U]: - return SarExtension.ext(self.stac_object) - - @property - def sat(self) -> SatExtension[U]: - return SatExtension.ext(self.stac_object) - - @property - def storage(self) -> StorageExtension[U]: - return StorageExtension.ext(self.stac_object) - - @property - def table(self) -> TableExtension[U]: - return TableExtension.ext(self.stac_object) - - @property - def version(self) -> BaseVersionExtension[U]: - return BaseVersionExtension.ext(self.stac_object) - - @property - def view(self) -> ViewExtension[U]: - return ViewExtension.ext(self.stac_object) - - -@dataclass -class AssetExt(_AssetExt[Asset]): - """Supporting the :attr:`~pystac.Asset.ext` accessor for interacting - with extension classes - """ - - stac_object: Asset - - @property - def file(self) -> FileExtension[Asset]: - return FileExtension.ext(self.stac_object) - - @property - def timestamps(self) -> TimestampsExtension[Asset]: - return TimestampsExtension.ext(self.stac_object) - - @property - def xarray(self) -> XarrayAssetsExtension[Asset]: - return XarrayAssetsExtension.ext(self.stac_object) - - -@dataclass -class ItemAssetExt(_AssetExt[ItemAssetDefinition]): - """Supporting the :attr:`~pystac.ItemAssetDefinition.ext` accessor for interacting - with extension classes - """ - - stac_object: ItemAssetDefinition - - -@dataclass -class LinkExt(_AssetsExt[Link]): - """Supporting the :attr:`~pystac.Link.ext` accessor for interacting - with extension classes - """ - - stac_object: Link - - @property - def file(self) -> FileExtension[Link]: - return FileExtension.ext(self.stac_object) diff --git a/pystac/extensions/file.py b/pystac/extensions/file.py deleted file mode 100644 index c6e24bd91..000000000 --- a/pystac/extensions/file.py +++ /dev/null @@ -1,384 +0,0 @@ -"""Implements the :stac-ext:`File Info Extension `.""" - -from __future__ import annotations - -import warnings -from collections.abc import Iterable -from typing import Any, Generic, Literal, TypeVar, cast - -from pystac import ( - Asset, - Catalog, - Collection, - ExtensionTypeError, - Item, - Link, - STACObjectType, -) -from pystac.extensions.base import ExtensionManagementMixin, PropertiesExtension -from pystac.extensions.hooks import ExtensionHooks -from pystac.serialization.identify import ( - OldExtensionShortIDs, - STACJSONDescription, - STACVersionID, -) -from pystac.utils import StringEnum, get_required, map_opt - -#: Generalized version of :class:`~pystac.Asset`, :class:`~pystac.Link`, -T = TypeVar("T", Asset, Link) - -SCHEMA_URI = "https://stac-extensions.github.io/file/v2.1.0/schema.json" - -PREFIX = "file:" -BYTE_ORDER_PROP = PREFIX + "byte_order" -CHECKSUM_PROP = PREFIX + "checksum" -HEADER_SIZE_PROP = PREFIX + "header_size" -SIZE_PROP = PREFIX + "size" -VALUES_PROP = PREFIX + "values" -LOCAL_PATH_PROP = PREFIX + "local_path" - - -class ByteOrder(StringEnum): - """List of allows values for the ``"file:byte_order"`` field defined by the - :stac-ext:`File Info Extension `.""" - - LITTLE_ENDIAN = "little-endian" - BIG_ENDIAN = "big-endian" - - -class MappingObject: - """Represents a value map used by assets that are used as classification layers, and - give details about the values in the asset and their meanings.""" - - properties: dict[str, Any] - - def __init__(self, properties: dict[str, Any]) -> None: - self.properties = properties - - def apply(self, values: list[Any], summary: str) -> None: - """Sets the properties for this :class:`~MappingObject` instance. - - Args: - values : The value(s) in the file. At least one array element is required. - summary : A short description of the value(s). - """ - self.values = values - self.summary = summary - - @classmethod - def create(cls, values: list[Any], summary: str) -> MappingObject: - """Creates a new :class:`~MappingObject` instance. - - Args: - values : The value(s) in the file. At least one array element is required. - summary : A short description of the value(s). - """ - m = cls({}) - m.apply(values=values, summary=summary) - return m - - @property - def values(self) -> list[Any]: - """Gets or sets the list of value(s) in the file. At least one array element is - required.""" - return get_required(self.properties.get("values"), self, "values") - - @values.setter - def values(self, v: list[Any]) -> None: - self.properties["values"] = v - - @property - def summary(self) -> str: - """Gets or sets the short description of the value(s).""" - return get_required(self.properties.get("summary"), self, "summary") - - @summary.setter - def summary(self, v: str) -> None: - self.properties["summary"] = v - - @classmethod - def from_dict(cls, d: dict[str, Any]) -> MappingObject: - return cls.create(**d) - - def to_dict(self) -> dict[str, Any]: - return self.properties - - -class FileExtension( - Generic[T], - PropertiesExtension, - ExtensionManagementMixin[Catalog | Collection | Item], -): - """A class that can be used to extend the properties of an :class:`~pystac.Asset` - or :class:`~pystac.Link` with properties from the - :stac-ext:`File Info Extension `. - - To create an instance of :class:`FileExtension`, use the - :meth:`FileExtension.ext` method. For example: - - .. code-block:: python - - >>> asset: pystac.Asset = ... - >>> file_ext = FileExtension.ext(asset) - """ - - name: Literal["file"] = "file" - - def apply( - self, - byte_order: ByteOrder | None = None, - checksum: str | None = None, - header_size: int | None = None, - size: int | None = None, - values: list[MappingObject] | None = None, - local_path: str | None = None, - ) -> None: - """Applies file extension properties to the extended Item. - - Args: - byte_order : Optional byte order of integer values in the file. One of - ``"big-endian"`` or ``"little-endian"``. - checksum : Optional multihash for the corresponding file, - encoded as hexadecimal (base 16) string with lowercase letters. - header_size : Optional header size of the file, in bytes. - size : Optional size of the file, in bytes. - values : Optional list of :class:`~MappingObject` instances that lists the - values that are in the file and describe their meaning. See the - :stac-ext:`Mapping Object ` docs for an example. - If given, at least one array element is required. - """ - self.byte_order = byte_order - self.checksum = checksum - self.header_size = header_size - self.size = size - self.values = values - self.local_path = local_path - - @property - def byte_order(self) -> ByteOrder | None: - """Gets or sets the byte order of integer values in the file. One of big-endian - or little-endian.""" - return self._get_property(BYTE_ORDER_PROP, ByteOrder) - - @byte_order.setter - def byte_order(self, v: ByteOrder | None) -> None: - self._set_property(BYTE_ORDER_PROP, v) - - @property - def checksum(self) -> str | None: - """Get or sets the multihash for the corresponding file, encoded as hexadecimal - (base 16) string with lowercase letters.""" - return self._get_property(CHECKSUM_PROP, str) - - @checksum.setter - def checksum(self, v: str | None) -> None: - self._set_property(CHECKSUM_PROP, v) - - @property - def header_size(self) -> int | None: - """Get or sets the header size of the file, in bytes.""" - return self._get_property(HEADER_SIZE_PROP, int) - - @header_size.setter - def header_size(self, v: int | None) -> None: - self._set_property(HEADER_SIZE_PROP, v) - - @property - def local_path(self) -> str | None: - """Get or sets a relative local path for the asset/link. - - The ``file:local_path`` field indicates a **relative** path that - can be used by clients for different purposes to organize the - files locally. For compatibility reasons the name-separator - character in paths **must** be ``/`` and the Windows separator ``\\`` - is **not** allowed. - """ - return self._get_property(LOCAL_PATH_PROP, str) - - @local_path.setter - def local_path(self, v: str | None) -> None: - self._set_property(LOCAL_PATH_PROP, v, pop_if_none=True) - - @property - def size(self) -> int | None: - """Get or sets the size of the file, in bytes.""" - return self._get_property(SIZE_PROP, int) - - @size.setter - def size(self, v: int | None) -> None: - self._set_property(SIZE_PROP, v) - - @property - def values(self) -> list[MappingObject] | None: - """Get or sets the list of :class:`~MappingObject` instances that lists the - values that are in the file and describe their meaning. See the - :stac-ext:`Mapping Object ` docs for an example. If given, - at least one array element is required.""" - return map_opt( - lambda values: [ - MappingObject.from_dict(mapping_obj) for mapping_obj in values - ], - self._get_property(VALUES_PROP, list[dict[str, Any]]), - ) - - @values.setter - def values(self, v: list[MappingObject] | None) -> None: - self._set_property( - VALUES_PROP, - map_opt( - lambda values: [mapping_obj.to_dict() for mapping_obj in values], v - ), - ) - - @classmethod - def get_schema_uri(cls) -> str: - return SCHEMA_URI - - @classmethod - def ext(cls, obj: Asset | Link, add_if_missing: bool = False) -> FileExtension[T]: - """Extends the given STAC Object with properties from the :stac-ext:`File Info - Extension `. - - This extension can be applied to instances of :class:`~pystac.Asset` or - :class:`~pystac.Link` - """ - if isinstance(obj, Asset): - cls.ensure_owner_has_extension(obj, add_if_missing) - return cast(FileExtension[T], AssetFileExtension(obj)) - elif isinstance(obj, Link): - cls.ensure_owner_has_extension(obj, add_if_missing) - return cast(FileExtension[T], LinkFileExtension(obj)) - else: - raise ExtensionTypeError(cls._ext_error_message(obj)) - - -class AssetFileExtension(FileExtension[Asset]): - """A concrete implementation of :class:`FileExtension` on an - :class:`~pystac.Asset` that extends the Asset fields to include properties defined - in the :stac-ext:`File Info Extension `. - - This class should generally not be instantiated directly. Instead, call - :meth:`FileExtension.ext` on an :class:`~pystac.Asset` to extend it. - """ - - asset_href: str - """The ``href`` value of the :class:`~pystac.Asset` being extended.""" - - properties: dict[str, Any] - """The :class:`~pystac.Asset` fields, including extension properties.""" - - additional_read_properties: Iterable[dict[str, Any]] | None = None - """If present, this will be a list containing 1 dictionary representing the - properties of the owner.""" - - def __init__(self, asset: Asset): - self.asset_href = asset.href - self.properties = asset.extra_fields - if asset.owner and hasattr(asset.owner, "properties"): - self.additional_read_properties = [asset.owner.properties] - - def __repr__(self) -> str: - return f"" - - -class LinkFileExtension(FileExtension[Link]): - """A concrete implementation of :class:`FileExtension` on an - :class:`~pystac.Link` that extends the Link fields to include properties defined - in the :stac-ext:`File Info Extension `. - - This class should generally not be instantiated directly. Instead, call - :meth:`FileExtension.ext` on an :class:`~pystac.Link` to extend it. - """ - - link_href: str - """The ``href`` value of the :class:`~pystac.Link` being extended.""" - - properties: dict[str, Any] - """The :class:`~pystac.Link` fields, including extension properties.""" - - additional_read_properties: Iterable[dict[str, Any]] | None = None - """If present, this will be a list containing 1 dictionary representing the - properties of the owner.""" - - def __init__(self, link: Link): - self.link_href = link.href - self.properties = link.extra_fields - if link.owner and hasattr(link.owner, "properties"): - self.additional_read_properties = [link.owner.properties] - - def __repr__(self) -> str: - return f"" - - -class FileExtensionHooks(ExtensionHooks): - schema_uri: str = SCHEMA_URI - prev_extension_ids = { - "file", - "https://stac-extensions.github.io/file/v1.0.0/schema.json", - "https://stac-extensions.github.io/file/v2.0.0/schema.json", - } - stac_object_types = { - STACObjectType.ITEM, - STACObjectType.COLLECTION, - STACObjectType.CATALOG, - } - removed_fields = { - "file:bits_per_sample", - "file:data_type", - "file:nodata", - "file:unit", - } - - def migrate( - self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription - ) -> None: - # The checksum field was previously it's own extension. - old_checksum: dict[str, str] | None = None - if info.version_range.latest_valid_version() < "v1.0.0-rc.2": - if OldExtensionShortIDs.CHECKSUM.value in info.extensions: - old_item_checksum = obj["properties"].get("checksum:multihash") - if old_item_checksum is not None: - if old_checksum is None: - old_checksum = {} - old_checksum["__item__"] = old_item_checksum - for asset_key, asset in obj["assets"].items(): - old_asset_checksum = asset.get("checksum:multihash") - if old_asset_checksum is not None: - if old_checksum is None: - old_checksum = {} - old_checksum[asset_key] = old_asset_checksum - - try: - obj["stac_extensions"].remove(OldExtensionShortIDs.CHECKSUM.value) - except ValueError: - pass - - super().migrate(obj, version, info) - - if old_checksum is not None: - if SCHEMA_URI not in obj["stac_extensions"]: - obj["stac_extensions"].append(SCHEMA_URI) - for key in old_checksum: - if key == "__item__": - obj["properties"][CHECKSUM_PROP] = old_checksum[key] - else: - obj["assets"][key][CHECKSUM_PROP] = old_checksum[key] - - found_fields = {} - for asset_key, asset in obj.get("assets", {}).items(): - if values := set(asset.keys()).intersection(self.removed_fields): - found_fields[asset_key] = values - - if found_fields: - warnings.warn( - f"Assets {list(found_fields.keys())} contain fields: " - f"{list(set.union(*found_fields.values()))} which " - "were removed from the file extension spec in v2.0.0. Please " - "consult the release notes " - "(https://github.com/stac-extensions/file/releases/tag/v2.0.0) " - "for instructions on how to migrate these fields.", - UserWarning, - ) - - -FILE_EXTENSION_HOOKS: ExtensionHooks = FileExtensionHooks() diff --git a/pystac/extensions/grid.py b/pystac/extensions/grid.py deleted file mode 100644 index 9640a2c7f..000000000 --- a/pystac/extensions/grid.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Implements the :stac-ext:`Grid Extension `.""" - -from __future__ import annotations - -import re -import warnings -from re import Pattern -from typing import Any, Literal - -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.1.0/schema.json" -SCHEMA_URIS: list[str] = [ - "https://stac-extensions.github.io/grid/v1.0.0/schema.json", - SCHEMA_URI, -] -PREFIX: str = "grid:" - -# Field names -CODE_PROP: str = PREFIX + "code" # required - -CODE_REGEX: str = r"[A-Z0-9]+-[-_.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[pystac.Item | pystac.Collection], -): - """A concrete implementation of :class:`~pystac.extensions.grid.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:`~pystac.extensions.grid.GridExtension.ext` on an :class:`~pystac.Item` - to extend it. - - .. code-block:: python - - >>> item: pystac.Item = ... - >>> proj_ext = GridExtension.ext(item) - """ - - name: Literal["grid"] = "grid" - - 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 f"" - - 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) -> str | None: - """Get or sets the grid code 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 get_schema_uris(cls) -> list[str]: - warnings.warn( - "get_schema_uris is deprecated and will be removed in v2", - DeprecationWarning, - ) - return SCHEMA_URIS - - @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.ensure_has_extension(obj, add_if_missing) - return GridExtension(obj) - else: - raise pystac.ExtensionTypeError(cls._ext_error_message(obj)) - - -class GridExtensionHooks(ExtensionHooks): - schema_uri: str = SCHEMA_URI - prev_extension_ids: set[str] = {*[uri for uri in SCHEMA_URIS if uri != SCHEMA_URI]} - stac_object_types = {pystac.STACObjectType.ITEM} - - -GRID_EXTENSION_HOOKS: ExtensionHooks = GridExtensionHooks() diff --git a/pystac/extensions/hooks.py b/pystac/extensions/hooks.py deleted file mode 100644 index 8db8ca4e4..000000000 --- a/pystac/extensions/hooks.py +++ /dev/null @@ -1,111 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from collections.abc import Iterable -from functools import lru_cache -from typing import TYPE_CHECKING, Any - -import pystac -from pystac.extensions.base import VERSION_REGEX -from pystac.serialization.identify import STACJSONDescription, STACVersionID - -if TYPE_CHECKING: - from pystac.stac_object import STACObject - - -class ExtensionHooks(ABC): - @property - @abstractmethod - def schema_uri(self) -> str: - """The schema_uri for the current version of this extension""" - raise NotImplementedError - - @property - @abstractmethod - def prev_extension_ids(self) -> set[str]: - """A set of previous extension IDs (schema URIs or old short ids) - that should be migrated to the latest schema URI in the 'stac_extensions' - property. Override with a class attribute so that the set of previous - IDs is only created once. - """ - raise NotImplementedError - - @property - @abstractmethod - def stac_object_types(self) -> set[pystac.STACObjectType]: - """A set of STACObjectType for which migration logic will be applied.""" - raise NotImplementedError - - @lru_cache - def _get_stac_object_types(self) -> set[str]: - """Translation of stac_object_types to strings, cached""" - return {x.value for x in self.stac_object_types} - - def get_object_links(self, obj: STACObject) -> list[str | pystac.RelType] | None: - return None - - def has_extension(self, obj: dict[str, Any]) -> bool: - schema_startswith = VERSION_REGEX.split(self.schema_uri)[0] + "/" - return any( - uri.startswith(schema_startswith) or uri in self.prev_extension_ids - for uri in obj.get("stac_extensions", []) - ) - - def migrate( - self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription - ) -> None: - """Migrate a STAC Object in dict format from a previous version. - The base implementation will update the stac_extensions to the latest - schema ID. This method will only be called for STAC objects that have been - identified as a previous version of STAC. Implementations should directly - manipulate the obj dict. Remember to call super() in order to change out - the old 'stac_extension' entry with the latest schema URI. - """ - # Migrate schema versions - for prev_id in self.prev_extension_ids: - if prev_id in info.extensions: - try: - i = obj["stac_extensions"].index(prev_id) - obj["stac_extensions"][i] = self.schema_uri - except ValueError: - obj["stac_extensions"].append(self.schema_uri) - break - - -class RegisteredExtensionHooks: - hooks: dict[str, ExtensionHooks] - - def __init__(self, hooks: Iterable[ExtensionHooks]): - self.hooks = {e.schema_uri: e for e in hooks} - - def add_extension_hooks(self, hooks: ExtensionHooks) -> None: - e_id = hooks.schema_uri - if e_id in self.hooks: - raise pystac.ExtensionAlreadyExistsError( - f"ExtensionDefinition with id '{e_id}' already exists." - ) - - self.hooks[e_id] = hooks - - def remove_extension_hooks(self, extension_id: str) -> None: - if extension_id in self.hooks: - del self.hooks[extension_id] - - def get_extended_object_links(self, obj: STACObject) -> list[str | pystac.RelType]: - result: list[str | pystac.RelType] | None = None - for ext in obj.stac_extensions: - if ext in self.hooks: - ext_result = self.hooks[ext].get_object_links(obj) - if ext_result is not None: - if result is None: - result = ext_result - else: - result.extend(ext_result) - return result or [] - - def migrate( - self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription - ) -> None: - for hooks in self.hooks.values(): - if info.object_type in hooks._get_stac_object_types(): - hooks.migrate(obj, version, info) diff --git a/pystac/extensions/item_assets.py b/pystac/extensions/item_assets.py deleted file mode 100644 index 140207275..000000000 --- a/pystac/extensions/item_assets.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Implements the :stac-ext:`Item Assets Definition Extension `.""" - -from __future__ import annotations - -import warnings -from typing import Any, Literal - -import pystac -from pystac.errors import DeprecatedWarning -from pystac.extensions.base import ExtensionManagementMixin -from pystac.extensions.hooks import ExtensionHooks -from pystac.item_assets import ItemAssetDefinition -from pystac.serialization.identify import STACJSONDescription, STACVersionID -from pystac.utils import get_required - -SCHEMA_URI = "https://stac-extensions.github.io/item-assets/v1.0.0/schema.json" - -ITEM_ASSETS_PROP = "item_assets" - - -class AssetDefinition(ItemAssetDefinition): - """ - DEPRECATED - - .. deprecated:: 1.12.0 - Use :class:`~pystac.ItemAssetDefinition` instead. - """ - - def __init__(cls, *args: Any, **kwargs: Any) -> None: - warnings.warn( - ( - "``AssetDefinition`` is deprecated. " - "Please use ``pystac.ItemAssetDefinition`` instead." - ), - DeprecationWarning, - ) - super().__init__(*args, **kwargs) - - -class ItemAssetsExtension(ExtensionManagementMixin[pystac.Collection]): - """ - DEPRECATED - - .. deprecated:: 1.12.0 - Use :attr:`~pystac.Collection.item_assets` instead. - """ - - name: Literal["item_assets"] = "item_assets" - collection: pystac.Collection - - def __init__(self, collection: pystac.Collection) -> None: - warnings.warn( - ( - "The ``item_assets`` extension is deprecated. " - "``item_assets`` is now a top-level property of ``Collection``." - ), - DeprecatedWarning, - ) - self.collection = collection - - @property - def item_assets(self) -> dict[str, ItemAssetDefinition]: - """Gets or sets a dictionary of assets that can be found in member Items. Maps - the asset key to an :class:`AssetDefinition` instance.""" - result: dict[str, Any] = get_required( - self.collection.extra_fields.get(ITEM_ASSETS_PROP), self, ITEM_ASSETS_PROP - ) - return {k: ItemAssetDefinition(v, self.collection) for k, v in result.items()} - - @item_assets.setter - def item_assets(self, v: dict[str, ItemAssetDefinition]) -> None: - self.collection.extra_fields[ITEM_ASSETS_PROP] = { - k: asset_def.properties for k, asset_def in v.items() - } - - def __repr__(self) -> str: - return f"" - - @classmethod - def get_schema_uri(cls) -> str: - return SCHEMA_URI - - @classmethod - def ext( - cls, obj: pystac.Collection, add_if_missing: bool = False - ) -> ItemAssetsExtension: - """Extends the given :class:`~pystac.Collection` with properties from the - :stac-ext:`Item Assets Extension `. - - Raises: - - pystac.ExtensionTypeError : If an invalid object type is passed. - """ - if isinstance(obj, pystac.Collection): - cls.ensure_has_extension(obj, add_if_missing) - return cls(obj) - else: - raise pystac.ExtensionTypeError(cls._ext_error_message(obj)) - - -class ItemAssetsExtensionHooks(ExtensionHooks): - schema_uri: str = SCHEMA_URI - prev_extension_ids = {"asset", "item-assets"} - stac_object_types = {pystac.STACObjectType.COLLECTION} - - def migrate( - self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription - ) -> None: - # Handle that the "item-assets" extension had the id of "assets", before - # collection assets (since removed) took over the ID of "assets" - if version < "1.0.0-beta.1" and "asset" in info.extensions: - if "assets" in obj: - obj["item_assets"] = obj["assets"] - del obj["assets"] - - super().migrate(obj, version, info) - - # As of STAC spec version 1.1.0 item-assets are part of core - if obj["stac_version"] >= "1.1.0" and self.schema_uri in obj.get( - "stac_extensions", [] - ): - obj["stac_extensions"].remove(self.schema_uri) - - -ITEM_ASSETS_EXTENSION_HOOKS: ExtensionHooks = ItemAssetsExtensionHooks() diff --git a/pystac/extensions/label.py b/pystac/extensions/label.py deleted file mode 100644 index c8595413e..000000000 --- a/pystac/extensions/label.py +++ /dev/null @@ -1,848 +0,0 @@ -"""Implements the :stac-ext:`Label Extension