diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 6d4dc472c..3d95c1921 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -32,23 +32,25 @@ jobs: experimental: - false include: - - python-version: "3.11.0-alpha.3" + - python-version: "3.11.0-alpha.7" os: ubuntu-latest experimental: true - - python-version: "3.11.0-alpha.3" + - python-version: "3.11.0-alpha.7" os: windows-latest experimental: true - - python-version: "3.11.0-alpha.3" + - python-version: "3.11.0-alpha.7" os: macos-latest experimental: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: requirements-test.txt - uses: actions-rs/toolchain@v1 # Wheels for orjson are not available for Python 3.11. This sets up the Rust @@ -60,30 +62,6 @@ jobs: default: true profile: minimal - - name: Cache dependencies (Linux) - if: startsWith(runner.os, 'Linux') - uses: actions/cache@v2 - with: - path: ~/.cache/pip - # Cache based on OS, Python version, and dependency hash - key: pip-test-${{ runner.os }}-python${{ matrix.python-version }}-${{ hashFiles('requirements-test.txt') }} - - - name: Cache dependencies (macOS) - if: startsWith(runner.os, 'macOS') - uses: actions/cache@v2 - with: - path: ~/Library/Caches/pip - # Cache based on OS, Python version, and dependency hash - key: pip-test-${{ runner.os }}-python${{ matrix.python-version }}-${{ hashFiles('requirements-test.txt') }} - - - name: Cache dependencies (Windows) - if: startsWith(runner.os, 'Windows') - uses: actions/cache@v2 - with: - path: ~\AppData\Local\pip\Cache - # Cache based on OS, Python version, and dependency hash - key: pip-test-${{ runner.os }}-python${{ matrix.python-version }}-${{ hashFiles('requirements-test.txt') }} - - name: Install dependencies run: | pip install --upgrade pip @@ -100,19 +78,14 @@ jobs: name: coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.8 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: "3.8" - - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ~/.cache/pip - # Cache based on OS, Python version, and dependency hash - key: pip-test-${{ runner.os }}-python3.8-${{ hashFiles('requirements-test.txt') }} + cache: 'pip' + cache-dependency-path: requirements-test.txt - name: Install dependencies run: | @@ -131,7 +104,7 @@ jobs: run: coverage xml --fail-under 0 - name: Upload All coverage to Codecov - uses: codecov/codecov-action@v2.1.0 + uses: codecov/codecov-action@v3 if: ${{ env.GITHUB_REPOSITORY }} == 'stac-utils/pystac' with: token: ${{ secrets.CODECOV_TOKEN }} @@ -154,22 +127,17 @@ jobs: - "3.8" - "3.9" - "3.10" - - "3.11.0-alpha.3" + - "3.11.0-alpha.7" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ~/.cache/pip - # Cache based on OS, Python version, and dependency hash - key: lint-${{ runner.os }}-python${{ matrix.python-version }}-${{ hashFiles('requirements-test.txt') }} + cache: 'pip' + cache-dependency-path: requirements-test.txt - name: Install dependencies run: | @@ -182,9 +150,9 @@ jobs: vanilla: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: python-version: "3.8" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e3a904424..841424785 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest if: ${{ github.repository }} == 'stac-utils/pystac' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.x - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: "3.x" diff --git a/CHANGELOG.md b/CHANGELOG.md index e37491a39..db5e5d6ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,33 @@ ### Fixed +## [v1.5.0] + +### Added + +- Enum MediaType entry for PDF documents ([#758](https://github.com/stac-utils/pystac/pull/758)) +- Enum MediaType entry for HTML documents ([#816](https://github.com/stac-utils/pystac/pull/816)) +- Updated Link to obtain stac_io from owner root ([#762](https://github.com/stac-utils/pystac/pull/762)) +- Replace test.com with special-use domain name. ([#769](https://github.com/stac-utils/pystac/pull/769)) +- Updated AssetDefinition to have create, apply methods ([#768](https://github.com/stac-utils/pystac/pull/768)) +- Add Grid Extension support ([#799](https://github.com/stac-utils/pystac/pull/799)) +- Rich HTML representations for Jupyter Notebook display ([#743](https://github.com/stac-utils/pystac/pull/743)) +- Add `assets` argument to `Item` and `Collection` init methods to allow adding Assets during object initialization ([#834](https://github.com/stac-utils/pystac/pull/834)) + +### Changed + +- Updated Raster Extension from v1.0.0 to v1.1.0 ([#809](https://github.com/stac-utils/pystac/pull/809)) + +### Fixed + +- Mutating `Asset.extra_fields` on a cloned `Asset` also mutated the original asset ([#826](https://github.com/stac-utils/pystac/pull/826)) +- "How to create STAC catalogs" tutorial ([#775](https://github.com/stac-utils/pystac/pull/775)) +- Add a `variables` argument, to accompany `dimensions`, for the `apply` method of stac objects extended with datacube ([#782](https://github.com/stac-utils/pystac/pull/782)) +- Deepcopy collection properties on clone. Implement `clone` method for `Summaries` ([#794](https://github.com/stac-utils/pystac/pull/794)) +- Collection assets are now preserved when using `Collection.clone` ([#834](https://github.com/stac-utils/pystac/pull/834)) +- Docstrings for `StacIO.read_text` and `StacIO.write_text` now match the type annotations for the `source` argument. ([#835](https://github.com/stac-utils/pystac/pull/835)) +- UTC timestamps now always have `tzutc` timezone even when system timezone is set to UTC. ([#848](https://github.com/stac-utils/pystac/pull/848)) + ## [v1.4.0] ### Added @@ -575,7 +602,8 @@ use `Band.create` Initial release. -[Unreleased]: +[Unreleased]: +[v1.5.0]: [v1.4.0]: [v1.3.0]: [v1.2.0]: diff --git a/README.md b/README.md index 66e5929a3..07aa7291e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ ## PySTAC ![Build Status](https://github.com/stac-utils/pystac/workflows/CI/badge.svg?branch=main) [![PyPI version](https://badge.fury.io/py/pystac.svg)](https://badge.fury.io/py/pystac) +[![Conda (channel only)](https://img.shields.io/conda/vn/conda-forge/pystac)](https://anaconda.org/conda-forge/pystac) [![Documentation](https://readthedocs.org/projects/pystac/badge/?version=latest)](https://pystac.readthedocs.io/en/latest/) [![codecov](https://codecov.io/gh/stac-utils/pystac/branch/main/graph/badge.svg)](https://codecov.io/gh/stac-utils/pystac) [![Gitter](https://badges.gitter.im/SpatioTemporal-Asset-Catalog/python.svg)](https://gitter.im/SpatioTemporal-Asset-Catalog/python?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) @@ -20,7 +21,9 @@ Note that while we support Python 3.10.\*, wheels for the `orjson` library are n platforms. If you install PySTAC with the `orjson` extra, you may need to have the Rust toolchain installed (e.g. via [rustup](https://rustup.rs/)) in order to build the package from source. -Support for Python 3.11 should be considered experimental until further notice. +Support for Python 3.11 should be considered experimental until further notice. There is a known issue with failing build of `orjson` on 3.11.0 alpha releases prior to alpha.6 (see +[#765(comment)](https://github.com/stac-utils/pystac/pull/765#pullrequestreview-908908772) for +some additional detail). PySTAC has a single required dependency (`python-dateutil`). PySTAC can be installed from pip or the source repository. diff --git a/docs/concepts.rst b/docs/concepts.rst index 8d2ac863d..bdf82024c 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -279,7 +279,7 @@ for reading from AWS's S3 cloud object storage using `boto3 from pystac.stac_io import DefaultStacIO, StacIO class CustomStacIO(DefaultStacIO): - def __init__(): + def __init__(self): self.s3 = boto3.resource("s3") def read_text( @@ -302,8 +302,7 @@ for reading from AWS's S3 cloud object storage using `boto3 if parsed.scheme == "s3": bucket = parsed.netloc key = parsed.path[1:] - s3 = boto3.resource("s3") - s3.Object(bucket, key).put(Body=txt, ContentEncoding="utf-8") + self.s3.Object(bucket, key).put(Body=txt, ContentEncoding="utf-8") else: super().write_text(dest, txt, *args, **kwargs) @@ -322,7 +321,7 @@ to take advantage of connection pooling using a `requests.Session from pystac.stac_io import DefaultStacIO, StacIO class ConnectionPoolingIO(DefaultStacIO): - def __init__(): + def __init__(self): self.session = requests.Session() def read_text( diff --git a/docs/conf.py b/docs/conf.py index 4c7a93382..7e5717210 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,7 +17,6 @@ import subprocess from typing import Any, Dict, List -from sphinx.util import logging sys.path.insert(0, os.path.abspath(".")) sys.path.insert(0, os.path.abspath("../")) @@ -239,11 +238,3 @@ # -- Substutition variables rst_epilog = f".. |stac_version| replace:: {STACVersion.DEFAULT_STAC_VERSION}" - -# -- Suppress warnings from the extlinks extension -# We do this to avoid warnings like the following in our Jupyter notebook tutorials -# where we do not want to use Sphinx constructs: -# WARNING: hardcoded link 'https://github.com/stac-extensions/eo' could be replaced -# by an extlink (try using ':stac-ext:`eo`' instead) -linklogger = logging.getLogger("sphinx.ext.extlinks") -linklogger.setLevel(40) # Ignore messages less severe than ERROR diff --git a/docs/tutorials/how-to-create-stac-catalogs.ipynb b/docs/tutorials/how-to-create-stac-catalogs.ipynb index 81d7433d3..3e8a6f8a3 100644 --- a/docs/tutorials/how-to-create-stac-catalogs.ipynb +++ b/docs/tutorials/how-to-create-stac-catalogs.ipynb @@ -12,7 +12,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This notebook runs through some of the basics of using PySTAC to create a static STAC. It was part of a 30 minute presentation at the [community STAC sprint](https://github.com/radiantearth/community-sprints/tree/master/11052019-arlignton-va) in Arlington, VA in November 2019." + "This notebook runs through some of the basics of using PySTAC to create a static STAC. It was part of a 30 minute presentation at the [community STAC sprint](https://github.com/radiantearth/community-sprints/tree/master/11052019-arlignton-va) in Arlington, VA in November 2019, updated to work with current PySTAC." ] }, { @@ -24,44 +24,38 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Requirement already satisfied: boto3 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (1.10.8)\n", - "Requirement already satisfied: botocore<1.14.0,>=1.13.8 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from boto3) (1.13.8)\n", - "Requirement already satisfied: s3transfer<0.3.0,>=0.2.0 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from boto3) (0.2.1)\n", - "Requirement already satisfied: jmespath<1.0.0,>=0.7.1 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from boto3) (0.9.4)\n", - "Requirement already satisfied: urllib3<1.26,>=1.20; python_version >= \"3.4\" in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from botocore<1.14.0,>=1.13.8->boto3) (1.25.6)\n", - "Requirement already satisfied: docutils<0.16,>=0.10 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from botocore<1.14.0,>=1.13.8->boto3) (0.15.2)\n", - "Requirement already satisfied: python-dateutil<3.0.0,>=2.1; python_version >= \"2.7\" in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from botocore<1.14.0,>=1.13.8->boto3) (2.8.1)\n", - "Requirement already satisfied: six>=1.5 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from python-dateutil<3.0.0,>=2.1; python_version >= \"2.7\"->botocore<1.14.0,>=1.13.8->boto3) (1.12.0)\n", - "\u001b[33mWARNING: You are using pip version 20.1.1; however, version 20.2 is available.\n", - "You should consider upgrading via the '/Users/rob/proj/stac/pystac/venv/bin/python -m pip install --upgrade pip' command.\u001b[0m\n", - "Requirement already satisfied: rasterio in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (1.1.0)\n", - "Requirement already satisfied: numpy in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from rasterio) (1.17.3)\n", - "Requirement already satisfied: snuggs>=1.4.1 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from rasterio) (1.4.7)\n", - "Requirement already satisfied: click<8,>=4.0 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from rasterio) (7.0)\n", - "Requirement already satisfied: click-plugins in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from rasterio) (1.1.1)\n", - "Requirement already satisfied: attrs in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from rasterio) (19.3.0)\n", - "Requirement already satisfied: cligj>=0.5 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from rasterio) (0.5.0)\n", - "Requirement already satisfied: affine in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from rasterio) (2.3.0)\n", - "Requirement already satisfied: pyparsing>=2.1.6 in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (from snuggs>=1.4.1->rasterio) (2.4.2)\n", - "\u001b[33mWARNING: You are using pip version 20.1.1; however, version 20.2 is available.\n", - "You should consider upgrading via the '/Users/rob/proj/stac/pystac/venv/bin/python -m pip install --upgrade pip' command.\u001b[0m\n", - "Requirement already satisfied: shapely in /Users/rob/proj/stac/pystac/venv/lib/python3.6/site-packages (1.6.4.post2)\n", - "\u001b[33mWARNING: You are using pip version 20.1.1; however, version 20.2 is available.\n", - "You should consider upgrading via the '/Users/rob/proj/stac/pystac/venv/bin/python -m pip install --upgrade pip' command.\u001b[0m\n" + "Requirement already satisfied: boto3 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (1.21.28)\n", + "Requirement already satisfied: rasterio in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (1.2.10)\n", + "Requirement already satisfied: shapely in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (1.8.1.post1)\n", + "Requirement already satisfied: pystac in /Users/gadomski/Code/pystac (1.3.0)\n", + "Requirement already satisfied: jmespath<2.0.0,>=0.7.1 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from boto3) (1.0.0)\n", + "Requirement already satisfied: botocore<1.25.0,>=1.24.28 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from boto3) (1.24.28)\n", + "Requirement already satisfied: s3transfer<0.6.0,>=0.5.0 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from boto3) (0.5.2)\n", + "Requirement already satisfied: certifi in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (2021.5.30)\n", + "Requirement already satisfied: numpy in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (1.22.3)\n", + "Requirement already satisfied: click-plugins in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (1.1.1)\n", + "Requirement already satisfied: click>=4.0 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (8.0.1)\n", + "Requirement already satisfied: snuggs>=1.4.1 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (1.4.7)\n", + "Requirement already satisfied: cligj>=0.5 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (0.7.2)\n", + "Requirement already satisfied: affine in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (2.3.1)\n", + "Requirement already satisfied: setuptools in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (56.0.0)\n", + "Requirement already satisfied: attrs in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from rasterio) (21.2.0)\n", + "Requirement already satisfied: python-dateutil>=2.7.0 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from pystac) (2.8.1)\n", + "Requirement already satisfied: urllib3<1.27,>=1.25.4 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from botocore<1.25.0,>=1.24.28->boto3) (1.26.5)\n", + "Requirement already satisfied: six>=1.5 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from python-dateutil>=2.7.0->pystac) (1.16.0)\n", + "Requirement already satisfied: pyparsing>=2.1.6 in /Users/gadomski/.virtualenvs/pystac/lib/python3.9/site-packages (from snuggs>=1.4.1->rasterio) (2.4.7)\n" ] } ], "source": [ - "!pip install boto3\n", - "!pip install rasterio\n", - "!pip install shapely" + "%pip install boto3 rasterio shapely pystac" ] }, { @@ -73,7 +67,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -96,7 +90,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -110,17 +104,17 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "('/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/image.tif',\n", - " )" + "('/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/image.tif',\n", + " )" ] }, - "execution_count": 4, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -141,7 +135,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -155,24 +149,16 @@ " as well as :class:`~pystac.Item` s.\n", "\n", " Args:\n", - " id (str): Identifier for the catalog. Must be unique within the STAC.\n", - " description (str): Detailed multi-line description to fully explain the catalog.\n", - " `CommonMark 0.28 syntax `_ MAY be used for rich text\n", - " representation.\n", - " title (str or None): Optional short descriptive one-line title for the catalog.\n", - " stac_extensions (List[str]): Optional list of extensions the Catalog implements.\n", - " href (str or None): Optional HREF for this catalog, which be set as the catalog's\n", - " self link's HREF.\n", - "\n", - " Attributes:\n", - " id (str): Identifier for the catalog.\n", - " description (str): Detailed multi-line description to fully explain the catalog.\n", - " title (str or None): Optional short descriptive one-line title for the catalog.\n", - " stac_extensions (List[str] or None): Optional list of extensions the Catalog implements.\n", - " extra_fields (dict or None): Extra fields that are part of the top-level JSON properties\n", - " of the Catalog.\n", - " links (List[Link]): A list of :class:`~pystac.Link` objects representing\n", - " all links associated with this Catalog.\n", + " id : Identifier for the catalog. Must be unique within the STAC.\n", + " description : Detailed multi-line description to fully explain the catalog.\n", + " `CommonMark 0.28 syntax `_ MAY be used for rich\n", + " text representation.\n", + " title : Optional short descriptive one-line title for the catalog.\n", + " stac_extensions : Optional list of extensions the Catalog implements.\n", + " href : Optional HREF for this catalog, which be set as the\n", + " catalog's self link's HREF.\n", + " catalog_type : Optional catalog type for this catalog. Must\n", + " be one of the values in :class:`~pystac.CatalogType`.\n", " \n" ] } @@ -190,7 +176,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -206,7 +192,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -232,7 +218,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -244,45 +230,24 @@ " satellite imagery, derived data, DEM's, etc.\n", "\n", " Args:\n", - " id (str): Provider identifier. Must be unique within the STAC.\n", - " geometry (dict): Defines the full footprint of the asset represented by this item,\n", - " formatted according to `RFC 7946, section 3.1 (GeoJSON)\n", - " `_.\n", - " bbox (List[float] or None): Bounding Box of the asset represented by this item using\n", - " either 2D or 3D geometries. The length of the array must be 2*n where n is the\n", - " number of dimensions. Could also be None in the case of a null geometry.\n", - " datetime (datetime or None): Datetime associated with this item. If None,\n", + " id : Provider identifier. Must be unique within the STAC.\n", + " geometry : Defines the full footprint of the asset represented by this\n", + " item, formatted according to\n", + " `RFC 7946, section 3.1 (GeoJSON) `_.\n", + " bbox : Bounding Box of the asset represented by this item\n", + " using either 2D or 3D geometries. The length of the array must be 2*n\n", + " where n is the number of dimensions. Could also be None in the case of a\n", + " null geometry.\n", + " datetime : Datetime associated with this item. If None,\n", " a start_datetime and end_datetime must be supplied in the properties.\n", - " properties (dict): A dictionary of additional metadata for the item.\n", - " stac_extensions (List[str]): Optional list of extensions the Item implements.\n", - " href (str or None): Optional HREF for this item, which be set as the item's\n", + " properties : A dictionary of additional metadata for the item.\n", + " stac_extensions : Optional list of extensions the Item implements.\n", + " href : Optional HREF for this item, which be set as the item's\n", " self link's HREF.\n", - " collection (Collection or str): The Collection or Collection ID that this item\n", + " collection : The Collection or Collection ID that this item\n", " belongs to.\n", - " extra_fields (dict or None): Extra fields that are part of the top-level JSON properties\n", - " of the Item.\n", - "\n", - " Attributes:\n", - " id (str): Provider identifier. Unique within the STAC.\n", - " geometry (dict): Defines the full footprint of the asset represented by this item,\n", - " formatted according to `RFC 7946, section 3.1 (GeoJSON)\n", - " `_.\n", - " bbox (List[float] or None): Bounding Box of the asset represented by this item using\n", - " either 2D or 3D geometries. The length of the array is 2*n where n is the\n", - " number of dimensions. Could also be None in the case of a null geometry.\n", - " datetime (datetime or None): Datetime associated with this item. If None,\n", - " the start_datetime and end_datetime in the common_metadata\n", - " will supply the datetime range of the Item.\n", - " properties (dict): A dictionary of additional metadata for the item.\n", - " stac_extensions (List[str] or None): Optional list of extensions the Item implements.\n", - " collection (Collection or None): Collection that this item is a part of.\n", - " links (List[Link]): A list of :class:`~pystac.Link` objects representing\n", - " all links associated with this STACObject.\n", - " assets (Dict[str, Asset]): Dictionary of asset objects that can be downloaded,\n", - " each with a unique key.\n", - " collection_id (str or None): The Collection ID that this item belongs to, if any.\n", - " extra_fields (dict or None): Extra fields that are part of the top-level JSON properties\n", - " of the Item.\n", + " extra_fields : Extra fields that are part of the top-level JSON\n", + " properties of the Item.\n", " \n" ] } @@ -300,7 +265,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -323,7 +288,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -350,7 +315,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ @@ -372,27 +337,16 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 14, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "item.get_parent() is None" + "assert item.get_parent() is None" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -401,7 +355,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -410,7 +364,7 @@ "" ] }, - "execution_count": 14, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -428,7 +382,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -455,42 +409,30 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "An object that contains a link to data associated with the Item that can be\n", - " downloaded or streamed.\n", + "An object that contains a link to data associated with an Item or Collection that\n", + " can be downloaded or streamed.\n", "\n", " Args:\n", - " href (str): Link to the asset object. Relative and absolute links are both allowed.\n", - " title (str): Optional displayed title for clients and users.\n", - " description (str): A description of the Asset providing additional details, such as\n", - " how it was processed or created. CommonMark 0.29 syntax MAY be used for rich\n", - " text representation.\n", - " media_type (str): Optional description of the media type. Registered Media Types\n", - " are preferred. See :class:`~pystac.MediaType` for common media types.\n", - " roles ([str]): Optional, Semantic roles (i.e. thumbnail, overview, data, metadata)\n", - " of the asset.\n", - " properties (dict): Optional, additional properties for this asset. This is used by\n", - " extensions as a way to serialize and deserialize properties on asset\n", - " object JSON.\n", - "\n", - " Attributes:\n", - " href (str): Link to the asset object. Relative and absolute links are both allowed.\n", - " title (str): Optional displayed title for clients and users.\n", - " description (str): A description of the Asset providing additional details, such as\n", - " how it was processed or created. CommonMark 0.29 syntax MAY be used for rich\n", - " text representation.\n", - " media_type (str): Optional description of the media type. Registered Media Types\n", + " href : Link to the asset object. Relative and absolute links are both\n", + " allowed.\n", + " title : Optional displayed title for clients and users.\n", + " description : A description of the Asset providing additional details,\n", + " such as how it was processed or created. CommonMark 0.29 syntax MAY be used\n", + " for rich text representation.\n", + " media_type : Optional description of the media type. Registered Media Types\n", " are preferred. See :class:`~pystac.MediaType` for common media types.\n", - " properties (dict): Optional, additional properties for this asset. This is used by\n", - " extensions as a way to serialize and deserialize properties on asset\n", + " roles : Optional, Semantic roles (i.e. thumbnail, overview,\n", + " data, metadata) of the asset.\n", + " extra_fields : Optional, additional fields for this asset. This is used\n", + " by extensions as a way to serialize and deserialize properties on asset\n", " object JSON.\n", - " owner (Item or None): The Item this asset belongs to.\n", " \n" ] } @@ -501,20 +443,9 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 19, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "item.add_asset(\n", " key='image', \n", @@ -534,7 +465,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 20, "metadata": {}, "outputs": [ { @@ -543,10 +474,10 @@ "text": [ "{\n", " \"type\": \"Feature\",\n", - " \"stac_version\": \"1.0.0-beta.2\",\n", + " \"stac_version\": \"1.0.0\",\n", " \"id\": \"local-image\",\n", " \"properties\": {\n", - " \"datetime\": \"2020-08-03T03:47:48.786929Z\"\n", + " \"datetime\": \"2022-03-29T12:47:45.754444Z\"\n", " },\n", " \"geometry\": {\n", " \"type\": \"Polygon\",\n", @@ -589,7 +520,7 @@ " ],\n", " \"assets\": {\n", " \"image\": {\n", - " \"href\": \"/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/image.tif\",\n", + " \"href\": \"/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/image.tif\",\n", " \"type\": \"image/tiff; application=geotiff\"\n", " }\n", " },\n", @@ -598,7 +529,8 @@ " 55.73478197572927,\n", " 37.66573047610874,\n", " 55.73882710285011\n", - " ]\n", + " ],\n", + " \"stac_extensions\": []\n", "}\n" ] } @@ -631,7 +563,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 21, "metadata": {}, "outputs": [ { @@ -657,20 +589,9 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 22, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "catalog.normalize_hrefs(os.path.join(tmp_dir.name, 'stac'))" ] @@ -684,15 +605,15 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 23, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/stac/catalog.json\n", - "/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/stac/local-image/local-image.json\n" + "/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/stac/catalog.json\n", + "/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/stac/local-image/local-image.json\n" ] } ], @@ -712,7 +633,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 24, "metadata": {}, "outputs": [], "source": [ @@ -721,17 +642,17 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 25, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/stac/catalog.json\r\n", - "\r\n", - "/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/stac/local-image:\r\n", - "local-image.json\r\n" + "/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/stac/catalog.json\n", + "\n", + "/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/stac/local-image:\n", + "local-image.json\n" ] } ], @@ -741,7 +662,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 26, "metadata": {}, "outputs": [ { @@ -749,33 +670,35 @@ "output_type": "stream", "text": [ "{\n", - " \"id\": \"test-catalog\",\n", - " \"stac_version\": \"1.0.0-beta.2\",\n", - " \"description\": \"Tutorial catalog.\",\n", - " \"links\": [\n", - " {\n", - " \"rel\": \"root\",\n", - " \"href\": \"./catalog.json\",\n", - " \"type\": \"application/json\"\n", - " },\n", - " {\n", - " \"rel\": \"item\",\n", - " \"href\": \"./local-image/local-image.json\",\n", - " \"type\": \"application/json\"\n", - " }\n", - " ]\n", + " \"type\": \"Catalog\",\n", + " \"id\": \"test-catalog\",\n", + " \"stac_version\": \"1.0.0\",\n", + " \"description\": \"Tutorial catalog.\",\n", + " \"links\": [\n", + " {\n", + " \"rel\": \"root\",\n", + " \"href\": \"./catalog.json\",\n", + " \"type\": \"application/json\"\n", + " },\n", + " {\n", + " \"rel\": \"item\",\n", + " \"href\": \"./local-image/local-image.json\",\n", + " \"type\": \"application/json\"\n", + " }\n", + " ],\n", + " \"stac_extensions\": []\n", "}\n" ] } ], "source": [ - "with open(catalog.get_self_href()) as f:\n", + "with open(item.self_href) as f:\n", " print(f.read())" ] }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 27, "metadata": {}, "outputs": [ { @@ -783,69 +706,70 @@ "output_type": "stream", "text": [ "{\n", - " \"type\": \"Feature\",\n", - " \"stac_version\": \"1.0.0-beta.2\",\n", - " \"id\": \"local-image\",\n", - " \"properties\": {\n", - " \"datetime\": \"2020-08-03T03:47:48.786929Z\"\n", - " },\n", - " \"geometry\": {\n", - " \"type\": \"Polygon\",\n", - " \"coordinates\": [\n", - " [\n", - " [\n", - " 37.6616853489879,\n", - " 55.73478197572927\n", - " ],\n", - " [\n", - " 37.6616853489879,\n", - " 55.73882710285011\n", - " ],\n", - " [\n", - " 37.66573047610874,\n", - " 55.73882710285011\n", - " ],\n", - " [\n", - " 37.66573047610874,\n", - " 55.73478197572927\n", - " ],\n", - " [\n", - " 37.6616853489879,\n", - " 55.73478197572927\n", - " ]\n", - " ]\n", + " \"type\": \"Feature\",\n", + " \"stac_version\": \"1.0.0\",\n", + " \"id\": \"local-image\",\n", + " \"properties\": {\n", + " \"datetime\": \"2022-03-29T12:47:45.754444Z\"\n", + " },\n", + " \"geometry\": {\n", + " \"type\": \"Polygon\",\n", + " \"coordinates\": [\n", + " [\n", + " [\n", + " 37.6616853489879,\n", + " 55.73478197572927\n", + " ],\n", + " [\n", + " 37.6616853489879,\n", + " 55.73882710285011\n", + " ],\n", + " [\n", + " 37.66573047610874,\n", + " 55.73882710285011\n", + " ],\n", + " [\n", + " 37.66573047610874,\n", + " 55.73478197572927\n", + " ],\n", + " [\n", + " 37.6616853489879,\n", + " 55.73478197572927\n", " ]\n", - " },\n", - " \"links\": [\n", - " {\n", - " \"rel\": \"root\",\n", - " \"href\": \"../catalog.json\",\n", - " \"type\": \"application/json\"\n", - " },\n", - " {\n", - " \"rel\": \"parent\",\n", - " \"href\": \"../catalog.json\",\n", - " \"type\": \"application/json\"\n", - " }\n", - " ],\n", - " \"assets\": {\n", - " \"image\": {\n", - " \"href\": \"/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/image.tif\",\n", - " \"type\": \"image/tiff; application=geotiff\"\n", - " }\n", - " },\n", - " \"bbox\": [\n", - " 37.6616853489879,\n", - " 55.73478197572927,\n", - " 37.66573047610874,\n", - " 55.73882710285011\n", + " ]\n", " ]\n", + " },\n", + " \"links\": [\n", + " {\n", + " \"rel\": \"root\",\n", + " \"href\": \"../catalog.json\",\n", + " \"type\": \"application/json\"\n", + " },\n", + " {\n", + " \"rel\": \"parent\",\n", + " \"href\": \"../catalog.json\",\n", + " \"type\": \"application/json\"\n", + " }\n", + " ],\n", + " \"assets\": {\n", + " \"image\": {\n", + " \"href\": \"/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/image.tif\",\n", + " \"type\": \"image/tiff; application=geotiff\"\n", + " }\n", + " },\n", + " \"bbox\": [\n", + " 37.6616853489879,\n", + " 55.73478197572927,\n", + " 37.66573047610874,\n", + " 55.73882710285011\n", + " ],\n", + " \"stac_extensions\": []\n", "}\n" ] } ], "source": [ - "with open(item.get_self_href()) as f:\n", + "with open(item.self_href) as f:\n", " print(f.read())" ] }, @@ -858,7 +782,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 28, "metadata": {}, "outputs": [], "source": [ @@ -874,7 +798,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 29, "metadata": {}, "outputs": [ { @@ -882,68 +806,69 @@ "output_type": "stream", "text": [ "{\n", - " \"type\": \"Feature\",\n", - " \"stac_version\": \"1.0.0-beta.2\",\n", - " \"id\": \"local-image\",\n", - " \"properties\": {\n", - " \"datetime\": \"2020-08-03T03:47:48.786929Z\"\n", - " },\n", - " \"geometry\": {\n", - " \"type\": \"Polygon\",\n", - " \"coordinates\": [\n", - " [\n", - " [\n", - " 37.6616853489879,\n", - " 55.73478197572927\n", - " ],\n", - " [\n", - " 37.6616853489879,\n", - " 55.73882710285011\n", - " ],\n", - " [\n", - " 37.66573047610874,\n", - " 55.73882710285011\n", - " ],\n", - " [\n", - " 37.66573047610874,\n", - " 55.73478197572927\n", - " ],\n", - " [\n", - " 37.6616853489879,\n", - " 55.73478197572927\n", - " ]\n", - " ]\n", + " \"type\": \"Feature\",\n", + " \"stac_version\": \"1.0.0\",\n", + " \"id\": \"local-image\",\n", + " \"properties\": {\n", + " \"datetime\": \"2022-03-29T12:47:45.754444Z\"\n", + " },\n", + " \"geometry\": {\n", + " \"type\": \"Polygon\",\n", + " \"coordinates\": [\n", + " [\n", + " [\n", + " 37.6616853489879,\n", + " 55.73478197572927\n", + " ],\n", + " [\n", + " 37.6616853489879,\n", + " 55.73882710285011\n", + " ],\n", + " [\n", + " 37.66573047610874,\n", + " 55.73882710285011\n", + " ],\n", + " [\n", + " 37.66573047610874,\n", + " 55.73478197572927\n", + " ],\n", + " [\n", + " 37.6616853489879,\n", + " 55.73478197572927\n", " ]\n", + " ]\n", + " ]\n", + " },\n", + " \"links\": [\n", + " {\n", + " \"rel\": \"root\",\n", + " \"href\": \"/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/stac/catalog.json\",\n", + " \"type\": \"application/json\"\n", " },\n", - " \"links\": [\n", - " {\n", - " \"rel\": \"self\",\n", - " \"href\": \"/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/stac/local-image/local-image.json\",\n", - " \"type\": \"application/json\"\n", - " },\n", - " {\n", - " \"rel\": \"root\",\n", - " \"href\": \"/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/stac/catalog.json\",\n", - " \"type\": \"application/json\"\n", - " },\n", - " {\n", - " \"rel\": \"parent\",\n", - " \"href\": \"/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/stac/catalog.json\",\n", - " \"type\": \"application/json\"\n", - " }\n", - " ],\n", - " \"assets\": {\n", - " \"image\": {\n", - " \"href\": \"/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/image.tif\",\n", - " \"type\": \"image/tiff; application=geotiff\"\n", - " }\n", + " {\n", + " \"rel\": \"parent\",\n", + " \"href\": \"/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/stac/catalog.json\",\n", + " \"type\": \"application/json\"\n", " },\n", - " \"bbox\": [\n", - " 37.6616853489879,\n", - " 55.73478197572927,\n", - " 37.66573047610874,\n", - " 55.73882710285011\n", - " ]\n", + " {\n", + " \"rel\": \"self\",\n", + " \"href\": \"/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/stac/local-image/local-image.json\",\n", + " \"type\": \"application/json\"\n", + " }\n", + " ],\n", + " \"assets\": {\n", + " \"image\": {\n", + " \"href\": \"/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/image.tif\",\n", + " \"type\": \"image/tiff; application=geotiff\"\n", + " }\n", + " },\n", + " \"bbox\": [\n", + " 37.6616853489879,\n", + " 55.73478197572927,\n", + " 37.66573047610874,\n", + " 55.73882710285011\n", + " ],\n", + " \"stac_extensions\": []\n", "}\n" ] } @@ -962,7 +887,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 30, "metadata": {}, "outputs": [], "source": [ @@ -972,7 +897,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 31, "metadata": {}, "outputs": [ { @@ -980,63 +905,64 @@ "output_type": "stream", "text": [ "{\n", - " \"type\": \"Feature\",\n", - " \"stac_version\": \"1.0.0-beta.2\",\n", - " \"id\": \"local-image\",\n", - " \"properties\": {\n", - " \"datetime\": \"2020-08-03T03:47:48.786929Z\"\n", - " },\n", - " \"geometry\": {\n", - " \"type\": \"Polygon\",\n", - " \"coordinates\": [\n", - " [\n", - " [\n", - " 37.6616853489879,\n", - " 55.73478197572927\n", - " ],\n", - " [\n", - " 37.6616853489879,\n", - " 55.73882710285011\n", - " ],\n", - " [\n", - " 37.66573047610874,\n", - " 55.73882710285011\n", - " ],\n", - " [\n", - " 37.66573047610874,\n", - " 55.73478197572927\n", - " ],\n", - " [\n", - " 37.6616853489879,\n", - " 55.73478197572927\n", - " ]\n", - " ]\n", + " \"type\": \"Feature\",\n", + " \"stac_version\": \"1.0.0\",\n", + " \"id\": \"local-image\",\n", + " \"properties\": {\n", + " \"datetime\": \"2022-03-29T12:47:45.754444Z\"\n", + " },\n", + " \"geometry\": {\n", + " \"type\": \"Polygon\",\n", + " \"coordinates\": [\n", + " [\n", + " [\n", + " 37.6616853489879,\n", + " 55.73478197572927\n", + " ],\n", + " [\n", + " 37.6616853489879,\n", + " 55.73882710285011\n", + " ],\n", + " [\n", + " 37.66573047610874,\n", + " 55.73882710285011\n", + " ],\n", + " [\n", + " 37.66573047610874,\n", + " 55.73478197572927\n", + " ],\n", + " [\n", + " 37.6616853489879,\n", + " 55.73478197572927\n", " ]\n", - " },\n", - " \"links\": [\n", - " {\n", - " \"rel\": \"root\",\n", - " \"href\": \"../catalog.json\",\n", - " \"type\": \"application/json\"\n", - " },\n", - " {\n", - " \"rel\": \"parent\",\n", - " \"href\": \"../catalog.json\",\n", - " \"type\": \"application/json\"\n", - " }\n", - " ],\n", - " \"assets\": {\n", - " \"image\": {\n", - " \"href\": \"../../image.tif\",\n", - " \"type\": \"image/tiff; application=geotiff\"\n", - " }\n", - " },\n", - " \"bbox\": [\n", - " 37.6616853489879,\n", - " 55.73478197572927,\n", - " 37.66573047610874,\n", - " 55.73882710285011\n", + " ]\n", " ]\n", + " },\n", + " \"links\": [\n", + " {\n", + " \"rel\": \"root\",\n", + " \"href\": \"../catalog.json\",\n", + " \"type\": \"application/json\"\n", + " },\n", + " {\n", + " \"rel\": \"parent\",\n", + " \"href\": \"../catalog.json\",\n", + " \"type\": \"application/json\"\n", + " }\n", + " ],\n", + " \"assets\": {\n", + " \"image\": {\n", + " \"href\": \"../../image.tif\",\n", + " \"type\": \"image/tiff; application=geotiff\"\n", + " }\n", + " },\n", + " \"bbox\": [\n", + " 37.6616853489879,\n", + " 55.73478197572927,\n", + " 37.66573047610874,\n", + " 55.73882710285011\n", + " ],\n", + " \"stac_extensions\": []\n", "}\n" ] } @@ -1064,11 +990,11 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 32, "metadata": {}, "outputs": [], "source": [ - "from pystac.extensions.eo import Band\n", + "from pystac.extensions.eo import Band, EOExtension\n", "\n", "# From: https://www.spaceimagingme.com/downloads/sensors/datasheets/DG_WorldView3_DS_2014.pdf\n", "\n", @@ -1098,7 +1024,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 33, "metadata": {}, "outputs": [], "source": [ @@ -1107,9 +1033,8 @@ " bbox=bbox,\n", " datetime=datetime.utcnow(),\n", " properties={})\n", - "\n", - "eo_item.ext.enable(pystac.Extensions.EO)\n", - "eo_item.ext.eo.apply(bands=wv3_bands)" + "eo = EOExtension.ext(eo_item, add_if_missing=True)\n", + "eo.apply(bands=wv3_bands)" ] }, { @@ -1121,33 +1046,30 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 34, "metadata": {}, "outputs": [], "source": [ "eo_item.common_metadata.platform = \"Maxar\"\n", - "eo_item.common_metadata.instrument=\"WorldView3\"\n", + "eo_item.common_metadata.instruments = [\"WorldView3\"]\n", "eo_item.common_metadata.gsd = 0.3" ] }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 35, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] } ], "source": [ - "eo_item" + "print(eo_item)" ] }, { @@ -1159,52 +1081,15 @@ }, { "cell_type": "code", - "execution_count": 34, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Help on method set_bands in module pystac.extensions.eo:\n", - "\n", - "set_bands(bands, asset=None) method of pystac.extensions.eo.EOItemExt instance\n", - " Set an Item or an Asset bands.\n", - " \n", - " If an Asset is supplied, sets the property on the Asset.\n", - " Otherwise sets the Item's value.\n", - "\n" - ] - } - ], - "source": [ - "eo_ext = eo_item.ext.eo\n", - "help(eo_ext.set_bands)\n", - "\n", - "#eo_item.add_asset(key='image', asset=)" - ] - }, - { - "cell_type": "code", - "execution_count": 35, + "execution_count": 36, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 35, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "asset = pystac.Asset(href=img_path, \n", " media_type=pystac.MediaType.GEOTIFF)\n", - "eo_ext.set_bands(wv3_bands, asset)\n", - "eo_item.add_asset(\"image\", asset)" + "eo_item.add_asset(\"image\", asset)\n", + "eo_on_asset = EOExtension.ext(eo_item.assets[\"image\"])\n", + "eo_on_asset.apply(wv3_bands)" ] }, { @@ -1216,14 +1101,14 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 37, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'href': '/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/image.tif',\n", - " 'type': 'image/tiff; application=geotiff',\n", + "{'href': '/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/image.tif',\n", + " 'type': ,\n", " 'eo:bands': [{'name': 'Coastal',\n", " 'common_name': 'coastal',\n", " 'description': 'Coastal: 400 - 450 nm'},\n", @@ -1246,7 +1131,7 @@ " 'description': 'Near-IR2: 860 - 1040 nm'}]}" ] }, - "execution_count": 36, + "execution_count": 37, "metadata": {}, "output_type": "execute_result" } @@ -1264,7 +1149,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 38, "metadata": {}, "outputs": [ { @@ -1273,7 +1158,7 @@ "[]" ] }, - "execution_count": 37, + "execution_count": 38, "metadata": {}, "output_type": "execute_result" } @@ -1285,7 +1170,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 39, "metadata": {}, "outputs": [ { @@ -1294,7 +1179,7 @@ "[]" ] }, - "execution_count": 38, + "execution_count": 39, "metadata": {}, "output_type": "execute_result" } @@ -1306,7 +1191,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 40, "metadata": {}, "outputs": [], "source": [ @@ -1323,7 +1208,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 41, "metadata": {}, "outputs": [], "source": [ @@ -1332,7 +1217,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 42, "metadata": {}, "outputs": [ { @@ -1341,69 +1226,50 @@ "[]" ] }, - "execution_count": 41, + "execution_count": 42, "metadata": {}, "output_type": "execute_result" } ], "source": [ + "assert isinstance(catalog2, pystac.Catalog)\n", "list(catalog2.get_items())" ] }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 43, "metadata": {}, "outputs": [], "source": [ - "item = next(catalog2.get_all_items())" + "item: pystac.Item = next(catalog2.get_all_items())" ] }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 44, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 43, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "item.ext.implements('eo')" + "assert EOExtension.has_extension(item)" ] }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 45, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "[,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ]" - ] - }, - "execution_count": 44, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "[, , , , , , , ]\n" + ] } ], "source": [ - "item.ext.eo.get_bands(item.assets['image'])" + "eo_on_asset = EOExtension.ext(item.assets[\"image\"])\n", + "print(eo_on_asset.bands)" ] }, { @@ -1419,17 +1285,17 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 46, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "('/var/folders/sv/zr8j0t4j1f726nhlt3vb8c300000gn/T/tmpt1wuelid/image.tif',\n", - " )" + "('/var/folders/9z/lnsvqfqj4gs2d1j1nw3vynrm0000gn/T/tmpzpx86d17/image.tif',\n", + " )" ] }, - "execution_count": 45, + "execution_count": 46, "metadata": {}, "output_type": "execute_result" } @@ -1444,7 +1310,7 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 47, "metadata": {}, "outputs": [], "source": [ @@ -1460,7 +1326,7 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 48, "metadata": {}, "outputs": [ { @@ -1471,44 +1337,31 @@ " enable discovery.\n", "\n", " Args:\n", - " id (str): Identifier for the collection. Must be unique within the STAC.\n", - " description (str): Detailed multi-line description to fully explain the collection.\n", - " `CommonMark 0.28 syntax `_ MAY be used for rich text\n", - " representation.\n", - " extent (Extent): Spatial and temporal extents that describe the bounds of\n", + " id : Identifier for the collection. Must be unique within the STAC.\n", + " description : Detailed multi-line description to fully explain the\n", + " collection. `CommonMark 0.28 syntax `_ MAY\n", + " be used for rich text representation.\n", + " extent : Spatial and temporal extents that describe the bounds of\n", " all items contained within this Collection.\n", - " title (str or None): Optional short descriptive one-line title for the collection.\n", - " stac_extensions (List[str]): Optional list of extensions the Collection implements.\n", - " href (str or None): Optional HREF for this collection, which be set as the collection's\n", - " self link's HREF.\n", - " license (str): Collection's license(s) as a `SPDX License identifier\n", - " `_, `various`, or `proprietary`. If collection includes\n", - " data with multiple different licenses, use `various` and add a link for each.\n", - " Defaults to 'proprietary'.\n", - " keywords (List[str]): Optional list of keywords describing the collection.\n", - " providers (List[Provider]): Optional list of providers of this Collection.\n", - " properties (dict): Optional dict of common fields across referenced items.\n", - " summaries (dict): An optional map of property summaries,\n", + " title : Optional short descriptive one-line title for the\n", + " collection.\n", + " stac_extensions : Optional list of extensions the Collection\n", + " implements.\n", + " href : Optional HREF for this collection, which be set as the\n", + " collection's self link's HREF.\n", + " catalog_type : Optional catalog type for this catalog. Must\n", + " be one of the values in :class`~pystac.CatalogType`.\n", + " license : Collection's license(s) as a\n", + " `SPDX License identifier `_,\n", + " `various`, or `proprietary`. If collection includes\n", + " data with multiple different licenses, use `various` and add a link for\n", + " each. Defaults to 'proprietary'.\n", + " keywords : Optional list of keywords describing the collection.\n", + " providers : Optional list of providers of this Collection.\n", + " summaries : An optional map of property summaries,\n", " either a set of values or statistics such as a range.\n", - " extra_fields (dict or None): Extra fields that are part of the top-level JSON properties\n", - " of the Collection.\n", - "\n", - " Attributes:\n", - " id (str): Identifier for the collection.\n", - " description (str): Detailed multi-line description to fully explain the collection.\n", - " extent (Extent): Spatial and temporal extents that describe the bounds of\n", - " all items contained within this Collection.\n", - " title (str or None): Optional short descriptive one-line title for the collection.\n", - " stac_extensions (List[str]): Optional list of extensions the Collection implements.\n", - " keywords (List[str] or None): Optional list of keywords describing the collection.\n", - " providers (List[Provider] or None): Optional list of providers of this Collection.\n", - " properties (dict or None): Optional dict of common fields across referenced items.\n", - " summaries (dict or None): An optional map of property summaries,\n", - " either a set of values or statistics such as a range.\n", - " links (List[Link]): A list of :class:`~pystac.Link` objects representing\n", - " all links associated with this Collection.\n", - " extra_fields (dict or None): Extra fields that are part of the top-level JSON properties\n", - " of the Catalog.\n", + " extra_fields : Extra fields that are part of the top-level\n", + " JSON properties of the Collection.\n", " \n" ] } @@ -1526,22 +1379,20 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 49, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Describes the spatio-temporal extents of a Collection.\n", + "Describes the spatiotemporal extents of a Collection.\n", "\n", " Args:\n", - " spatial (SpatialExtent): Potential spatial extent covered by the collection.\n", - " temporal (TemporalExtent): Potential temporal extent covered by the collection.\n", - "\n", - " Attributes:\n", - " spatial (SpatialExtent): Potential spatial extent covered by the collection.\n", - " temporal (TemporalExtent): Potential temporal extent covered by the collection.\n", + " spatial : Potential spatial extent covered by the collection.\n", + " temporal : Potential temporal extent covered by the collection.\n", + " extra_fields : Dictionary containing additional top-level fields defined on the\n", + " Extent object.\n", " \n" ] } @@ -1561,27 +1412,15 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 50, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 49, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "collection_item = pystac.Item(id='local-image-col-1',\n", " geometry=footprint,\n", " bbox=bbox,\n", " datetime=datetime.utcnow(),\n", - " properties={},\n", - " stac_extensions=[pystac.Extensions.EO])\n", + " properties={})\n", "\n", "collection_item.common_metadata.gsd = 0.3\n", "collection_item.common_metadata.platform = 'Maxar'\n", @@ -1589,15 +1428,15 @@ "\n", "asset = pystac.Asset(href=img_path, \n", " media_type=pystac.MediaType.GEOTIFF)\n", - "collection_item.ext.eo.set_bands(wv3_bands, asset)\n", - "collection_item.add_asset('image', asset)\n", + "collection_item.add_asset(\"image\", asset)\n", + "eo = EOExtension.ext(collection_item.assets[\"image\"], add_if_missing=True)\n", + "eo.apply(wv3_bands)\n", "\n", "collection_item2 = pystac.Item(id='local-image-col-2',\n", " geometry=footprint2,\n", " bbox=bbox2,\n", " datetime=datetime.utcnow(),\n", - " properties={},\n", - " stac_extensions=[pystac.Extensions.EO])\n", + " properties={})\n", "\n", "collection_item2.common_metadata.gsd = 0.3\n", "collection_item2.common_metadata.platform = 'Maxar'\n", @@ -1605,11 +1444,11 @@ "\n", "asset2 = pystac.Asset(href=img_path,\n", " media_type=pystac.MediaType.GEOTIFF)\n", - "collection_item2.ext.eo.set_bands([\n", + "collection_item2.add_asset(\"image\", asset2)\n", + "eo = EOExtension.ext(collection_item2.assets[\"image\"], add_if_missing=True)\n", + "eo.apply([\n", " band for band in wv3_bands if band.name in [\"Red\", \"Green\", \"Blue\"]\n", - "], asset2)\n", - "\n", - "collection_item2.add_asset('image', asset2)" + "])\n" ] }, { @@ -1621,7 +1460,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 51, "metadata": {}, "outputs": [], "source": [ @@ -1634,7 +1473,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 52, "metadata": {}, "outputs": [], "source": [ @@ -1644,7 +1483,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 53, "metadata": {}, "outputs": [], "source": [ @@ -1653,7 +1492,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 54, "metadata": {}, "outputs": [], "source": [ @@ -1672,7 +1511,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 55, "metadata": {}, "outputs": [], "source": [ @@ -1681,7 +1520,7 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 56, "metadata": {}, "outputs": [], "source": [ @@ -1692,7 +1531,7 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 57, "metadata": {}, "outputs": [ { @@ -1712,7 +1551,7 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": 58, "metadata": {}, "outputs": [], "source": [ @@ -1731,7 +1570,7 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": 59, "metadata": {}, "outputs": [], "source": [ @@ -1758,42 +1597,50 @@ "source": [ "### Allowing PySTAC to read from AWS S3\n", "\n", - "PySTAC aims to be virtually zero-dependency (notwithstanding the why-isn't-this-in-stdlib datetime-util), so it doesn't have the ability to read from or write to anything but the local file system. However, we can hook into PySTAC's IO in the following way. Learn more about how to use STAC_IO in the [documentation on the topic](https://pystac.readthedocs.io/en/latest/concepts.html#using-stac-io):" + "PySTAC aims to be virtually zero-dependency (notwithstanding the why-isn't-this-in-stdlib datetime-util), so it doesn't have the ability to read from or write to anything but the local file system. However, we can hook into PySTAC's IO in the following way. Learn more about how to customize I/O in STAC from the [documentation](https://pystac.readthedocs.io/en/stable/concepts.html#i-o-in-pystac):" ] }, { "cell_type": "code", - "execution_count": 59, + "execution_count": 60, "metadata": {}, "outputs": [], "source": [ + "from typing import Union, Any\n", "from urllib.parse import urlparse\n", + "\n", "import boto3\n", - "from pystac import STAC_IO\n", + "from pystac import Link\n", + "from pystac.stac_io import DefaultStacIO\n", "\n", - "def my_read_method(uri):\n", - " parsed = urlparse(uri)\n", - " if parsed.scheme == 's3':\n", - " bucket = parsed.netloc\n", - " key = parsed.path[1:]\n", - " s3 = boto3.resource('s3')\n", - " obj = s3.Object(bucket, key)\n", - " return obj.get()['Body'].read().decode('utf-8')\n", - " else:\n", - " return STAC_IO.default_read_text_method(uri)\n", "\n", - "def my_write_method(uri, txt):\n", - " parsed = urlparse(uri)\n", - " if parsed.scheme == 's3':\n", - " bucket = parsed.netloc\n", - " key = parsed.path[1:]\n", - " s3 = boto3.resource(\"s3\")\n", - " s3.Object(bucket, key).put(Body=txt)\n", - " else:\n", - " STAC_IO.default_write_text_method(uri, txt)\n", + "class CustomStacIO(DefaultStacIO):\n", + " def __init__(self):\n", + " self.s3 = boto3.resource(\"s3\")\n", "\n", - "STAC_IO.read_text_method = my_read_method\n", - "STAC_IO.write_text_method = my_write_method" + " def read_text(\n", + " self, source: Union[str, Link], *args: Any, **kwargs: Any\n", + " ) -> str:\n", + " parsed = urlparse(uri)\n", + " if parsed.scheme == \"s3\":\n", + " bucket = parsed.netloc\n", + " key = parsed.path[1:]\n", + "\n", + " obj = self.s3.Object(bucket, key)\n", + " return obj.get()[\"Body\"].read().decode(\"utf-8\")\n", + " else:\n", + " return super().read_text(source, *args, **kwargs)\n", + "\n", + " def write_text(\n", + " self, dest: Union[str, Link], txt: str, *args: Any, **kwargs: Any\n", + " ) -> None:\n", + " parsed = urlparse(uri)\n", + " if parsed.scheme == \"s3\":\n", + " bucket = parsed.netloc\n", + " key = parsed.path[1:]\n", + " self.s3.Object(bucket, key).put(Body=txt, ContentEncoding=\"utf-8\")\n", + " else:\n", + " super().write_text(dest, txt, *args, **kwargs)\n" ] }, { @@ -1805,15 +1652,17 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 63, "metadata": {}, "outputs": [], "source": [ "# From https://alexwlchan.net/2017/07/listing-s3-keys/\n", + "from botocore import UNSIGNED\n", + "from botocore.config import Config\n", "\n", "def get_s3_keys(bucket, prefix):\n", " \"\"\"Generate all the keys in an S3 bucket.\"\"\"\n", - " s3 = boto3.client('s3')\n", + " s3 = boto3.client('s3', config=Config(signature_version=UNSIGNED))\n", " kwargs = {'Bucket': bucket, 'Prefix': prefix}\n", " while True:\n", " resp = s3.list_objects_v2(**kwargs)\n", @@ -1835,17 +1684,17 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 64, "metadata": {}, "outputs": [], "source": [ "moscow_training_chip_uris = list(get_s3_keys(bucket='spacenet-dataset', \n", - " prefix='spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS'))" + " prefix='spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/'))" ] }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 65, "metadata": {}, "outputs": [], "source": [ @@ -1870,7 +1719,7 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 66, "metadata": {}, "outputs": [], "source": [ @@ -1879,7 +1728,7 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 67, "metadata": {}, "outputs": [ { @@ -1897,7 +1746,7 @@ " '1005': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1005.tif'}}" ] }, - "execution_count": 64, + "execution_count": 67, "metadata": {}, "output_type": "execute_result" } @@ -1915,7 +1764,7 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 68, "metadata": {}, "outputs": [], "source": [ @@ -1935,7 +1784,7 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 71, "metadata": {}, "outputs": [ { @@ -1956,6 +1805,9 @@ } ], "source": [ + "import os\n", + "os.environ[\"AWS_NO_SIGN_REQUEST\"] = \"true\"\n", + "\n", "for chip_id in chip_id_to_data:\n", " img_uri = chip_id_to_data[chip_id]['img']\n", " print('Processing {}'.format(img_uri))\n", @@ -1965,18 +1817,19 @@ " geometry=footprint,\n", " bbox=bbox,\n", " datetime=datetime.utcnow(),\n", - " properties={},\n", - " stac_extensions=[pystac.Extensions.EO]) \n", + " properties={})\n", " \n", " item.common_metadata.gsd = 0.3\n", " item.common_metadata.platform = 'Maxar'\n", " item.common_metadata.instruments = ['WorldView3']\n", " \n", - " item.ext.eo.bands = wv3_bands\n", + " eo = EOExtension.ext(item, add_if_missing=True)\n", + " eo.bands = wv3_bands\n", " asset = pystac.Asset(href=img_uri,\n", " media_type=pystac.MediaType.COG)\n", - " item.ext.eo.set_bands(wv3_bands, asset)\n", " item.add_asset(key='ps-ms', asset=asset)\n", + " eo = EOExtension.ext(item.assets[\"ps-ms\"])\n", + " eo.bands = wv3_bands\n", " chip_id_to_items[chip_id] = item" ] }, @@ -1991,7 +1844,7 @@ }, { "cell_type": "code", - "execution_count": 67, + "execution_count": 72, "metadata": {}, "outputs": [], "source": [ @@ -2005,7 +1858,7 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": 73, "metadata": {}, "outputs": [], "source": [ @@ -2016,7 +1869,7 @@ }, { "cell_type": "code", - "execution_count": 69, + "execution_count": 74, "metadata": {}, "outputs": [], "source": [ @@ -2025,7 +1878,7 @@ }, { "cell_type": "code", - "execution_count": 70, + "execution_count": 75, "metadata": {}, "outputs": [], "source": [ @@ -2037,7 +1890,7 @@ }, { "cell_type": "code", - "execution_count": 71, + "execution_count": 76, "metadata": {}, "outputs": [], "source": [ @@ -2046,7 +1899,7 @@ }, { "cell_type": "code", - "execution_count": 72, + "execution_count": 77, "metadata": {}, "outputs": [ { @@ -2080,7 +1933,7 @@ }, { "cell_type": "code", - "execution_count": 73, + "execution_count": 78, "metadata": {}, "outputs": [], "source": [ @@ -2090,7 +1943,7 @@ }, { "cell_type": "code", - "execution_count": 74, + "execution_count": 79, "metadata": {}, "outputs": [ { @@ -2127,7 +1980,7 @@ }, { "cell_type": "code", - "execution_count": 75, + "execution_count": 80, "metadata": {}, "outputs": [], "source": [ @@ -2137,7 +1990,7 @@ }, { "cell_type": "code", - "execution_count": 76, + "execution_count": 81, "metadata": {}, "outputs": [], "source": [ @@ -2156,7 +2009,7 @@ }, { "cell_type": "code", - "execution_count": 77, + "execution_count": 82, "metadata": {}, "outputs": [], "source": [ @@ -2173,7 +2026,7 @@ }, { "cell_type": "code", - "execution_count": 78, + "execution_count": 84, "metadata": {}, "outputs": [ { @@ -2183,31 +2036,32 @@ "Applies label extension properties to the extended Item.\n", "\n", " Args:\n", - " label_description (str): A description of the label, how it was created,\n", + " label_description : A description of the label, how it was created,\n", " and what it is recommended for\n", - " label_type (str): An ENUM of either vector label type or raster label type. Use\n", + " label_type : An Enum of either vector label type or raster label type. Use\n", " one of :class:`~pystac.LabelType`.\n", - " label_properties (list or None): These are the names of the property field(s) in each\n", + " label_properties : These are the names of the property field(s) in each\n", " Feature of the label asset's FeatureCollection that contains the classes\n", " (keywords from label:classes if the property defines classes).\n", " If labels are rasters, this should be None.\n", - " label_classes (List[LabelClass]): Optional, but reqiured if ussing categorical data.\n", - " A list of LabelClasses defining the list of possible class names for each\n", - " label:properties. (e.g., tree, building, car, hippo)\n", - " label_tasks (List[str]): Recommended to be a subset of 'regression', 'classification',\n", + " label_classes : Optional, but required if using categorical data.\n", + " A list of :class:`LabelClasses` instances defining the list of possible\n", + " class names for each label:properties. (e.g., tree, building, car,\n", + " hippo)\n", + " label_tasks : Recommended to be a subset of 'regression', 'classification',\n", " 'detection', or 'segmentation', but may be an arbitrary value.\n", " label_methods: Recommended to be a subset of 'automated' or 'manual',\n", " but may be an arbitrary value.\n", - " label_overviews (List[LabelOverview]): Optional list of LabelOverview classes\n", - " that store counts (for classification-type data) or summary statistics (for\n", - " continuous numerical/regression data).\n", + " label_overviews : Optional list of :class:`LabelOverview` instances\n", + " that store counts (for classification-type data) or summary statistics\n", + " (for continuous numerical/regression data).\n", " \n" ] } ], "source": [ - "from pystac.extensions import label\n", - "print(label.LabelItemExt.apply.__doc__)" + "from pystac.extensions.label import LabelExtension\n", + "print(LabelExtension.apply.__doc__)" ] }, { @@ -2219,25 +2073,28 @@ }, { "cell_type": "code", - "execution_count": 79, + "execution_count": 85, "metadata": {}, "outputs": [], "source": [ + "from pystac.extensions.label import LabelType\n", + "\n", "for chip_id in chip_id_to_data:\n", " img_item = collection.get_item('img_{}'.format(chip_id))\n", + " assert img_item\n", " label_uri = chip_id_to_data[chip_id]['label']\n", " \n", " label_item = pystac.Item(id='label_{}'.format(chip_id),\n", " geometry=img_item.geometry,\n", " bbox=img_item.bbox,\n", " datetime=datetime.utcnow(),\n", - " properties={},\n", - " stac_extensions=[pystac.Extensions.LABEL])\n", - " label_item.ext.label.apply(label_description=\"SpaceNet 5 Road labels\",\n", - " label_type=label.LabelType.VECTOR,\n", + " properties={})\n", + " label = LabelExtension.ext(label_item, add_if_missing=True)\n", + " label.apply(label_description=\"SpaceNet 5 Road labels\",\n", + " label_type=LabelType.VECTOR,\n", " label_tasks=['segmentation', 'regression'])\n", - " label_item.ext.label.add_source(img_item)\n", - " label_item.ext.label.add_geojson_labels(label_uri)\n", + " label.add_source(img_item)\n", + " label.add_geojson_labels(label_uri)\n", " \n", " label_catalog.add_item(label_item)" ] @@ -2251,7 +2108,7 @@ }, { "cell_type": "code", - "execution_count": 80, + "execution_count": 86, "metadata": {}, "outputs": [ { @@ -2290,39 +2147,45 @@ }, { "cell_type": "code", - "execution_count": 81, + "execution_count": 87, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'type': 'Feature',\n", - " 'stac_version': '1.0.0-beta.2',\n", + " 'stac_version': '1.0.0',\n", " 'id': 'label_1',\n", " 'properties': {'label:description': 'SpaceNet 5 Road labels',\n", - " 'label:type': 'vector',\n", + " 'label:type': ,\n", " 'label:properties': None,\n", " 'label:tasks': ['segmentation', 'regression'],\n", - " 'datetime': '2020-08-03T03:47:56.599629Z'},\n", + " 'datetime': '2022-03-29T12:58:05.404487Z'},\n", " 'geometry': {'type': 'Polygon',\n", " 'coordinates': (((37.68191035616281, 55.73478210707574),\n", " (37.68191035616281, 55.73882710285011),\n", " (37.68595535193718, 55.73882710285011),\n", " (37.68595535193718, 55.73478210707574),\n", " (37.68191035616281, 55.73478210707574)),)},\n", - " 'links': [{'rel': 'source', 'href': None, 'type': 'application/json'},\n", - " {'rel': 'root', 'href': None, 'type': 'application/json'},\n", - " {'rel': 'parent', 'href': None, 'type': 'application/json'}],\n", + " 'links': [{'rel': 'source',\n", + " 'href': None,\n", + " 'type': },\n", + " {'rel': ,\n", + " 'href': None,\n", + " 'type': },\n", + " {'rel': ,\n", + " 'href': None,\n", + " 'type': }],\n", " 'assets': {'labels': {'href': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/geojson_roads_speed/SN5_roads_train_AOI_7_Moscow_geojson_roads_speed_chip1.geojson',\n", - " 'type': 'application/geo+json'}},\n", + " 'type': }},\n", " 'bbox': [37.68191035616281,\n", " 55.73478210707574,\n", " 37.68595535193718,\n", " 55.73882710285011],\n", - " 'stac_extensions': ['label']}" + " 'stac_extensions': ['https://stac-extensions.github.io/label/v1.0.1/schema.json']}" ] }, - "execution_count": 81, + "execution_count": 87, "metadata": {}, "output_type": "execute_result" } @@ -2331,13 +2194,6 @@ "label_item = catalog.get_child('spacenet-data-labels').get_item('label_1')\n", "label_item.to_dict()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -2356,7 +2212,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.5" + "version": "3.9.12" } }, "nbformat": 4, diff --git a/mypy.ini b/mypy.ini index ffdd91a23..5fcd1391e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,11 +2,11 @@ show_error_codes = True strict = True -[mypy-jsonschema.*] +[mypy-jinja2.*] ignore_missing_imports = True -[mypy-setuptools.*] +[mypy-jsonschema.*] ignore_missing_imports = True -[mypy-sphinx.util] +[mypy-setuptools.*] ignore_missing_imports = True diff --git a/pystac/asset.py b/pystac/asset.py index 2c9582a13..d6ea612c2 100644 --- a/pystac/asset.py +++ b/pystac/asset.py @@ -1,7 +1,9 @@ -from copy import copy +from html import escape +from copy import copy, deepcopy from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union from pystac import common_metadata +from pystac.html.jinja_env import get_jinja_env from pystac import utils if TYPE_CHECKING: @@ -90,7 +92,8 @@ def get_absolute_href(self) -> Optional[str]: """Gets the absolute href for this asset, if possible. If this Asset has no associated Item, and the asset HREF is a relative path, - this method will return None. + this method will return ``None``. If the Item that owns the Asset has no + self HREF, this will also return ``None``. Returns: str: The absolute HREF of this asset, or None if an absolute HREF could not @@ -100,9 +103,10 @@ def get_absolute_href(self) -> Optional[str]: return self.href else: if self.owner is not None: - return utils.make_absolute_href(self.href, self.owner.get_self_href()) - else: - return None + item_self = self.owner.get_self_href() + if item_self is not None: + return utils.make_absolute_href(self.href, item_self) + return None def to_dict(self) -> Dict[str, Any]: """Generate a dictionary representing the JSON of this Asset. @@ -132,7 +136,7 @@ def to_dict(self) -> Dict[str, Any]: return d def clone(self) -> "Asset": - """Clones this asset. + """Clones this asset. Makes a ``deepcopy`` of the :attr:`~pystac.Asset.extra_fields`. Returns: Asset: The clone of this asset. @@ -144,7 +148,7 @@ def clone(self) -> "Asset": description=self.description, media_type=self.media_type, roles=self.roles, - extra_fields=self.extra_fields, + extra_fields=deepcopy(self.extra_fields), ) @property @@ -156,6 +160,14 @@ def common_metadata(self) -> "CommonMetadata_Type": def __repr__(self) -> str: return "".format(self.href) + def _repr_html_(self) -> str: + jinja_env = get_jinja_env() + if jinja_env: + template = jinja_env.get_template("Asset.jinja2") + return str(template.render(asset=self)) + else: + return escape(repr(self)) + @classmethod def from_dict(cls, d: Dict[str, Any]) -> "Asset": """Constructs an Asset from a dict. diff --git a/pystac/catalog.py b/pystac/catalog.py index ce13e5e67..038032b43 100644 --- a/pystac/catalog.py +++ b/pystac/catalog.py @@ -1,6 +1,8 @@ import os +from html import escape from copy import deepcopy from pystac.errors import STACTypeError +from pystac.html.jinja_env import get_jinja_env from typing import ( Any, Callable, @@ -115,7 +117,7 @@ class Catalog(STACObject): Args: id : Identifier for the catalog. Must be unique within the STAC. description : Detailed multi-line description to fully explain the catalog. - `CommonMark 0.28 syntax `_ MAY be used for rich + `CommonMark 0.29 syntax `_ MAY be used for rich text representation. title : Optional short descriptive one-line title for the catalog. stac_extensions : Optional list of extensions the Catalog implements. @@ -196,6 +198,14 @@ def __init__( def __repr__(self) -> str: return "".format(self.id) + def _repr_html_(self) -> str: + jinja_env = get_jinja_env() + if jinja_env: + template = jinja_env.get_template("Catalog.jinja2") + return str(template.render(catalog=self)) + else: + return escape(repr(self)) + def set_root(self, root: Optional["Catalog"]) -> None: STACObject.set_root(self, root) if root is not None: @@ -517,7 +527,7 @@ def clone(self) -> "Catalog": id=self.id, description=self.description, title=self.title, - stac_extensions=self.stac_extensions, + stac_extensions=self.stac_extensions.copy(), extra_fields=deepcopy(self.extra_fields), catalog_type=self.catalog_type, ) diff --git a/pystac/collection.py b/pystac/collection.py index 86d4ccd54..893275fc8 100644 --- a/pystac/collection.py +++ b/pystac/collection.py @@ -1,6 +1,9 @@ +from html import escape from copy import deepcopy from datetime import datetime + from pystac.errors import STACTypeError +from pystac.html.jinja_env import get_jinja_env from typing import ( Any, Dict, @@ -14,7 +17,6 @@ cast, ) -import dateutil.parser from dateutil import tz import pystac @@ -24,7 +26,7 @@ from pystac.layout import HrefLayoutStrategy from pystac.link import Link from pystac.provider import Provider -from pystac.utils import datetime_to_str +from pystac.utils import datetime_to_str, str_to_datetime from pystac.serialization import ( identify_stac_object_type, identify_stac_object, @@ -250,9 +252,9 @@ def from_dict(d: Dict[str, Any]) -> "TemporalExtent": end = None if i[0]: - start = dateutil.parser.parse(i[0]) + start = str_to_datetime(i[0]) if i[1]: - end = dateutil.parser.parse(i[1]) + end = str_to_datetime(i[1]) parsed_intervals.append([start, end]) return TemporalExtent( @@ -421,7 +423,7 @@ class Collection(Catalog): Args: id : Identifier for the collection. Must be unique within the STAC. description : Detailed multi-line description to fully explain the - collection. `CommonMark 0.28 syntax `_ MAY + collection. `CommonMark 0.29 syntax `_ MAY be used for rich text representation. extent : Spatial and temporal extents that describe the bounds of all items contained within this Collection. @@ -444,6 +446,9 @@ class Collection(Catalog): either a set of values or statistics such as a range. extra_fields : Extra fields that are part of the top-level JSON properties of the Collection. + assets : A dictionary mapping string keys to :class:`~pystac.Asset` objects. All + :class:`~pystac.Asset` values in the dictionary will have their + :attr:`~pystac.Asset.owner` attribute set to the created Collection. """ assets: Dict[str, Asset] @@ -502,6 +507,7 @@ def __init__( keywords: Optional[List[str]] = None, providers: Optional[List["Provider_Type"]] = None, summaries: Optional[Summaries] = None, + assets: Optional[Dict[str, Asset]] = None, ): super().__init__( id, @@ -521,10 +527,21 @@ def __init__( self.summaries = summaries or Summaries.empty() self.assets = {} + if assets is not None: + for k, asset in assets.items(): + self.add_asset(k, asset) def __repr__(self) -> str: return "".format(self.id) + def _repr_html_(self) -> str: + jinja_env = get_jinja_env() + if jinja_env: + template = jinja_env.get_template("Collection.jinja2") + return str(template.render(catalog=self)) + else: + return escape(repr(self)) + def add_item( self, item: "Item_Type", @@ -562,13 +579,14 @@ def clone(self) -> "Collection": description=self.description, extent=self.extent.clone(), title=self.title, - stac_extensions=self.stac_extensions, - extra_fields=self.extra_fields, + stac_extensions=self.stac_extensions.copy(), + extra_fields=deepcopy(self.extra_fields), catalog_type=self.catalog_type, license=self.license, - keywords=self.keywords, - providers=self.providers, - summaries=self.summaries, + keywords=self.keywords.copy() if self.keywords is not None else None, + providers=deepcopy(self.providers), + summaries=self.summaries.clone(), + assets={k: asset.clone() for k, asset in self.assets.items()}, ) clone._resolved_objects.cache(clone) @@ -621,7 +639,9 @@ def from_dict( if summaries is not None: summaries = Summaries(summaries) - assets: Optional[Dict[str, Any]] = d.get("assets", None) + assets: Optional[Dict[str, Any]] = { + k: Asset.from_dict(v) for k, v in d.get("assets", {}).items() + } links = d.pop("links") d.pop("stac_version") @@ -639,6 +659,7 @@ def from_dict( summaries=summaries, href=href, catalog_type=catalog_type, + assets=assets, ) for link in links: @@ -649,10 +670,6 @@ def from_dict( if link["rel"] != pystac.RelType.SELF or href is None: collection.add_link(Link.from_dict(link)) - if assets is not None: - for asset_key, asset_dict in assets.items(): - collection.add_asset(asset_key, Asset.from_dict(asset_dict)) - if root: collection.set_root(root) diff --git a/pystac/extensions/datacube.py b/pystac/extensions/datacube.py index d02264a64..ea0857c9b 100644 --- a/pystac/extensions/datacube.py +++ b/pystac/extensions/datacube.py @@ -411,6 +411,12 @@ class VariableType(StringEnum): class Variable: + """Object representing a variable in the datacube. The dimensions field lists + zero or more :stac-ext:`Datacube Dimension Object ` + instances. See the :stac-ext:`Datacube Variable Object + ` docs for details. + """ + properties: Dict[str, Any] def __init__(self, properties: Dict[str, Any]) -> None: @@ -522,15 +528,22 @@ class DatacubeExtension( >>> dc_ext = DatacubeExtension.ext(item) """ - def apply(self, dimensions: Dict[str, Dimension]) -> None: + def apply( + self, + dimensions: Dict[str, Dimension], + variables: Optional[Dict[str, Variable]] = None, + ) -> None: """Applies label extension properties to the extended :class:`~pystac.Collection`, :class:`~pystac.Item` or :class:`~pystac.Asset`. Args: dimensions : Dictionary mapping dimension name to a :class:`Dimension` object. + variables : Dictionary mapping variable name to a :class:`Variable` + object. """ self.dimensions = dimensions + self.variables = variables @property def dimensions(self) -> Dict[str, Dimension]: diff --git a/pystac/extensions/grid.py b/pystac/extensions/grid.py new file mode 100644 index 000000000..928897ebc --- /dev/null +++ b/pystac/extensions/grid.py @@ -0,0 +1,107 @@ +"""Implements the :stac-ext:`Grid Extension `.""" + +import re +from typing import Any, Dict, Optional, Pattern, Set, Union + +import pystac +from pystac.extensions.base import ExtensionManagementMixin, PropertiesExtension +from pystac.extensions.hooks import ExtensionHooks + +SCHEMA_URI: str = "https://stac-extensions.github.io/grid/v1.0.0/schema.json" +PREFIX: str = "grid:" + +# Field names +CODE_PROP: str = PREFIX + "code" # required + +CODE_REGEX: str = r"[A-Z]+-[-_.A-Za-z0-9]+" +CODE_PATTERN: Pattern[str] = re.compile(CODE_REGEX) + + +def validated_code(v: str) -> str: + if not isinstance(v, str): + raise ValueError("Invalid Grid code: must be str") + if not CODE_PATTERN.fullmatch(v): + raise ValueError( + f"Invalid Grid code: {v}" f" does not match the regex {CODE_REGEX}" + ) + return v + + +class GridExtension( + PropertiesExtension, + ExtensionManagementMixin[Union[pystac.Item, pystac.Collection]], +): + """A concrete implementation of :class:`GridExtension` on an :class:`~pystac.Item` + that extends the properties of the Item to include properties defined in the + :stac-ext:`Grid Extension `. + + This class should generally not be instantiated directly. Instead, call + :meth:`GridExtension.ext` on an :class:`~pystac.Item` to extend it. + + .. code-block:: python + + >>> item: pystac.Item = ... + >>> proj_ext = GridExtension.ext(item) + """ + + item: pystac.Item + """The :class:`~pystac.Item` being extended.""" + + properties: Dict[str, Any] + """The :class:`~pystac.Item` properties, including extension properties.""" + + def __init__(self, item: pystac.Item): + self.item = item + self.properties = item.properties + + def __repr__(self) -> str: + return "".format(self.item.id) + + def apply(self, code: str) -> None: + """Applies Grid extension properties to the extended Item. + + Args: + code : REQUIRED. The code of the Item's grid location. + """ + self.code = validated_code(code) + + @property + def code(self) -> Optional[str]: + """Get or sets the latitude band of the datasource.""" + return self._get_property(CODE_PROP, str) + + @code.setter + def code(self, v: str) -> None: + self._set_property(CODE_PROP, validated_code(v), pop_if_none=False) + + @classmethod + def get_schema_uri(cls) -> str: + return SCHEMA_URI + + @classmethod + def ext(cls, obj: pystac.Item, add_if_missing: bool = False) -> "GridExtension": + """Extends the given STAC Object with properties from the :stac-ext:`Grid + Extension `. + + This extension can be applied to instances of :class:`~pystac.Item`. + + Raises: + + pystac.ExtensionTypeError : If an invalid object type is passed. + """ + if isinstance(obj, pystac.Item): + cls.validate_has_extension(obj, add_if_missing) + return GridExtension(obj) + else: + raise pystac.ExtensionTypeError( + f"Grid Extension does not apply to type '{type(obj).__name__}'" + ) + + +class GridExtensionHooks(ExtensionHooks): + schema_uri: str = SCHEMA_URI + prev_extension_ids: Set[str] = set() + stac_object_types = {pystac.STACObjectType.ITEM} + + +Grid_EXTENSION_HOOKS: ExtensionHooks = GridExtensionHooks() diff --git a/pystac/extensions/item_assets.py b/pystac/extensions/item_assets.py index c92b1282f..a146e72b5 100644 --- a/pystac/extensions/item_assets.py +++ b/pystac/extensions/item_assets.py @@ -36,6 +36,64 @@ def __eq__(self, o: object) -> bool: return NotImplemented return self.to_dict() == o.to_dict() + @classmethod + def create( + cls, + title: Optional[str], + description: Optional[str], + media_type: Optional[str], + roles: Optional[List[str]], + ) -> "AssetDefinition": + """ + Creates a new asset definition. + + Args: + title : Displayed title for clients and users. + description : Description of the Asset providing additional details, + such as how it was processed or created. + `CommonMark 0.29 `__ syntax MAY be used + for rich text representation. + media_type : `media type\ + `__ + of the asset. + roles : `semantic roles + `__ + of the asset, similar to the use of rel in links. + """ + asset_defn = cls({}) + asset_defn.apply( + title=title, description=description, media_type=media_type, roles=roles + ) + return asset_defn + + def apply( + self, + title: Optional[str], + description: Optional[str], + media_type: Optional[str], + roles: Optional[List[str]], + ) -> None: + """ + Sets the properties for this asset definition. + + Args: + title : Displayed title for clients and users. + description : Description of the Asset providing additional details, + such as how it was processed or created. + `CommonMark 0.29 `__ syntax MAY be used + for rich text representation. + media_type : `media type\ + `__ + of the asset. + roles : `semantic roles + `__ + of the asset, similar to the use of rel in links. + """ + self.title = title + self.description = description + self.media_type = media_type + self.roles = roles + @property def title(self) -> Optional[str]: """Gets or sets the displayed title for clients and users.""" @@ -65,7 +123,7 @@ def description(self, v: Optional[str]) -> None: @property def media_type(self) -> Optional[str]: """Gets or sets the `media type - `__ + `__ of the asset.""" return self.properties.get(ASSET_TYPE_PROP) @@ -79,7 +137,7 @@ def media_type(self, v: Optional[str]) -> None: @property def roles(self) -> Optional[List[str]]: """Gets or sets the `semantic roles - `__ + `__ of the asset, similar to the use of rel in links.""" return self.properties.get(ASSET_ROLES_PROP) diff --git a/pystac/extensions/raster.py b/pystac/extensions/raster.py index 6119b61e7..0c7623c24 100644 --- a/pystac/extensions/raster.py +++ b/pystac/extensions/raster.py @@ -10,7 +10,7 @@ ) from pystac.utils import StringEnum, get_opt, get_required, map_opt -SCHEMA_URI = "https://stac-extensions.github.io/raster/v1.0.0/schema.json" +SCHEMA_URI = "https://stac-extensions.github.io/raster/v1.1.0/schema.json" BANDS_PROP = "raster:bands" @@ -39,6 +39,12 @@ class DataType(StringEnum): OTHER = "other" +class NoDataStrings(StringEnum): + INF = "inf" + NINF = "-inf" + NAN = "nan" + + class Statistics: """Represents statistics information attached to a band in the raster extension. @@ -350,7 +356,7 @@ def __init__(self, properties: Dict[str, Any]) -> None: def apply( self, - nodata: Optional[float] = None, + nodata: Optional[Union[float, NoDataStrings]] = None, sampling: Optional[Sampling] = None, data_type: Optional[DataType] = None, bits_per_sample: Optional[float] = None, @@ -400,7 +406,7 @@ def apply( @classmethod def create( cls, - nodata: Optional[float] = None, + nodata: Optional[Union[float, NoDataStrings]] = None, sampling: Optional[Sampling] = None, data_type: Optional[DataType] = None, bits_per_sample: Optional[float] = None, @@ -452,7 +458,7 @@ def create( return b @property - def nodata(self) -> Optional[float]: + def nodata(self) -> Optional[Union[float, NoDataStrings]]: """Get or sets the nodata pixel value Returns: @@ -461,7 +467,7 @@ def nodata(self) -> Optional[float]: return self.properties.get("nodata") @nodata.setter - def nodata(self, v: Optional[float]) -> None: + def nodata(self, v: Optional[Union[float, NoDataStrings]]) -> None: if v is not None: self.properties["nodata"] = v else: diff --git a/pystac/html/Asset.jinja2 b/pystac/html/Asset.jinja2 new file mode 100644 index 000000000..f771db01a --- /dev/null +++ b/pystac/html/Asset.jinja2 @@ -0,0 +1,31 @@ +{% import 'Macros.jinja2' as macros %} + + diff --git a/pystac/html/Catalog.jinja2 b/pystac/html/Catalog.jinja2 new file mode 100644 index 000000000..b47125d7f --- /dev/null +++ b/pystac/html/Catalog.jinja2 @@ -0,0 +1,30 @@ +{% import 'Macros.jinja2' as macros %} + + \ No newline at end of file diff --git a/pystac/html/Collection.jinja2 b/pystac/html/Collection.jinja2 new file mode 100644 index 000000000..6bb12e664 --- /dev/null +++ b/pystac/html/Collection.jinja2 @@ -0,0 +1,20 @@ +{% extends 'Catalog.jinja2' %} +{% import 'Macros.jinja2' as macros %} + +{% block subclass_fields %} + {# Providers field #} + {% if catalog.providers %} + Providers: +
    + {% for provider in catalog.providers %} +
  • {{ provider._repr_html_() }}
  • + {% endfor %} +
+ + {% endif %} +{% endblock %} + +{% block subclass_details %} + {{ macros.assets(catalog) }} +{% endblock %} + diff --git a/pystac/html/Item.jinja2 b/pystac/html/Item.jinja2 new file mode 100644 index 000000000..3a556b63c --- /dev/null +++ b/pystac/html/Item.jinja2 @@ -0,0 +1,33 @@ +{% import 'Macros.jinja2' as macros %} + + \ No newline at end of file diff --git a/pystac/html/ItemCollection.jinja2 b/pystac/html/ItemCollection.jinja2 new file mode 100644 index 000000000..20835fe29 --- /dev/null +++ b/pystac/html/ItemCollection.jinja2 @@ -0,0 +1,31 @@ +{% import 'Macros.jinja2' as macros %} + + diff --git a/pystac/html/Link.jinja2 b/pystac/html/Link.jinja2 new file mode 100644 index 000000000..5a495264e --- /dev/null +++ b/pystac/html/Link.jinja2 @@ -0,0 +1,23 @@ +{% import 'Macros.jinja2' as macros %} + + \ No newline at end of file diff --git a/pystac/html/Macros.jinja2 b/pystac/html/Macros.jinja2 new file mode 100644 index 000000000..63c79f8f0 --- /dev/null +++ b/pystac/html/Macros.jinja2 @@ -0,0 +1,93 @@ +{% macro square(background_color='#E1E1E1', border_color='#9D9D9D') -%} +
+
+{%- endmacro %} + + +{% macro extra_fields(parent) -%} + {% if parent.extra_fields %} + {% for key, value in parent.extra_fields.items() %} + {{ key }}: {{ value }} + {% endfor %} + {% endif %} +{%- endmacro %} + + +{% macro links(parent) -%} + {% if parent.links|length > 0 %} +
+ +

Links

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

Items

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

Children

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

STAC Extensions

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

Assets

+
+ {% for key, asset in parent.assets.items() %} + {{ asset._repr_html_() }} + {% endfor %} +
+ {% endif %} +{%- endmacro %} \ No newline at end of file diff --git a/pystac/html/Provider.jinja2 b/pystac/html/Provider.jinja2 new file mode 100644 index 000000000..ddca00662 --- /dev/null +++ b/pystac/html/Provider.jinja2 @@ -0,0 +1,12 @@ +{% import 'Macros.jinja2' as macros %} + +
+ {% if provider.url %} + {{ provider.name }} + {% else %} + {{ provider.name }} + {% endif %} + ({{ provider.roles|join(", ") }}){% if provider.description %}: {{ provider.description }} {% endif %} + {{ macros.extra_fields(provider) }} +
+ diff --git a/pystac/html/__init__.py b/pystac/html/__init__.py new file mode 100644 index 000000000..25947a849 --- /dev/null +++ b/pystac/html/__init__.py @@ -0,0 +1,3 @@ +from .jinja_env import get_jinja_env + +__all__ = ["get_jinja_env"] diff --git a/pystac/html/jinja_env.py b/pystac/html/jinja_env.py new file mode 100644 index 000000000..f2389789b --- /dev/null +++ b/pystac/html/jinja_env.py @@ -0,0 +1,19 @@ +from functools import lru_cache +from itertools import islice + + +@lru_cache() +def get_jinja_env(): # type: ignore + try: + from jinja2 import Environment, PackageLoader, select_autoescape + except ModuleNotFoundError: + return None + + environment = Environment( + loader=PackageLoader("pystac", "html"), autoescape=select_autoescape() + ) + + environment.filters["first"] = lambda x: islice(x, 1) + environment.filters["is_nonempty_generator"] = lambda x: next(x, None) is not None + + return environment diff --git a/pystac/item.py b/pystac/item.py index b9730acba..99cc5c1e4 100644 --- a/pystac/item.py +++ b/pystac/item.py @@ -1,11 +1,11 @@ +from html import escape from copy import copy, deepcopy from datetime import datetime as Datetime from pystac.catalog import Catalog from typing import Any, Dict, List, Optional, Union, cast -import dateutil.parser - import pystac +from pystac.html.jinja_env import get_jinja_env from pystac import STACError, STACObjectType from pystac.asset import Asset from pystac.link import Link @@ -49,6 +49,9 @@ class Item(STACObject): belongs to. extra_fields : Extra fields that are part of the top-level JSON properties of the Item. + assets : A dictionary mapping string keys to :class:`~pystac.Asset` objects. All + :class:`~pystac.Asset` values in the dictionary will have their + :attr:`~pystac.Asset.owner` attribute set to the created Item. """ assets: Dict[str, Asset] @@ -105,6 +108,7 @@ def __init__( href: Optional[str] = None, collection: Optional[Union[str, Collection]] = None, extra_fields: Optional[Dict[str, Any]] = None, + assets: Optional[Dict[str, Asset]] = None, ): super().__init__(stac_extensions or []) @@ -142,9 +146,22 @@ def __init__( else: self.collection_id = collection + self.assets = {} + if assets is not None: + for k, asset in assets.items(): + self.add_asset(k, asset) + def __repr__(self) -> str: return "".format(self.id) + def _repr_html_(self) -> str: + jinja_env = get_jinja_env() + if jinja_env: + template = jinja_env.get_template("Item.jinja2") + return str(template.render(item=self)) + else: + return escape(repr(self)) + def set_self_href(self, href: Optional[str]) -> None: """Sets the absolute HREF that is represented by the ``rel == 'self'`` :class:`~pystac.Link`. @@ -349,13 +366,11 @@ def clone(self) -> "Item": properties=deepcopy(self.properties), stac_extensions=deepcopy(self.stac_extensions), collection=self.collection_id, + assets={k: asset.clone() for k, asset in self.assets.items()}, ) for link in self.links: clone.add_link(link.clone()) - for k, asset in self.assets.items(): - clone.add_asset(k, asset.clone()) - return clone def _object_links(self) -> List[Union[str, pystac.RelType]]: @@ -394,7 +409,7 @@ def from_dict( datetime = properties.get("datetime") if datetime is not None: - datetime = dateutil.parser.parse(datetime) + datetime = str_to_datetime(datetime) links = d.pop("links") assets = d.pop("assets") @@ -410,6 +425,7 @@ def from_dict( stac_extensions=stac_extensions, collection=collection_id, extra_fields=d, + assets={k: Asset.from_dict(v) for k, v in assets.items()}, ) has_self_link = False @@ -420,11 +436,6 @@ def from_dict( if not has_self_link and href is not None: item.add_link(Link.self_href(href)) - for k, v in assets.items(): - asset = Asset.from_dict(v) - asset.set_owner(item) - item.assets[k] = asset - if root: item.set_root(root) diff --git a/pystac/item_collection.py b/pystac/item_collection.py index de38ac34f..4ff373c16 100644 --- a/pystac/item_collection.py +++ b/pystac/item_collection.py @@ -1,8 +1,10 @@ from copy import deepcopy from pystac.errors import STACTypeError from typing import Any, Dict, Iterator, List, Optional, Collection, Iterable, Union +from html import escape import pystac +from pystac.html.jinja_env import get_jinja_env from pystac.utils import make_absolute_href, is_absolute_href from pystac.serialization.identify import identify_stac_object_type @@ -235,3 +237,11 @@ def is_item_collection(d: Dict[str, Any]) -> bool: identify_stac_object_type(feature) == pystac.STACObjectType.ITEM for feature in d.get("features", []) ) + + def _repr_html_(self) -> str: + jinja_env = get_jinja_env() + if jinja_env: + template = jinja_env.get_template("ItemCollection.jinja2") + return str(template.render(item_collection=self)) + else: + return escape(repr(self)) diff --git a/pystac/link.py b/pystac/link.py index e5e36d40e..57b69b4da 100644 --- a/pystac/link.py +++ b/pystac/link.py @@ -1,8 +1,10 @@ import os from copy import copy +from html import escape from typing import Any, Dict, Optional, TYPE_CHECKING, Union import pystac +from pystac.html.jinja_env import get_jinja_env from pystac.utils import make_absolute_href, make_relative_href, is_absolute_href if TYPE_CHECKING: @@ -254,6 +256,14 @@ def __fspath__(self) -> str: def __repr__(self) -> str: return "".format(self.rel, self.target) + def _repr_html_(self) -> str: + jinja_env = get_jinja_env() + if jinja_env: + template = jinja_env.get_template("Link.jinja2") + return str(template.render(link=self)) + else: + return escape(repr(self)) + def resolve_stac_object(self, root: Optional["Catalog_Type"] = None) -> "Link": """Resolves a STAC object from the HREF of this link, if the link is not already resolved. @@ -298,6 +308,10 @@ def resolve_stac_object(self, root: Optional["Catalog_Type"] = None) -> "Link": if self.owner is not None: if isinstance(self.owner, pystac.Catalog): stac_io = self.owner._stac_io + elif self.rel != pystac.RelType.ROOT: + owner_root = self.owner.get_root() + if owner_root is not None: + stac_io = owner_root._stac_io if stac_io is None: stac_io = pystac.StacIO.default() diff --git a/pystac/media_type.py b/pystac/media_type.py index 57c47e256..e6e7c9b37 100644 --- a/pystac/media_type.py +++ b/pystac/media_type.py @@ -10,6 +10,7 @@ class MediaType(StringEnum): GEOTIFF = "image/tiff; application=geotiff" HDF = "application/x-hdf" # Hierarchical Data Format versions 4 and earlier. HDF5 = "application/x-hdf5" # Hierarchical Data Format version 5 + HTML = "text/html" JPEG = "image/jpeg" JPEG2000 = "image/jp2" JSON = "application/json" @@ -17,3 +18,4 @@ class MediaType(StringEnum): TEXT = "text/plain" TIFF = "image/tiff" XML = "application/xml" + PDF = "application/pdf" diff --git a/pystac/provider.py b/pystac/provider.py index d03d59c28..0622433d0 100644 --- a/pystac/provider.py +++ b/pystac/provider.py @@ -1,5 +1,7 @@ from typing import Any, Dict, List, Optional +from html import escape +from pystac.html.jinja_env import get_jinja_env from pystac.utils import StringEnum @@ -70,6 +72,14 @@ def __eq__(self, o: object) -> bool: return NotImplemented return self.to_dict() == o.to_dict() + def _repr_html_(self) -> str: + jinja_env = get_jinja_env() + if jinja_env: + template = jinja_env.get_template("Provider.jinja2") + return str(template.render(provider=self)) + else: + return escape(repr(self)) + def to_dict(self) -> Dict[str, Any]: """Generate a dictionary representing the JSON of this Provider. diff --git a/pystac/stac_io.py b/pystac/stac_io.py index dd64e1930..6d98fffc7 100644 --- a/pystac/stac_io.py +++ b/pystac/stac_io.py @@ -42,11 +42,11 @@ class StacIO(ABC): def read_text(self, source: HREF, *args: Any, **kwargs: Any) -> str: """Read text from the given URI. - The source to read from can be specified as a string or a - :class:`~pystac.Link`. If it is a string, it must be a URI or local path from - which to read. Using a :class:`~pystac.Link` enables implementations to use - additional link information, such as paging information contained in the - extended links described in the `STAC API spec + The source to read from can be specified as a string or :class:`os.PathLike` + object (:class:`~pystac.Link` is a path-like object). If it is a string, it + must be a URI or local path from which to read. Using a :class:`~pystac.Link` + enables implementations to use additional link information, such as paging + information contained in the extended links described in the `STAC API spec `__. Args: @@ -71,10 +71,11 @@ def write_text( ) -> None: """Write the given text to a file at the given URI. - The destination to write to from can be specified as a string or a - :class:`~pystac.Link`. If it is a string, it must be a URI or local path from - which to read. Using a :class:`~pystac.Link` enables implementations to use - additional link information. + The destination to write to can be specified as a string or + :class:`os.PathLike` object (:class:`~pystac.Link` is a path-like object). If + it is a string, it must be a URI or local path from which to read. Using a + :class:`~pystac.Link` enables implementations to use additional link + information. Args: dest : The destination to write to. diff --git a/pystac/summaries.py b/pystac/summaries.py index 9652b4b7b..f7850628c 100644 --- a/pystac/summaries.py +++ b/pystac/summaries.py @@ -1,3 +1,4 @@ +from copy import deepcopy import sys import numbers from enum import Enum @@ -287,6 +288,21 @@ def is_empty(self) -> bool: any(self.lists) or any(self.ranges) or any(self.schemas) or any(self.other) ) + def clone(self) -> "Summaries": + """Clones this object. + + Returns: + Summaries: The clone of this object + """ + summaries = Summaries( + summaries=deepcopy(self._summaries), maxcount=self.maxcount + ) + summaries.lists = deepcopy(self.lists) + summaries.other = deepcopy(self.other) + summaries.ranges = deepcopy(self.ranges) + summaries.schemas = deepcopy(self.schemas) + return summaries + def to_dict(self) -> Dict[str, Any]: return { **{k: v for k, v in self.lists.items() if len(v) < self.maxcount}, diff --git a/pystac/utils.py b/pystac/utils.py index 8c1d4fd04..860e4ebba 100644 --- a/pystac/utils.py +++ b/pystac/utils.py @@ -330,7 +330,7 @@ def str_to_datetime(s: str) -> datetime: Args: s (str) : The string to convert to :class:`datetime.datetime`. """ - return dateutil.parser.parse(s) + return dateutil.parser.isoparse(s) def geometry_to_bbox(geometry: Dict[str, Any]) -> List[float]: diff --git a/pystac/version.py b/pystac/version.py index 32f4a026a..ba4486ac1 100644 --- a/pystac/version.py +++ b/pystac/version.py @@ -1,7 +1,7 @@ import os from typing import Optional -__version__ = "1.4.0" +__version__ = "1.5.0" """Library version""" diff --git a/requirements-docs.txt b/requirements-docs.txt index 52ae5c0d0..ec5b5d99e 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,7 +1,7 @@ -ipython==8.0.1 -Sphinx==4.4.0 +ipython==8.4.0 +Sphinx==4.5.0 sphinxcontrib-fulltoc==1.2.0 -nbsphinx==0.8.8 -pydata-sphinx-theme==0.8.0 +nbsphinx==0.8.9 +pydata-sphinx-theme==0.8.1 sphinx-panels==0.6.0 jinja2<4.0 diff --git a/requirements-test.txt b/requirements-test.txt index d6d16c626..1b34d47ca 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,17 +1,17 @@ -mypy==0.931 +mypy==0.961 flake8==4.0.1 -black==22.1.0 +black==22.6.0 codespell==2.1.0 -jsonschema==4.4.0 -coverage==6.3.2 -doc8==0.10.1 +jsonschema==4.7.2 +coverage==6.4.2 +doc8==0.11.2 -types-python-dateutil==2.8.9 +types-python-dateutil==2.8.18 types-orjson==3.6.2 -pre-commit==2.17.0 +pre-commit==2.20.0 # optional dependencies -orjson==3.6.7 +orjson==3.7.7 diff --git a/setup.cfg b/setup.cfg index ff51dc013..36013b9a7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,9 @@ [metadata] version = attr: pystac.version.__version__ + +[tool:pytest] +minversion = 6.0 +addopts = -ra -q +testpaths = + tests +asyncio_mode = auto \ No newline at end of file diff --git a/setup.py b/setup.py index 1bc077b2e..ca4065801 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ author_email="stac@radiant.earth", url="https://github.com/stac-utils/pystac", packages=find_packages(exclude=["tests*"]), - package_data={"": ["py.typed"]}, + package_data={"": ["py.typed", "*.jinja2"]}, py_modules=[splitext(basename(path))[0] for path in glob("pystac/*.py")], python_requires=">=3.7", install_requires=[ diff --git a/tests/data-files/examples/1.0.0/example-sentinel2.json b/tests/data-files/examples/1.0.0/example-sentinel2.json new file mode 100644 index 000000000..7d6bf667e --- /dev/null +++ b/tests/data-files/examples/1.0.0/example-sentinel2.json @@ -0,0 +1,818 @@ +{ + "type": "Feature", + "stac_version": "1.0.0", + "id": "S2A_MSIL1C_20210908T042701_R133_T46RER_20210908T070248", + "properties": { + "providers": [ + { + "name": "ESA", + "roles": [ + "producer", + "processor", + "licensor" + ], + "url": "https://earth.esa.int/web/guest/home" + } + ], + "platform": "sentinel-2a", + "constellation": "sentinel-2", + "instruments": [ + "msi" + ], + "eo:cloud_cover": 88.2972, + "sat:orbit_state": "descending", + "sat:relative_orbit": 133, + "proj:epsg": 32646, + "mgrs:utm_zone": 46, + "mgrs:latitude_band": "R", + "mgrs:grid_square": "ER", + "grid:code": "MGRS-46RER", + "view:sun_azimuth": 142.987598836457, + "view:sun_elevation": 63.5068357330561, + "sentinel2:product_uri": "S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE", + "sentinel2:generation_time": "2021-09-08T07:02:48.000000Z", + "sentinel2:processing_baseline": "03.01", + "sentinel2:product_type": "S2MSI1C", + "sentinel2:datatake_id": "GS2A_20210908T042701_032448_N03.01", + "sentinel2:datatake_type": "INS-NOBS", + "sentinel2:datastrip_id": "S2A_OPER_MSI_L1C_DS_VGS4_20210908T070248_S20210908T043714_N03.01", + "sentinel2:granule_id": "S2A_OPER_MSI_L1C_TL_VGS4_20210908T070248_A032448_T46RER_N03.01", + "sentinel2:mgrs_tile": "46RER", + "sentinel2:reflectance_conversion_factor": 0.983841990384341, + "sentinel2:degraded_msi_data_percentage": 0.0, + "datetime": "2021-09-08T04:27:01.024000Z" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 93.43341763893636, + 28.023673957735582 + ], + [ + 93.40196468575007, + 27.9143684592852 + ], + [ + 93.36091831507485, + 27.766687360630367 + ], + [ + 93.31695738599002, + 27.619769241040945 + ], + [ + 93.2783823780312, + 27.471306867624268 + ], + [ + 93.23722635567444, + 27.32359772615637 + ], + [ + 93.19755659090528, + 27.17555811144318 + ], + [ + 93.16090624762064, + 27.033537745066344 + ], + [ + 92.99979835697575, + 27.03417096113827 + ], + [ + 92.99979654001324, + 28.025435531419042 + ], + [ + 93.43341763893636, + 28.023673957735582 + ] + ] + ] + }, + "links": [ + { + "rel": "license", + "href": "https://sentinel.esa.int/documents/247904/690755/Sentinel_Data_Legal_Notice" + }, + { + "rel": "self", + "href": "/var/folders/3q/jbg6x0zx3194zq6_2jbwygjw0000gn/T/tmp9ewihdpa/S2A_MSIL1C_20210908T042701_R133_T46RER_20210908T070248.json", + "type": "application/json" + } + ], + "assets": { + "coastal": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B01.jp2", + "type": "image/jp2", + "title": "Coastal aerosol (band 1) - 60m", + "eo:bands": [ + { + "name": "coastal", + "common_name": "coastal", + "description": "Coastal aerosol (band 1)", + "center_wavelength": 0.443, + "full_width_half_max": 0.027 + } + ], + "gsd": 60, + "proj:shape": [ + 1830, + 1830 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 60.0, + 0.0, + 499980.0, + 0.0, + -60.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 60, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "blue": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B02.jp2", + "type": "image/jp2", + "title": "Blue (band 2) - 10m", + "eo:bands": [ + { + "name": "blue", + "common_name": "blue", + "description": "Blue (band 2)", + "center_wavelength": 0.49, + "full_width_half_max": 0.098 + } + ], + "gsd": 10, + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "green": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B03.jp2", + "type": "image/jp2", + "title": "Green (band 3) - 10m", + "eo:bands": [ + { + "name": "green", + "common_name": "green", + "description": "Green (band 3)", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + } + ], + "gsd": 10, + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "red": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B04.jp2", + "type": "image/jp2", + "title": "Red (band 4) - 10m", + "eo:bands": [ + { + "name": "red", + "common_name": "red", + "description": "Red (band 4)", + "center_wavelength": 0.665, + "full_width_half_max": 0.038 + } + ], + "gsd": 10, + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "rededge1": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B05.jp2", + "type": "image/jp2", + "title": "Red edge 1 (band 5) - 20m", + "eo:bands": [ + { + "name": "rededge1", + "common_name": "rededge", + "description": "Red edge 1 (band 5)", + "center_wavelength": 0.704, + "full_width_half_max": 0.019 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "rededge2": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B06.jp2", + "type": "image/jp2", + "title": "Red edge 2 (band 6) - 20m", + "eo:bands": [ + { + "name": "rededge2", + "common_name": "rededge", + "description": "Red edge 2 (band 6)", + "center_wavelength": 0.74, + "full_width_half_max": 0.018 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "rededge3": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B07.jp2", + "type": "image/jp2", + "title": "Red edge 3 (band 7) - 20m", + "eo:bands": [ + { + "name": "rededge3", + "common_name": "rededge", + "description": "Red edge 3 (band 7)", + "center_wavelength": 0.783, + "full_width_half_max": 0.028 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "nir": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B08.jp2", + "type": "image/jp2", + "title": "NIR 1 (band 8) - 10m", + "eo:bands": [ + { + "name": "nir", + "common_name": "nir", + "description": "NIR 1 (band 8)", + "center_wavelength": 0.842, + "full_width_half_max": 0.145 + } + ], + "gsd": 10, + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "nir08": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B8A.jp2", + "type": "image/jp2", + "title": "NIR 2 (band 8A) - 20m", + "eo:bands": [ + { + "name": "nir08", + "common_name": "nir08", + "description": "NIR 2 (band 8A)", + "center_wavelength": 0.865, + "full_width_half_max": 0.033 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "nir09": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B09.jp2", + "type": "image/jp2", + "title": "NIR 3 (band 9) - 60m", + "eo:bands": [ + { + "name": "nir09", + "common_name": "nir09", + "description": "NIR 3 (band 9)", + "center_wavelength": 0.945, + "full_width_half_max": 0.026 + } + ], + "gsd": 60, + "proj:shape": [ + 1830, + 1830 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 60.0, + 0.0, + 499980.0, + 0.0, + -60.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 60, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "cirrus": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B10.jp2", + "type": "image/jp2", + "title": "Cirrus (band 10) - 60m", + "eo:bands": [ + { + "name": "cirrus", + "common_name": "cirrus", + "description": "Cirrus (band 10)", + "center_wavelength": 1.3735, + "full_width_half_max": 0.075 + } + ], + "gsd": 60, + "proj:shape": [ + 1830, + 1830 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 60.0, + 0.0, + 499980.0, + 0.0, + -60.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 60, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "swir16": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B11.jp2", + "type": "image/jp2", + "title": "SWIR 1 (band 11) - 20m", + "eo:bands": [ + { + "name": "swir16", + "common_name": "swir16", + "description": "SWIR 1 (band 11)", + "center_wavelength": 1.61, + "full_width_half_max": 0.143 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "swir22": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B12.jp2", + "type": "image/jp2", + "title": "SWIR 2 (band 12) - 20m", + "eo:bands": [ + { + "name": "swir22", + "common_name": "swir22", + "description": "SWIR 2 (band 12)", + "center_wavelength": 2.19, + "full_width_half_max": 0.242 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "visual": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_TCI.jp2", + "type": "image/jp2", + "title": "True color image", + "eo:bands": [ + { + "name": "red", + "common_name": "red", + "description": "Red (band 4)", + "center_wavelength": 0.665, + "full_width_half_max": 0.038 + }, + { + "name": "green", + "common_name": "green", + "description": "Green (band 3)", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + }, + { + "name": "blue", + "common_name": "blue", + "description": "Blue (band 2)", + "center_wavelength": 0.49, + "full_width_half_max": 0.098 + } + ], + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "roles": [ + "visual" + ] + }, + "safe_manifest": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/manifest.safe", + "type": "application/xml", + "roles": [ + "metadata" + ] + }, + "product_metadata": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/MTD_MSIL1C.xml", + "type": "application/xml", + "roles": [ + "metadata" + ] + }, + "granule_metadata": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/MTD_TL.xml", + "type": "application/xml", + "roles": [ + "metadata" + ] + }, + "inspire_metadata": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/INSPIRE.xml", + "type": "application/xml", + "roles": [ + "metadata" + ] + }, + "datastrip_metadata": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/DATASTRIP/DS_VGS4_20210908T070248_S20210908T043714/MTD_DS.xml", + "type": "application/xml", + "roles": [ + "metadata" + ] + } + }, + "bbox": [ + 92.99979654001324, + 27.033537745066344, + 93.43341763893636, + 28.025435531419042 + ], + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.0.0/schema.json", + "https://stac-extensions.github.io/sat/v1.0.0/schema.json", + "https://stac-extensions.github.io/projection/v1.0.0/schema.json", + "https://stac-extensions.github.io/mgrs/v1.0.0/schema.json", + "https://stac-extensions.github.io/grid/v1.0.0/schema.json", + "https://stac-extensions.github.io/view/v1.0.0/schema.json" + ] +} \ No newline at end of file diff --git a/tests/data-files/examples/example-info.csv b/tests/data-files/examples/example-info.csv index 4e980338e..faf65b02a 100644 --- a/tests/data-files/examples/example-info.csv +++ b/tests/data-files/examples/example-info.csv @@ -119,6 +119,7 @@ "1.0.0/collection.json","Collection","1.0.0","https://stac-extensions.github.io/eo/v1.0.0/schema.json|https://stac-extensions.github.io/view/v1.0.0/schema.json" "1.0.0/collectionless-item.json","Feature","1.0.0","https://stac-extensions.github.io/eo/v1.0.0/schema.json|https://stac-extensions.github.io/view/v1.0.0/schema.json" "1.0.0/core-item.json","Feature","1.0.0","" +"1.0.0/example-sentinel2.json","Feature","1.0.0","https://stac-extensions.github.io/eo/v1.0.0/schema.json|https://stac-extensions.github.io/sat/v1.0.0/schema.json|https://stac-extensions.github.io/projection/v1.0.0/schema.json|https://stac-extensions.github.io/mgrs/v1.0.0/schema.json|https://stac-extensions.github.io/grid/v1.0.0/schema.json|https://stac-extensions.github.io/view/v1.0.0/schema.json" "1.0.0/extended-item.json","Feature","1.0.0","https://stac-extensions.github.io/eo/v1.0.0/schema.json|https://stac-extensions.github.io/projection/v1.0.0/schema.json|https://stac-extensions.github.io/scientific/v1.0.0/schema.json|https://stac-extensions.github.io/view/v1.0.0/schema.json|https://stac-extensions.github.io/remote-data/v1.0.0/schema.json" "1.0.0/extensions-collection/collection.json","Collection","1.0.0","" "1.0.0/extensions-collection/proj-example/proj-example.json","Feature","1.0.0","https://stac-extensions.github.io/eo/v1.0.0/schema.json|https://stac-extensions.github.io/projection/v1.0.0/schema.json" diff --git a/tests/data-files/grid/example-sentinel2.json b/tests/data-files/grid/example-sentinel2.json new file mode 100644 index 000000000..7d6bf667e --- /dev/null +++ b/tests/data-files/grid/example-sentinel2.json @@ -0,0 +1,818 @@ +{ + "type": "Feature", + "stac_version": "1.0.0", + "id": "S2A_MSIL1C_20210908T042701_R133_T46RER_20210908T070248", + "properties": { + "providers": [ + { + "name": "ESA", + "roles": [ + "producer", + "processor", + "licensor" + ], + "url": "https://earth.esa.int/web/guest/home" + } + ], + "platform": "sentinel-2a", + "constellation": "sentinel-2", + "instruments": [ + "msi" + ], + "eo:cloud_cover": 88.2972, + "sat:orbit_state": "descending", + "sat:relative_orbit": 133, + "proj:epsg": 32646, + "mgrs:utm_zone": 46, + "mgrs:latitude_band": "R", + "mgrs:grid_square": "ER", + "grid:code": "MGRS-46RER", + "view:sun_azimuth": 142.987598836457, + "view:sun_elevation": 63.5068357330561, + "sentinel2:product_uri": "S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE", + "sentinel2:generation_time": "2021-09-08T07:02:48.000000Z", + "sentinel2:processing_baseline": "03.01", + "sentinel2:product_type": "S2MSI1C", + "sentinel2:datatake_id": "GS2A_20210908T042701_032448_N03.01", + "sentinel2:datatake_type": "INS-NOBS", + "sentinel2:datastrip_id": "S2A_OPER_MSI_L1C_DS_VGS4_20210908T070248_S20210908T043714_N03.01", + "sentinel2:granule_id": "S2A_OPER_MSI_L1C_TL_VGS4_20210908T070248_A032448_T46RER_N03.01", + "sentinel2:mgrs_tile": "46RER", + "sentinel2:reflectance_conversion_factor": 0.983841990384341, + "sentinel2:degraded_msi_data_percentage": 0.0, + "datetime": "2021-09-08T04:27:01.024000Z" + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 93.43341763893636, + 28.023673957735582 + ], + [ + 93.40196468575007, + 27.9143684592852 + ], + [ + 93.36091831507485, + 27.766687360630367 + ], + [ + 93.31695738599002, + 27.619769241040945 + ], + [ + 93.2783823780312, + 27.471306867624268 + ], + [ + 93.23722635567444, + 27.32359772615637 + ], + [ + 93.19755659090528, + 27.17555811144318 + ], + [ + 93.16090624762064, + 27.033537745066344 + ], + [ + 92.99979835697575, + 27.03417096113827 + ], + [ + 92.99979654001324, + 28.025435531419042 + ], + [ + 93.43341763893636, + 28.023673957735582 + ] + ] + ] + }, + "links": [ + { + "rel": "license", + "href": "https://sentinel.esa.int/documents/247904/690755/Sentinel_Data_Legal_Notice" + }, + { + "rel": "self", + "href": "/var/folders/3q/jbg6x0zx3194zq6_2jbwygjw0000gn/T/tmp9ewihdpa/S2A_MSIL1C_20210908T042701_R133_T46RER_20210908T070248.json", + "type": "application/json" + } + ], + "assets": { + "coastal": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B01.jp2", + "type": "image/jp2", + "title": "Coastal aerosol (band 1) - 60m", + "eo:bands": [ + { + "name": "coastal", + "common_name": "coastal", + "description": "Coastal aerosol (band 1)", + "center_wavelength": 0.443, + "full_width_half_max": 0.027 + } + ], + "gsd": 60, + "proj:shape": [ + 1830, + 1830 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 60.0, + 0.0, + 499980.0, + 0.0, + -60.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 60, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "blue": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B02.jp2", + "type": "image/jp2", + "title": "Blue (band 2) - 10m", + "eo:bands": [ + { + "name": "blue", + "common_name": "blue", + "description": "Blue (band 2)", + "center_wavelength": 0.49, + "full_width_half_max": 0.098 + } + ], + "gsd": 10, + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "green": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B03.jp2", + "type": "image/jp2", + "title": "Green (band 3) - 10m", + "eo:bands": [ + { + "name": "green", + "common_name": "green", + "description": "Green (band 3)", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + } + ], + "gsd": 10, + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "red": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B04.jp2", + "type": "image/jp2", + "title": "Red (band 4) - 10m", + "eo:bands": [ + { + "name": "red", + "common_name": "red", + "description": "Red (band 4)", + "center_wavelength": 0.665, + "full_width_half_max": 0.038 + } + ], + "gsd": 10, + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "rededge1": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B05.jp2", + "type": "image/jp2", + "title": "Red edge 1 (band 5) - 20m", + "eo:bands": [ + { + "name": "rededge1", + "common_name": "rededge", + "description": "Red edge 1 (band 5)", + "center_wavelength": 0.704, + "full_width_half_max": 0.019 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "rededge2": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B06.jp2", + "type": "image/jp2", + "title": "Red edge 2 (band 6) - 20m", + "eo:bands": [ + { + "name": "rededge2", + "common_name": "rededge", + "description": "Red edge 2 (band 6)", + "center_wavelength": 0.74, + "full_width_half_max": 0.018 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "rededge3": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B07.jp2", + "type": "image/jp2", + "title": "Red edge 3 (band 7) - 20m", + "eo:bands": [ + { + "name": "rededge3", + "common_name": "rededge", + "description": "Red edge 3 (band 7)", + "center_wavelength": 0.783, + "full_width_half_max": 0.028 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "nir": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B08.jp2", + "type": "image/jp2", + "title": "NIR 1 (band 8) - 10m", + "eo:bands": [ + { + "name": "nir", + "common_name": "nir", + "description": "NIR 1 (band 8)", + "center_wavelength": 0.842, + "full_width_half_max": 0.145 + } + ], + "gsd": 10, + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 10, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "nir08": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B8A.jp2", + "type": "image/jp2", + "title": "NIR 2 (band 8A) - 20m", + "eo:bands": [ + { + "name": "nir08", + "common_name": "nir08", + "description": "NIR 2 (band 8A)", + "center_wavelength": 0.865, + "full_width_half_max": 0.033 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "nir09": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B09.jp2", + "type": "image/jp2", + "title": "NIR 3 (band 9) - 60m", + "eo:bands": [ + { + "name": "nir09", + "common_name": "nir09", + "description": "NIR 3 (band 9)", + "center_wavelength": 0.945, + "full_width_half_max": 0.026 + } + ], + "gsd": 60, + "proj:shape": [ + 1830, + 1830 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 60.0, + 0.0, + 499980.0, + 0.0, + -60.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 60, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "cirrus": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B10.jp2", + "type": "image/jp2", + "title": "Cirrus (band 10) - 60m", + "eo:bands": [ + { + "name": "cirrus", + "common_name": "cirrus", + "description": "Cirrus (band 10)", + "center_wavelength": 1.3735, + "full_width_half_max": 0.075 + } + ], + "gsd": 60, + "proj:shape": [ + 1830, + 1830 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 60.0, + 0.0, + 499980.0, + 0.0, + -60.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 60, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "swir16": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B11.jp2", + "type": "image/jp2", + "title": "SWIR 1 (band 11) - 20m", + "eo:bands": [ + { + "name": "swir16", + "common_name": "swir16", + "description": "SWIR 1 (band 11)", + "center_wavelength": 1.61, + "full_width_half_max": 0.143 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "swir22": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_B12.jp2", + "type": "image/jp2", + "title": "SWIR 2 (band 12) - 20m", + "eo:bands": [ + { + "name": "swir22", + "common_name": "swir22", + "description": "SWIR 2 (band 12)", + "center_wavelength": 2.19, + "full_width_half_max": 0.242 + } + ], + "gsd": 20, + "proj:shape": [ + 5490, + 5490 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 20.0, + 0.0, + 499980.0, + 0.0, + -20.0, + 3100020.0 + ], + "raster:bands": [ + { + "nodata": 0, + "data_type": "uint16", + "bits_per_sample": 15, + "spatial_resolution": 20, + "unit": "none", + "scale": 0.0001, + "offset": 0 + } + ], + "roles": [ + "data" + ] + }, + "visual": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/IMG_DATA/T46RER_20210908T042701_TCI.jp2", + "type": "image/jp2", + "title": "True color image", + "eo:bands": [ + { + "name": "red", + "common_name": "red", + "description": "Red (band 4)", + "center_wavelength": 0.665, + "full_width_half_max": 0.038 + }, + { + "name": "green", + "common_name": "green", + "description": "Green (band 3)", + "center_wavelength": 0.56, + "full_width_half_max": 0.045 + }, + { + "name": "blue", + "common_name": "blue", + "description": "Blue (band 2)", + "center_wavelength": 0.49, + "full_width_half_max": 0.098 + } + ], + "proj:shape": [ + 10980, + 10980 + ], + "proj:bbox": [ + 499980.0, + 2990220.0, + 609780.0, + 3100020.0 + ], + "proj:transform": [ + 10.0, + 0.0, + 499980.0, + 0.0, + -10.0, + 3100020.0 + ], + "roles": [ + "visual" + ] + }, + "safe_manifest": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/manifest.safe", + "type": "application/xml", + "roles": [ + "metadata" + ] + }, + "product_metadata": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/MTD_MSIL1C.xml", + "type": "application/xml", + "roles": [ + "metadata" + ] + }, + "granule_metadata": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/GRANULE/L1C_T46RER_A032448_20210908T043714/MTD_TL.xml", + "type": "application/xml", + "roles": [ + "metadata" + ] + }, + "inspire_metadata": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/INSPIRE.xml", + "type": "application/xml", + "roles": [ + "metadata" + ] + }, + "datastrip_metadata": { + "href": "/Users/philvarner/code/sentinel2/tests/data-files/S2A_MSIL1C_20210908T042701_N0301_R133_T46RER_20210908T070248.SAFE/DATASTRIP/DS_VGS4_20210908T070248_S20210908T043714/MTD_DS.xml", + "type": "application/xml", + "roles": [ + "metadata" + ] + } + }, + "bbox": [ + 92.99979654001324, + 27.033537745066344, + 93.43341763893636, + 28.025435531419042 + ], + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.0.0/schema.json", + "https://stac-extensions.github.io/sat/v1.0.0/schema.json", + "https://stac-extensions.github.io/projection/v1.0.0/schema.json", + "https://stac-extensions.github.io/mgrs/v1.0.0/schema.json", + "https://stac-extensions.github.io/grid/v1.0.0/schema.json", + "https://stac-extensions.github.io/view/v1.0.0/schema.json" + ] +} \ No newline at end of file diff --git a/tests/data-files/raster/raster-planet-example.json b/tests/data-files/raster/raster-planet-example.json index b85aea962..66fb39d3c 100644 --- a/tests/data-files/raster/raster-planet-example.json +++ b/tests/data-files/raster/raster-planet-example.json @@ -1245,6 +1245,6 @@ "https://stac-extensions.github.io/projection/v1.0.0/schema.json", "https://stac-extensions.github.io/eo/v1.0.0/schema.json", "https://stac-extensions.github.io/processing/v1.0.0/schema.json", - "https://stac-extensions.github.io/raster/v1.0.0/schema.json" + "https://stac-extensions.github.io/raster/v1.1.0/schema.json" ] } \ No newline at end of file diff --git a/tests/data-files/raster/raster-sentinel2-example.json b/tests/data-files/raster/raster-sentinel2-example.json index dbaaef625..807270404 100644 --- a/tests/data-files/raster/raster-sentinel2-example.json +++ b/tests/data-files/raster/raster-sentinel2-example.json @@ -548,6 +548,13 @@ "full_width_half_max": 0.026 } ], + "raster:bands": [ + { + "data_type": "uint16", + "spatial_resolution": 60, + "nodata": "nan" + } + ], "proj:shape": [ 1830, 1830 @@ -711,7 +718,7 @@ "https://stac-extensions.github.io/eo/v1.0.0/schema.json", "https://stac-extensions.github.io/view/v1.0.0/schema.json", "https://stac-extensions.github.io/projection/v1.0.0/schema.json", - "https://stac-extensions.github.io/raster/v1.0.0/schema.json" + "https://stac-extensions.github.io/raster/v1.1.0/schema.json" ], "virtual:assets": { "SIR": { diff --git a/tests/extensions/test_datacube.py b/tests/extensions/test_datacube.py index ef1f60e8c..f7ee17217 100644 --- a/tests/extensions/test_datacube.py +++ b/tests/extensions/test_datacube.py @@ -87,3 +87,16 @@ def test_set_variables(self) -> None: self.assertEqual( item.properties["cube:variables"], {"temp": new_variable.to_dict()} ) + + def test_apply_variables(self) -> None: + item = pystac.Item.from_file(self.example_uri) + cube = DatacubeExtension.ext(item) + variables = cube.variables + assert variables is not None + key, value = variables.popitem() + target = value.to_dict() + cube.variables = None + cube.apply(dimensions={}, variables={key: value}) + variables = cube.variables + assert variables is not None + self.assertEqual(target, cube.variables[key].to_dict()) diff --git a/tests/extensions/test_eo.py b/tests/extensions/test_eo.py index be879f9f3..656fb3e55 100644 --- a/tests/extensions/test_eo.py +++ b/tests/extensions/test_eo.py @@ -21,6 +21,7 @@ def test_create(self) -> None: description=Band.band_description("red"), center_wavelength=0.65, full_width_half_max=0.1, + solar_illumination=42.0, ) self.assertEqual(band.name, "B01") @@ -28,7 +29,7 @@ def test_create(self) -> None: self.assertEqual(band.description, "Common name: red, Range: 0.6 to 0.7") self.assertEqual(band.center_wavelength, 0.65) self.assertEqual(band.full_width_half_max, 0.1) - + self.assertEqual(band.solar_illumination, 42.0) self.assertEqual(band.__repr__(), "") def test_band_description_unknown_band(self) -> None: diff --git a/tests/extensions/test_grid.py b/tests/extensions/test_grid.py new file mode 100644 index 000000000..e2edb9610 --- /dev/null +++ b/tests/extensions/test_grid.py @@ -0,0 +1,122 @@ +"""Tests for pystac.extensions.grid.""" + +import datetime +from typing import Any, Dict +import unittest + +import pystac +from pystac import ExtensionTypeError +from pystac.extensions import grid +from pystac.extensions.grid import GridExtension +from tests.utils import TestCases + +code = "MGRS-4CFJ" + + +def make_item() -> pystac.Item: + """Create basic test items that are only slightly different.""" + asset_id = "an/asset" + start = datetime.datetime(2018, 1, 2) + item = pystac.Item( + id=asset_id, geometry=None, bbox=None, datetime=start, properties={} + ) + + GridExtension.add_to(item) + return item + + +class GridTest(unittest.TestCase): + def setUp(self) -> None: + super().setUp() + self.item = make_item() + self.sentinel_example_uri = TestCases.get_path( + "data-files/grid/example-sentinel2.json" + ) + + def test_stac_extensions(self) -> None: + self.assertTrue(GridExtension.has_extension(self.item)) + + def test_item_repr(self) -> None: + grid_item_ext = GridExtension.ext(self.item) + self.assertEqual( + f"", grid_item_ext.__repr__() + ) + + def test_attributes(self) -> None: + GridExtension.ext(self.item).apply(code) + self.assertEqual(code, GridExtension.ext(self.item).code) + self.item.validate() + + def test_invalid_code_value(self) -> None: + with self.assertRaises(ValueError): + GridExtension.ext(self.item).apply("not_a_valid_code") + + def test_modify(self) -> None: + GridExtension.ext(self.item).apply(code) + GridExtension.ext(self.item).apply(code + "a") + self.assertEqual(code + "a", GridExtension.ext(self.item).code) + self.item.validate() + + def test_from_dict(self) -> None: + d: Dict[str, Any] = { + "type": "Feature", + "stac_version": "1.0.0", + "id": "an/asset", + "properties": { + "grid:code": code, + "datetime": "2018-01-02T00:00:00Z", + }, + "geometry": None, + "links": [], + "assets": {}, + "stac_extensions": [GridExtension.get_schema_uri()], + } + item = pystac.Item.from_dict(d) + self.assertEqual(code, GridExtension.ext(item).code) + + def test_to_from_dict(self) -> None: + GridExtension.ext(self.item).apply(code) + d = self.item.to_dict() + self.assertEqual(code, d["properties"][grid.CODE_PROP]) + + item = pystac.Item.from_dict(d) + self.assertEqual(code, GridExtension.ext(item).code) + + def test_clear_code(self) -> None: + GridExtension.ext(self.item).apply(code) + + with self.assertRaises(ValueError): + GridExtension.ext(self.item).code = None + + def test_extension_not_implemented(self) -> None: + # Should raise exception if Item does not include extension URI + item = pystac.Item.from_file(self.sentinel_example_uri) + item.stac_extensions.remove(GridExtension.get_schema_uri()) + + with self.assertRaises(pystac.ExtensionNotImplemented): + _ = GridExtension.ext(item) + + # Should raise exception if owning Item does not include extension URI + item.properties["grid:code"] = None + + with self.assertRaises(pystac.ExtensionNotImplemented): + _ = GridExtension.ext(item) + + def test_item_ext_add_to(self) -> None: + item = pystac.Item.from_file(self.sentinel_example_uri) + item.stac_extensions.remove(GridExtension.get_schema_uri()) + self.assertNotIn(GridExtension.get_schema_uri(), item.stac_extensions) + + _ = GridExtension.ext(item, add_if_missing=True) + + self.assertIn(GridExtension.get_schema_uri(), item.stac_extensions) + + def test_should_raise_exception_when_passing_invalid_extension_object( + self, + ) -> None: + self.assertRaisesRegex( + ExtensionTypeError, + r"^Grid Extension does not apply to type 'object'$", + GridExtension.ext, + object(), + ) diff --git a/tests/extensions/test_item_assets.py b/tests/extensions/test_item_assets.py index 67267d075..28e243d0c 100644 --- a/tests/extensions/test_item_assets.py +++ b/tests/extensions/test_item_assets.py @@ -46,6 +46,19 @@ def setUp(self) -> None: TestCases.get_path("data-files/item-assets/example-landsat8.json") ) + def test_create(self) -> None: + title = "Coastal Band (B1)" + description = "Coastal Band Top Of the Atmosphere" + media_type = "image/tiff; application=geotiff" + roles = ["data"] + asset_defn = AssetDefinition.create( + title=title, description=description, media_type=media_type, roles=roles + ) + self.assertEqual(asset_defn.title, title) + self.assertEqual(asset_defn.description, description) + self.assertEqual(asset_defn.media_type, media_type) + self.assertEqual(asset_defn.roles, roles) + def test_title(self) -> None: asset_defn = AssetDefinition({}) title = "Very Important Asset" diff --git a/tests/extensions/test_raster.py b/tests/extensions/test_raster.py index 677929c6b..423bba66e 100644 --- a/tests/extensions/test_raster.py +++ b/tests/extensions/test_raster.py @@ -6,6 +6,7 @@ from pystac.utils import get_opt from pystac.extensions.raster import ( Histogram, + NoDataStrings, RasterExtension, RasterBand, Sampling, @@ -41,6 +42,7 @@ def test_validate_raster(self) -> None: def test_asset_bands(self) -> None: item = pystac.Item.from_file(self.PLANET_EXAMPLE_URI) + item2 = pystac.Item.from_file(self.SENTINEL2_EXAMPLE_URI) # Get data_asset = item.assets["data"] @@ -74,8 +76,12 @@ def test_asset_bands(self) -> None: asset_bands = RasterExtension.ext(index_asset).bands self.assertIs(None, asset_bands) + b09_asset = item2.assets["B09"] + b09_bands = RasterExtension.ext(b09_asset).bands + assert b09_bands is not None + self.assertEqual(b09_bands[0].nodata, "nan") + # Set - item2 = pystac.Item.from_file(self.SENTINEL2_EXAMPLE_URI) b2_asset = item2.assets["B02"] self.assertEqual( get_opt(get_opt(RasterExtension.ext(b2_asset).bands)[0].statistics).maximum, @@ -90,6 +96,9 @@ def test_asset_bands(self) -> None: get_opt(get_opt(new_b2_asset_bands)[0].statistics).maximum, 20567 ) + new_b2_asset_band0 = get_opt(new_b2_asset_bands)[0] + new_b2_asset_band0.nodata = NoDataStrings.INF + item2.validate() # Check adding a new asset @@ -127,7 +136,7 @@ def test_asset_bands(self) -> None: histogram=new_histograms[1], ), RasterBand.create( - nodata=3, + nodata=NoDataStrings.NINF, unit="test3", statistics=new_stats[2], histogram=new_histograms[2], @@ -148,6 +157,9 @@ def test_asset_bands(self) -> None: item.assets["test"].extra_fields["raster:bands"][1]["histogram"]["min"], 3848.354901960784, ) + self.assertEqual( + item.assets["test"].extra_fields["raster:bands"][2]["nodata"], "-inf" + ) for s in new_stats: s.minimum = None @@ -190,7 +202,7 @@ def test_asset_bands(self) -> None: histogram=new_histograms[1], ) new_bands[0].apply( - nodata=3, + nodata=NoDataStrings.NAN, unit="test3", statistics=new_stats[0], histogram=new_histograms[2], @@ -202,6 +214,9 @@ def test_asset_bands(self) -> None: ], 1, ) + self.assertEqual( + item.assets["test"].extra_fields["raster:bands"][0]["nodata"], "nan" + ) def test_extension_not_implemented(self) -> None: # Should raise exception if Item does not include extension URI diff --git a/tests/extensions/test_scientific.py b/tests/extensions/test_scientific.py index 0d881d2f7..9edf3c3b3 100644 --- a/tests/extensions/test_scientific.py +++ b/tests/extensions/test_scientific.py @@ -452,7 +452,7 @@ def test_set_doi_summaries(self) -> None: sci_summaries = ScientificExtension.summaries(collection) sci_summaries.doi = [PUB2_DOI] - new_dois = ScientificExtension.summaries(self.collection).doi + new_dois = ScientificExtension.summaries(collection).doi assert new_dois is not None self.assertListEqual([PUB2_DOI], new_dois) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 1403d182e..2b46ed01e 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -327,7 +327,7 @@ def test_save_uses_previous_catalog_type(self) -> None: def test_save_to_provided_href(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: catalog = TestCases.test_case_1() - href = "http://test.com" + href = "https://stac.test" folder = os.path.join(tmp_dir, "cat") catalog.normalize_hrefs(href) catalog.save(catalog_type=CatalogType.ABSOLUTE_PUBLISHED, dest_href=folder) @@ -341,7 +341,7 @@ def test_save_to_provided_href(self) -> None: def test_save_relative_published_no_self_links(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: catalog = TestCases.test_case_1() - href = "http://test.com" + href = "https://stac.test" folder = os.path.join(tmp_dir, "cat") catalog.normalize_hrefs(href) catalog.save(catalog_type=CatalogType.RELATIVE_PUBLISHED, dest_href=folder) @@ -394,7 +394,7 @@ def test_save_with_different_stac_io(self) -> None: def test_subcatalogs_saved_to_correct_path(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: catalog = TestCases.test_case_1() - href = "http://test.com" + href = "https://stac.test" catalog.normalize_hrefs(href) catalog.save(catalog_type=CatalogType.ABSOLUTE_PUBLISHED, dest_href=tmp_dir) diff --git a/tests/test_collection.py b/tests/test_collection.py index 2a307c38f..82e78b583 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -80,6 +80,19 @@ def test_clone_uses_previous_catalog_type(self) -> None: clone = catalog.clone() self.assertEqual(clone.catalog_type, CatalogType.SELF_CONTAINED) + def test_clone_cant_mutate_original(self) -> None: + collection = TestCases.test_case_8() + assert collection.keywords is not None + self.assertListEqual(collection.keywords, ["disaster", "open"]) + clone = collection.clone() + clone.extra_fields["test"] = "extra" + self.assertNotIn("test", collection.extra_fields) + assert clone.keywords is not None + clone.keywords.append("clone") + self.assertListEqual(clone.keywords, ["disaster", "open", "clone"]) + self.assertListEqual(collection.keywords, ["disaster", "open"]) + self.assertNotEqual(id(collection.summaries), id(clone.summaries)) + def test_multiple_extents(self) -> None: cat1 = TestCases.test_case_1() country = cat1.get_child("country-1") @@ -258,6 +271,25 @@ def test_from_invalid_dict_raises_exception(self) -> None: with self.assertRaises(pystac.STACTypeError): _ = pystac.Collection.from_dict(catalog_dict) + def test_clone_preserves_assets(self) -> None: + path = TestCases.get_path("data-files/collections/with-assets.json") + original_collection = Collection.from_file(path) + assert len(original_collection.assets) > 0 + assert all( + asset.owner is original_collection + for asset in original_collection.assets.values() + ) + + cloned_collection = original_collection.clone() + + for key in original_collection.assets: + with self.subTest(f"Preserves {key} asset"): + self.assertIn(key, cloned_collection.assets) + cloned_asset = cloned_collection.assets.get(key) + if cloned_asset is not None: + with self.subTest(f"Sets owner for {key}"): + self.assertIs(cloned_asset.owner, cloned_collection) + class ExtentTest(unittest.TestCase): def setUp(self) -> None: diff --git a/tests/test_item.py b/tests/test_item.py index 904ac0be6..4e6d8d6c3 100644 --- a/tests/test_item.py +++ b/tests/test_item.py @@ -72,14 +72,28 @@ def test_set_self_href_none_ignores_relative_asset_hrefs(self) -> None: self.assertFalse(is_absolute_href(asset.href)) def test_asset_absolute_href(self) -> None: + item_path = TestCases.get_path("data-files/item/sample-item.json") item_dict = self.get_example_item_dict() item = Item.from_dict(item_dict) + item.set_self_href(item_path) rel_asset = Asset("./data.geojson") rel_asset.set_owner(item) - expected_href = os.path.abspath("./data.geojson") + expected_href = os.path.abspath( + os.path.join(os.path.dirname(item_path), "./data.geojson") + ) actual_href = rel_asset.get_absolute_href() self.assertEqual(expected_href, actual_href) + def test_asset_absolute_href_no_item_self(self) -> None: + item_dict = self.get_example_item_dict() + item = Item.from_dict(item_dict) + assert item.get_self_href() is None + + rel_asset = Asset("./data.geojson") + rel_asset.set_owner(item) + actual_href = rel_asset.get_absolute_href() + self.assertEqual(None, actual_href) + def test_extra_fields(self) -> None: item = pystac.Item.from_file( TestCases.get_path("data-files/item/sample-item.json") @@ -207,15 +221,23 @@ def test_0_9_item_with_no_extensions_does_not_read_collection_data(self) -> None ) self.assertFalse(did_merge) - def test_clone_sets_asset_owner(self) -> None: + def test_clone_preserves_assets(self) -> None: cat = TestCases.test_case_2() - item = next(iter(cat.get_all_items())) - original_asset = list(item.assets.values())[0] - assert original_asset.owner is item + original_item = next(iter(cat.get_all_items())) + assert len(original_item.assets) > 0 + assert all( + asset.owner is original_item for asset in original_item.assets.values() + ) - clone = item.clone() - clone_asset = list(clone.assets.values())[0] - self.assertIs(clone_asset.owner, clone) + cloned_item = original_item.clone() + + for key in original_item.assets: + with self.subTest(f"Preserves {key} asset"): + self.assertIn(key, cloned_item.assets) + cloned_asset = cloned_item.assets.get(key) + if cloned_asset is not None: + with self.subTest(f"Sets owner for {key}"): + self.assertIs(cloned_asset.owner, cloned_item) def test_make_asset_href_relative_is_noop_on_relative_hrefs(self) -> None: cat = TestCases.test_case_2() @@ -266,6 +288,44 @@ def test_clone(self) -> None: self.assertIsInstance(cloned_item, self.BasicCustomItem) +class AssetTest(unittest.TestCase): + def setUp(self) -> None: + self.maxDiff = None + with open(TestCases.get_path("data-files/item/sample-item.json")) as src: + item_dict = json.load(src) + + self.asset_dict = item_dict["assets"]["analytic"] + + def example_asset(self) -> Asset: + return Asset.from_dict(self.asset_dict) + + def test_clone(self) -> None: + original_asset = self.example_asset() + cloned_asset = original_asset.clone() + + self.assertDictEqual(original_asset.to_dict(), self.asset_dict) + self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict) + + # Changes to original asset should not affect cloned Asset + original_asset.description = "Some new description" + self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict) + + original_asset.href = "/path/to/new/href" + self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict) + + original_asset.title = "New Title" + self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict) + + original_asset.roles = ["new role"] + self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict) + + original_asset.roles.append("new role") + self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict) + + original_asset.extra_fields["new_field"] = "new_value" + self.assertDictEqual(cloned_asset.to_dict(), self.asset_dict) + + class AssetSubClassTest(unittest.TestCase): class CustomAsset(Asset): pass diff --git a/tests/test_summaries.py b/tests/test_summaries.py index c542da203..adb06daad 100644 --- a/tests/test_summaries.py +++ b/tests/test_summaries.py @@ -59,6 +59,17 @@ def test_summary_not_empty(self) -> None: summaries = Summarizer().summarize(coll.get_all_items()) self.assertFalse(summaries.is_empty()) + def test_clone_summary(self) -> None: + coll = TestCases.test_case_5() + summaries = Summarizer().summarize(coll.get_all_items()) + summaries_dict = summaries.to_dict() + self.assertEqual(len(summaries_dict["eo:bands"]), 4) + self.assertEqual(len(summaries_dict["proj:epsg"]), 1) + clone = summaries.clone() + self.assertTrue(isinstance(clone, Summaries)) + clone_dict = clone.to_dict() + self.assertDictEqual(clone_dict, summaries_dict) + class RangeSummaryTest(unittest.TestCase): def setUp(self) -> None: diff --git a/tests/test_utils.py b/tests/test_utils.py index 453dc6fcd..ada187a21 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,13 +1,21 @@ +from typing import Optional import unittest import os import json import ntpath import sys +import time from datetime import datetime, timezone, timedelta +from dateutil import tz from pystac import utils -from pystac.utils import make_relative_href, make_absolute_href, is_absolute_href +from pystac.utils import ( + make_relative_href, + make_absolute_href, + is_absolute_href, + str_to_datetime, +) from tests.utils import TestCases @@ -273,6 +281,42 @@ def test_datetime_to_str(self) -> None: got = utils.datetime_to_str(dt) self.assertEqual(expected, got) + def test_str_to_datetime(self) -> None: + def _set_tzinfo(tz_str: Optional[str]) -> None: + if tz_str is None: + if "TZ" in os.environ: + del os.environ["TZ"] + else: + os.environ["TZ"] = tz_str + # time.tzset() only available for Unix/Linux + if hasattr(time, "tzset"): + time.tzset() + + utc_timestamp = "2015-06-27T10:25:31Z" + + prev_tz = os.environ.get("TZ") + + with self.subTest(tz=None): + _set_tzinfo(None) + utc_datetime = str_to_datetime(utc_timestamp) + self.assertIs(utc_datetime.tzinfo, tz.tzutc()) + self.assertIsNot(utc_datetime.tzinfo, tz.tzlocal()) + + with self.subTest(tz="UTC"): + _set_tzinfo("UTC") + utc_datetime = str_to_datetime(utc_timestamp) + self.assertIs(utc_datetime.tzinfo, tz.tzutc()) + self.assertIsNot(utc_datetime.tzinfo, tz.tzlocal()) + + with self.subTest(tz="US/Central"): + _set_tzinfo("US/Central") + utc_datetime = str_to_datetime(utc_timestamp) + self.assertIs(utc_datetime.tzinfo, tz.tzutc()) + self.assertIsNot(utc_datetime.tzinfo, tz.tzlocal()) + + if prev_tz is not None: + _set_tzinfo(prev_tz) + def test_geojson_bbox(self) -> None: # Use sample Geojson from https://en.wikipedia.org/wiki/GeoJSON with open(