pax_global_header00006660000000000000000000000064145157607470014532gustar00rootroot0000000000000052 comment=c262e0a9745ca893dd47bd24777fc4b6ceb2dbb3 pystac-1.9.0/000077500000000000000000000000001451576074700130445ustar00rootroot00000000000000pystac-1.9.0/.codespellignore000066400000000000000000000000101451576074700162120ustar00rootroot00000000000000filetestpystac-1.9.0/.coveragerc000066400000000000000000000002241451576074700151630ustar00rootroot00000000000000[report] fail_under = 90 exclude_lines = if TYPE_CHECKING: [run] branch = true source = pystac omit = pystac/extensions/label.py pystac-1.9.0/.github/000077500000000000000000000000001451576074700144045ustar00rootroot00000000000000pystac-1.9.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001451576074700165675ustar00rootroot00000000000000pystac-1.9.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000013071451576074700212620ustar00rootroot00000000000000**Describe the bug** A clear and concise description of what the bug is. **To reproduce** Steps to reproduce the behavior: > Ex. > > 1. Install stactools > 2. Run `scripts/test` > 3. See error Include OS, Python version, and PySTAC version. **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots and shell session dumps** If applicable, add session dumps and/or screenshots to help explain your problem. > ex. `scripts/lint >> lint_errors.txt` **Additional context** Add any other context about the problem here. **Issue Checklist** - [ ] OS, Python version, PySTAC version are included. - [ ] Existing issues were reviewed to prevent duplicate submission. pystac-1.9.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000012171451576074700223150ustar00rootroot00000000000000**Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. Ex. I would like to use PySTAC to do [...] **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **PySTAC version** Include your PySTAC version in case a similar feature already exists in a newer or pre-release version. **Additional context** Add any other context or screenshots about the feature request here. pystac-1.9.0/.github/dependabot.yml000066400000000000000000000003551451576074700172370ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: daily commit-message: prefix: build(deps) - package-ecosystem: pip directory: / schedule: interval: daily pystac-1.9.0/.github/pull_request_template.md000066400000000000000000000010041451576074700213400ustar00rootroot00000000000000**Related Issue(s):** - # **Description:** **PR Checklist:** - [ ] `pre-commit` hooks pass locally - [ ] Tests pass (run `scripts/test`) - [ ] Documentation has been updated to reflect changes, if applicable - [ ] This PR maintains or improves overall codebase code coverage. - [ ] Changes are added to the [CHANGELOG](https://github.com/stac-utils/pystac/blob/main/CHANGELOG.md). See [the docs](https://pystac.readthedocs.io/en/latest/contributing.html#changelog) for information about adding to the changelog. pystac-1.9.0/.github/workflows/000077500000000000000000000000001451576074700164415ustar00rootroot00000000000000pystac-1.9.0/.github/workflows/continuous-integration.yml000066400000000000000000000111101451576074700237050ustar00rootroot00000000000000name: CI on: push: branches: - main - "0.3" - "0.4" - "0.5" - "1.0" - "2.0" pull_request: merge_group: concurrency: # Cancel running job if another commit is pushed to the branch group: ${{ github.ref }} cancel-in-progress: true jobs: test: name: test runs-on: ${{ matrix.os }} strategy: # Allow other matrix jobs to complete if 1 fails fail-fast: false matrix: python-version: - "3.9" - "3.10" - "3.11" os: - ubuntu-latest - windows-latest - macos-latest steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} cache: "pip" - name: Install dependencies run: pip install .[validation,test] - name: Execute test suite run: ./scripts/test shell: bash env: TMPDIR: "${{ matrix.os == 'windows-latest' && 'D:\\a\\_temp' || '' }}" coverage: name: coverage runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python 3.9 uses: actions/setup-python@v4 with: python-version: "3.9" cache: "pip" - name: Install with dependencies run: pip install .[validation,test] - name: Run coverage with orjson run: pytest tests --cov - name: Uninstall orjson run: pip uninstall -y orjson - name: Run coverage without orjson, appending results run: pytest tests --cov --cov-append - name: Prepare ./coverage.xml # Ignore the configured fail-under to ensure we upload the coverage report. We # will trigger a failure for coverage drops in a later job run: coverage xml --fail-under 0 - name: Upload All coverage to Codecov uses: codecov/codecov-action@v3 if: ${{ env.GITHUB_REPOSITORY }} == 'stac-utils/pystac' with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.xml fail_ci_if_error: false - name: Check for coverage drop # This will use the configured fail-under, causing this job to fail if the # coverage drops. run: coverage report lint: runs-on: ubuntu-latest strategy: # Allow other matrix jobs to complete if 1 fails fail-fast: false matrix: python-version: - "3.9" - "3.10" - "3.11" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} cache: "pip" - name: Install with test dependencies run: pip install .[test] - name: Execute linters & type checkers run: pre-commit run --all-files without-orjson: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: "3.9" - name: Install run: pip install .[validation,test] - name: Uninstall orjson run: pip uninstall -y orjson - name: Run tests run: pytest tests check-all-dependencies: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: "3.9" cache: "pip" - name: Install all dependencies run: pip install .[bench,docs,orjson,test,urllib3,validation] check-benchmarks: # This checks to make sure any API changes haven't broken any of the # benchmarks. It doesn't do any actual benchmarking, since (IMO) that's not # appropriate for CI on Github actions. runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: "3.9" cache: "pip" - name: Install pystac run: pip install .[bench] - name: Set asv machine run: asv machine --yes - name: Check benchmarks run: asv run -a repeat=1 -a rounds=1 HEAD docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: "3.9" cache: "pip" - name: Install pandoc run: sudo apt-get install pandoc - name: Install pystac run: pip install .[docs] - name: Check docs run: make -C docs html SPHINXOPTS="-W --keep-going" pystac-1.9.0/.github/workflows/release.yml000066400000000000000000000013361451576074700206070ustar00rootroot00000000000000name: Release on: release: types: - published jobs: release: name: release runs-on: ubuntu-latest if: ${{ github.repository }} == 'stac-utils/pystac' steps: - uses: actions/checkout@v4 - name: Set up Python 3.x uses: actions/setup-python@v4 with: python-version: "3.x" - name: Install release dependencies run: | python -m pip install --upgrade pip pip install build twine - name: Build and publish package env: TWINE_USERNAME: ${{ secrets.PYPI_STACUTILS_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_STACUTILS_PASSWORD }} run: | python -m build twine upload dist/* pystac-1.9.0/.gitignore000066400000000000000000000044321451576074700150370ustar00rootroot00000000000000*.pyc *.egg-info *.eggs .DS_Store data config.json stdout* /integration* .idea .vscode # Sphinx documentation .ipynb_checkpoints/ docs/tutorials/pystac-example* docs/tutorials/spacenet-stac/ docs/tutorials/spacenet-cog-stac/ docs/tutorials/data/ docs/quickstart_stac/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. # Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # asv environments .asvpystac-1.9.0/.pre-commit-config.yaml000066400000000000000000000025621451576074700173320ustar00rootroot00000000000000# Configuration file for pre-commit (https://pre-commit.com/). # Please run `pre-commit run --all-files` when adding or changing entries. repos: - repo: https://github.com/asottile/pyupgrade rev: v3.15.0 hooks: - id: pyupgrade args: - "--py39-plus" - repo: local hooks: - id: ruff name: ruff entry: ruff language: system stages: [commit] types: [python] args: [--fix, --exit-non-zero-on-fix] - id: black name: black entry: black language: system stages: [commit] types: [python] - id: black-jupyter name: black-jupyter entry: black language: python require_serial: true types_or: [python, pyi, jupyter] additional_dependencies: ["black[jupyter]"] - id: codespell name: codespell entry: codespell args: [--ignore-words=.codespellignore] language: system stages: [commit] types_or: [jupyter, markdown, python, shell] - id: doc8 name: doc8 entry: doc8 language: system files: \.rst$ require_serial: true - id: mypy name: mypy entry: mypy args: [--no-incremental] language: system stages: [commit] types: [python] require_serial: true pystac-1.9.0/.readthedocs.yaml000066400000000000000000000006711451576074700162770ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 build: os: ubuntu-22.04 tools: python: "3.9" formats: # Temporarily disabling PDF downloads due to problem with nbsphinx in LateX builds # - pdf - htmlzip python: install: - method: pip path: . extra_requirements: - docs sphinx: fail_on_warning: false pystac-1.9.0/CHANGELOG.md000066400000000000000000001457301451576074700146670ustar00rootroot00000000000000# Changelog ## [Unreleased] ## [v1.9.0] - 2023-10-23 ### Added - Simpler extension interface ([#1243](https://github.com/stac-utils/pystac/pull/1243)]) - More permissive schema_uri matching to allow future versions of extension schemas ([#1231](https://github.com/stac-utils/pystac/pull/1231)) - Better error messages from jsonschema validation ([#1233](https://github.com/stac-utils/pystac/pull/1233)) - `validate_all_dict` replaces the previous implementation of `validate_all` (i.e., `validate_all` was renamed to `validate_all_dict`, and `validate_all` was changed as described below) ([#1246](https://github.com/stac-utils/pystac/pull/1246)) ### Changed - `validate_all` now accepts a `STACObject` (in addition to accepting a dict, which is now deprecated), but prohibits supplying a value for `href`, which must be supplied _only_ when supplying an object as a dict. Once `validate_all` removes support for an object as a dict, the `href` parameter will also be removed. ([#1246](https://github.com/stac-utils/pystac/pull/1246)) - Report `href` when schema url resolution fails ([#1263](https://github.com/stac-utils/pystac/pull/1263)) - Version extension updated to v1.2.0 ([#1262](https://github.com/stac-utils/pystac/pull/1262)) - Datacube extension updated to v2.2.0 ([#1269](https://github.com/stac-utils/pystac/pull/1269)) ### Fixed - Typing of `href` arguments ([#1234](https://github.com/stac-utils/pystac/pull/1234)) - Interactions between **pytest-recording** and the validator schema cache ([#1242](https://github.com/stac-utils/pystac/pull/1242)) - Call `registry` when instantiating `Draft7Validator` ([#1240](https://github.com/stac-utils/pystac/pull/1240)) - Migration for the classification, datacube, table, and timestamps extensions ([#1258](https://github.com/stac-utils/pystac/pull/1258)) - Handling of `bboxes` and `intervals` arguments to `SpatialExtent` and `TemporalExtent`, respectively ([#1268](https://github.com/stac-utils/pystac/pull/1268)) ### Removed - Python 3.8 support ([#1236](https://github.com/stac-utils/pystac/pull/1236)) ### Deprecated - `ExtensionManagementMixin.validate_has_extension` is replaced with `ExtensionManagementMixin.ensure_has_extension`. Calling `ExtensionManagementMixin.validate_has_extension` will raise a `DeprecationWarning` and call `ExtensionManagementMixin.ensure_has_extension` ([#1248](https://github.com/stac-utils/pystac/pull/1248)) - `validate_all` for dicts; use `validate_all_dict` instead ([#1246](https://github.com/stac-utils/pystac/pull/1246)) - `Label` extension ([#1270](https://github.com/stac-utils/pystac/pull/1270)) ## [v1.8.4] - 2023-09-22 ### Added - Permissive deserialization of Collection temporal extents ([#1222](https://github.com/stac-utils/pystac/pull/1222)) ### Fixed - Update usage of jsonschema ([#1215](https://github.com/stac-utils/pystac/pull/1215)) ### Deprecated - `pystac.validation.local_validator.LocalValidator` ([#1215](https://github.com/stac-utils/pystac/pull/1215)) ## [v1.8.3] - 2023-07-12 ### Added - Allow to pass a Dict with field names and summary strategies to the `fields` parameter in the `Summarizer` constructor ([#1195](https://github.com/stac-utils/pystac/pull/1195)) ### Changed - Pin jsonschema version to <4.18 until regresssions are fixed ### Fixed - Fix the documentation rendering of the `fields` parameter in the `Summarizer` constructor ([#1195](https://github.com/stac-utils/pystac/pull/1195)) ## [v1.8.2] - 2023-07-12 ### Fixed - Explicitly re-export HREF from `link` ([#1182](https://github.com/stac-utils/pystac/pull/1182)) - Include `fields-normalized.json` in build ([#1188](https://github.com/stac-utils/pystac/pull/1188)) ## [v1.8.1] - 2023-06-30 ### Fixed - Include jsonschemas in package ([#1181](https://github.com/stac-utils/pystac/pull/1181)) ## [v1.8.0] - 2023-06-27 ### Added - `sort_links_by_id` to Catalog `get_child()` and `modify_links` to `get_stac_objects()` ([#1064](https://github.com/stac-utils/pystac/pull/1064)) - `*ids` to Catalog and Collection `get_items()` for only including the provided ids in the iterator ([#1075](https://github.com/stac-utils/pystac/pull/1075)) - `recursive` to Catalog and Collection `get_items()` to walk the sub-catalogs and sub-collections ([#1075](https://github.com/stac-utils/pystac/pull/1075)) - MGRS Extension ([#1088](https://github.com/stac-utils/pystac/pull/1088)) - All HTTP requests are logged when level is set to `logging.DEBUG` ([#1096](https://github.com/stac-utils/pystac/pull/1096)) - `set_parent` to Catalog `add_item` and `add_child` to avoid overriding existing parents ([#1117](https://github.com/stac-utils/pystac/pull/1117), [#1155](https://github.com/stac-utils/pystac/pull/1155)) - `owner` attribute to `AssetDefinition` in the item-assets extension ([#1110](https://github.com/stac-utils/pystac/pull/1110)) - Windows `\\` path delimiters are converted to POSIX style `/` delimiters ([#1125](https://github.com/stac-utils/pystac/pull/1125)) - Updated raster extension to work with the item_assets extension's AssetDefinition objects ([#1110](https://github.com/stac-utils/pystac/pull/1110)) - Classification extension ([#1093](https://github.com/stac-utils/pystac/pull/1093)), with support for adding classification information to item_assets' `AssetDefinition`s and raster's `RasterBand` objects. - `get_derived_from`, `add_derived_from` and `remove_derived_from` to Items ([#1136](https://github.com/stac-utils/pystac/pull/1136)) - `ItemEOExtension.get_assets` for getting assets filtered on band `name` or `common_name` ([#1140](https://github.com/stac-utils/pystac/pull/1140)) - `max_items` and `recursive` to `Catalog.validate_all` ([#1141](https://github.com/stac-utils/pystac/pull/1141)) - `KML` as a built in media type ([#1127](https://github.com/stac-utils/pystac/issues/1127)) - `move/copy/delete` operations for local Assets ([#1158](https://github.com/stac-utils/pystac/issues/1158)) - Latest core STAC spec jsonshemas are included in pytstac and used for validation ([#1165](https://github.com/stac-utils/pystac/pull/1165)) - Xarray Assets Extension class ([#1161](https://github.com/stac-utils/pystac/pull/1161)) ### Changed - Include a copy of the `fields.json` file (for summaries) with each distribution of PySTAC ([#1045](https://github.com/stac-utils/pystac/pull/1045)) - Make Catalog, Collection `.get_assets()` return a deepcopy ([#1087](https://github.com/stac-utils/pystac/pull/1087)) - Removed documentation references to `to_dict` methods returning JSON ([#1074](https://github.com/stac-utils/pystac/pull/1074)) - Expand support for previous extension schema URIs ([#1091](https://github.com/stac-utils/pystac/pull/1091)) - Use `pyproject.toml` instead of `setup.py` ([#1100](https://github.com/stac-utils/pystac/pull/1100)) - `DefaultStacIO` now raises an error if it tries to write to a non-local url ([#1107](https://github.com/stac-utils/pystac/pull/1107)) - Allow instantiation of pystac objects even with `"stac_extensions": null` ([#1109](https://github.com/stac-utils/pystac/pull/1109)) - Make `Link.to_dict()` only contain strings ([#1114](https://github.com/stac-utils/pystac/pull/1114)) - Updated raster extension to work with the item_assets extension's AssetDefinition objects ([#1110](https://github.com/stac-utils/pystac/pull/1110)) - Return all validation errors from validation methods of `JsonSchemaSTACValidator` ([#1120](https://github.com/stac-utils/pystac/pull/1120)) - EO extension updated to v1.1.0 ([#1131](https://github.com/stac-utils/pystac/pull/1131)) - Use `id` in STACTypeError instead of entire dict ([#1126](https://github.com/stac-utils/pystac/pull/1126)) - Make sure that `get_items` is backwards compatible ([#1139](https://github.com/stac-utils/pystac/pull/1139)) - Make `_repr_html_` look like `_repr_json_` output ([#1142](https://github.com/stac-utils/pystac/pull/1142)) - Improved error message when `.ext` is called on a Collection ([#1157](https://github.com/stac-utils/pystac/pull/1157)) - `add_child` and `add_item` return a Link object instead of None ([#1160](https://github.com/stac-utils/pystac/pull/1160)) - `add_children` and `add_items` return a list of Link objects instead of None ([#1160](https://github.com/stac-utils/pystac/pull/1160)) - Include collection assets in `make_all_asset_hrefs_relative/absolute` ([#1168](https://github.com/stac-utils/pystac/pull/1168)) - Use cassettes for all tests that pull files from remote ([#1162](https://github.com/stac-utils/pystac/pull/1162)) - Landsat tutorial notebook updated to collection 2 sources ([#1152](https://github.com/stac-utils/pystac/pull/1152)) ### Fixed - Include the item's root when resolving its collection link ([#1171](https://github.com/stac-utils/pystac/pull/1171)) ### Deprecated - `pystac.summaries.FIELDS_JSON_URL` ([#1045](https://github.com/stac-utils/pystac/pull/1045)) - Catalog `get_item()`. Use `get_items(id)` instead ([#1075](https://github.com/stac-utils/pystac/pull/1075)) - Catalog and Collection `get_all_items`. Use `get_items(recursive=True)` instead ([#1075](https://github.com/stac-utils/pystac/pull/1075)) ## [v1.7.3] ### Fixed - Duplicate `self` links in Items ([#1103](https://github.com/stac-utils/pystac/pull/1103)) ## [v1.7.2] ### Fixed - Projection extension v1.0.0 support ([#1081](https://github.com/stac-utils/pystac/pull/1081)) ## [v1.7.1] ### Changed - Use [ruff](https://github.com/charliermarsh/ruff) instead of **isort** and **flake8** ([#1034](https://github.com/stac-utils/pystac/pull/1034)) - Update links in doc notebooks to not point to specific versions ([#1039](https://github.com/stac-utils/pystac/pull/1039)) ### Fixed - Item `__geo_interface__` now correctly returns a Feature, rather than only the Geometry ([#1049](https://github.com/stac-utils/pystac/pull/1049)) ## [v1.7.0] ### Added - Additional util methods `now_in_utc` and `now_to_rfc3339_str` ([#760](https://github.com/stac-utils/pystac/pull/760)) - `media_type` and `role` filtering to Item and Collection `get_assets()` method ([#936](https://github.com/stac-utils/pystac/pull/936)) - `Asset.has_role` ([#936](https://github.com/stac-utils/pystac/pull/936)) - Enum MediaType entry for flatgeobuf ([discussion](https://github.com/flatgeobuf/flatgeobuf/discussions/112#discussioncomment-4606721)) ([#938](https://github.com/stac-utils/pystac/pull/938)) - Custom `header` support to `DefaultStacIO` ([#889](https://github.com/stac-utils/pystac/pull/889)) - Python 3.11 checks in CI ([#908](https://github.com/stac-utils/pystac/pull/908)) - Ability to only update resolved links when using `Catalog.normalize_hrefs` and `Catalog.normalize_and_save`, via a new `skip_unresolved` argument ([#900](https://github.com/stac-utils/pystac/pull/900)) - Optional argument `timespec` to `utils.datetime_to_str` ([#929](https://github.com/stac-utils/pystac/pull/929)) - `isort` ([#961](https://github.com/stac-utils/pystac/pull/961)) - `AsIsLayoutStrategy` ([#919](https://github.com/stac-utils/pystac/pull/919)) - `__geo_interface__` for items ([#885](https://github.com/stac-utils/pystac/pull/885)) - Optional `strategy` parameter to `catalog.add_items()` ([#967](https://github.com/stac-utils/pystac/pull/967)) - `start_datetime` and `end_datetime` arguments to the `Item` constructor ([#918](https://github.com/stac-utils/pystac/pull/918)) - `RetryStacIO` ([#986](https://github.com/stac-utils/pystac/pull/986)) - `STACObject.remove_hierarchical_links` and `Link.is_hierarchical` ([#999](https://github.com/stac-utils/pystac/pull/999)) - `extra_fields` to `AssetDefinition` in the item assets extension ([#1003](https://github.com/stac-utils/pystac/pull/1003)) - `Catalog.fully_resolve` ([#1001](https://github.com/stac-utils/pystac/pull/1001)) - A `DeprecatedWarning` when deserializing an Item or Collection to a STAC object via the `from_dict()` method ([1006](https://github.com/stac-utils/pystac/pull/1006)) - Support for relative stac extension paths via `make_absolute_href` ([#884](https://github.com/stac-utils/pystac/pull/884)) ### Changed - Projection extension updated to use v1.1.0 ([#989](https://github.com/stac-utils/pystac/pull/989)). - Update Grid Extension support to v1.1.0 and fix issue with grid:code prefix validation ([#925](https://github.com/stac-utils/pystac/pull/925)) - Switch to pytest ([#939](https://github.com/stac-utils/pystac/pull/939)) - Use `from __future__ import annotations` for type signatures ([#962](https://github.com/stac-utils/pystac/pull/962)) - Use `TypeVar` for alternate constructors ([#983](https://github.com/stac-utils/pystac/pull/983)) - Behavior when required fields are missing in `Item.from_dict` ([#994](https://github.com/stac-utils/pystac/pull/994)) - By default, `ItemCollection` now clones items in iterator (`clone_items=True`) ([#1016](https://github.com/stac-utils/pystac/pull/1016)) ### Deprecated - `TemplateError` in `layout.py` deprecated in favor of duplicate in `errors.py` ([#1018](https://github.com/stac-utils/pystac/pull/1018)) ### Fixed - Creating dictionaries from Catalogs and Collections without root hrefs now creates valid STAC ([#896](https://github.com/stac-utils/pystac/pull/896)) - Dependency resolution when installing `requirements-dev.txt` ([#897](https://github.com/stac-utils/pystac/pull/897)) - Serializing optional Collection attributes ([#916](https://github.com/stac-utils/pystac/pull/916)) - A couple non-running tests ([#912](https://github.com/stac-utils/pystac/pull/912)) - Filtering on `media_type` in `get_links()` and `get_single_link()` ([#966](https://github.com/stac-utils/pystac/pull/966)) - Missing hrefs and duplicate Item fields in html generated by `_repr_html_()` ([#975](https://github.com/stac-utils/pystac/pull/975)) - Allow subclasses in a few more `clone` methods ([#983](https://github.com/stac-utils/pystac/pull/983)) - Pass `href` from `Item.from_dict` to constructor ([#984](https://github.com/stac-utils/pystac/pull/984)) - Serializing the table extension ([#992](https://github.com/stac-utils/pystac/pull/992)) ## [v1.6.1] ### Fixed - Pins `jsonschema` to >=4.0.1 to avoid a `RefResolutionError` when validating some extensions ([#857](https://github.com/stac-utils/pystac/pull/857)) - Fixed bug in custom StacIO example in documentation ([#879](https://github.com/stac-utils/pystac/pull/879)) ## [v1.6.0] ### Removed - Support for Python 3.7 ([#853](https://github.com/stac-utils/pystac/pull/853)) ## [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 - Experimental support for Python 3.11 ([#731](https://github.com/stac-utils/pystac/pull/731)) - Accept PathLike objects in `StacIO` I/O methods, `pystac.read_file` and `pystac.write_file` ([#728](https://github.com/stac-utils/pystac/pull/728)) - Support for Storage Extension ([#745](https://github.com/stac-utils/pystac/pull/745)) - Optional `StacIO` instance as argument to `Catalog.save`/`Catalog.normalize_and_save` ([#751](https://github.com/stac-utils/pystac/pull/751)) - More thorough docstrings for `pystac.utils` functions and classes ([#735](https://github.com/stac-utils/pystac/pull/735)) ### Changed - Label Extension version updated to `v1.0.1` ([#726](https://github.com/stac-utils/pystac/pull/726)) - Option to filter by `media_type` in `STACObject.get_links` and `STACObject.get_single_link` ([#704](https://github.com/stac-utils/pystac/pull/704)) ### Fixed - Self links no longer included in Items for "relative published" catalogs ([#725](https://github.com/stac-utils/pystac/pull/725)) - Adding New and Custom Extensions tutorial now up-to-date with new extensions API ([#724](https://github.com/stac-utils/pystac/pull/724)) - Clarify error message when using `PropertyExtension.ext(..., add_if_missing=True)` on an `Asset` with no owner ([#746](https://github.com/stac-utils/pystac/pull/746)) - Type errors when initializing `TemporalExtent` using a list of `datetime` objects ([#744](https://github.com/stac-utils/pystac/pull/744)) ## [v1.3.0] ### Added - Type annotations for instance attributes on all classes ([#705](https://github.com/stac-utils/pystac/pull/705)) - `extensions.datacube.Variable.to_dict` method ([#699](https://github.com/stac-utils/pystac/pull/699)]) - Clarification of possible errors when using `.ext` to extend an object ([#701](https://github.com/stac-utils/pystac/pull/701)) - Downloadable documentation as zipped HTML ([#715](https://github.com/stac-utils/pystac/pull/715)) ### Removed - Downloadable documentation in ePub format ([#715](https://github.com/stac-utils/pystac/pull/715)) ### Changed - Reorganize docs and switch to PyData theme ([#687](https://github.com/stac-utils/pystac/pull/687)) ### Fixed - Quickstart tutorial is now up-to-date with all package changes ([#674](https://github.com/stac-utils/pystac/pull/674)) - Creating absolute URLs from absolute URLs ([#697](https://github.com/stac-utils/pystac/pull/697)]) - Serialization error when using `pystac.extensions.file.MappingObject` ([#700](https://github.com/stac-utils/pystac/pull/700)) - Use `PropertiesExtension._get_property` to properly set return type in `TableExtension` ([#712](https://github.com/stac-utils/pystac/pull/712)) - `DatacubeExtension.variables` now has a setter ([#699](https://github.com/stac-utils/pystac/pull/699)]) - Landsat STAC tutorial is now up-to-date with all package changes ([#692](https://github.com/stac-utils/pystac/pull/674)) - Paths to sub-catalog files when using `Catalog.save` ([#714](https://github.com/stac-utils/pystac/pull/714)) - Link to PySTAC Introduction tutorial in tutorials index page ([#719](https://github.com/stac-utils/pystac/pull/719)) ## [v1.2.0] ### Added - Added Table-extension ([#646](https://github.com/stac-utils/pystac/pull/646)) - Stable support for Python 3.10 ([#656](https://github.com/stac-utils/pystac/pull/656)) - `.python-version` files are now ignored by Git ([#647](https://github.com/stac-utils/pystac/pull/647)) - Added a flag to allow users to skip transforming hierarchical link HREFs based on root catalog type ([#663](https://github.com/stac-utils/pystac/pull/663)) ### Removed - Exclude `tests` from package distribution. This should make the package lighter ([#604](https://github.com/stac-utils/pystac/pull/604)) ### Changed - Enable [strict mode](https://mypy.readthedocs.io/en/latest/command_line.html?highlight=strict%20mode#cmdoption-mypy-strict) for `mypy` ([#591](https://github.com/stac-utils/pystac/pull/591)) - Links will get their `title` from their target if no `title` is provided ([#607](https://github.com/stac-utils/pystac/pull/607)) - Relax typing on `LabelClasses` from `List` to `Sequence` ([#627](https://github.com/stac-utils/pystac/pull/627)) - Upgraded datacube-extension to version 2.0.0 ([#645](https://github.com/stac-utils/pystac/pull/645)) - By default, ItemCollections will not modify Item HREFs based on root catalog type to avoid performance costs of root link reads ([#663](https://github.com/stac-utils/pystac/pull/663)) ### Fixed - `generate_subcatalogs` can include multiple template values in a single subfolder layer ([#595](https://github.com/stac-utils/pystac/pull/595)) - Avoid implicit re-exports ([#591](https://github.com/stac-utils/pystac/pull/591)) - Fix issue that caused incorrect root links when constructing multi-leveled catalogs ([#658](https://github.com/stac-utils/pystac/pull/658)) - Regression where string `Enum` values were not serialized properly in methods like `Link.to_dict` ([#654](https://github.com/stac-utils/pystac/pull/654)) ## [v1.1.0] ### Added - Include type information during packaging for use with e.g. `mypy` ([#579](https://github.com/stac-utils/pystac/pull/579)) - Optional `dest_href` argument to `Catalog.save` to allow saving `Catalog` instances to locations other than their `self` href ([#565](https://github.com/stac-utils/pystac/pull/565)) ### Changed - Pin the rustc version in Continuous Integration to work around ([#581](https://github.com/stac-utils/pystac/pull/581)) ## [v1.0.1] ### Changed - HREFs in `Link` objects with `rel == "self"` are converted to absolute HREFs ([#574](https://github.com/stac-utils/pystac/pull/574)) ## [v1.0.0] ### Added - `ProjectionExtension.crs_string` to provide a single string to describe the coordinate reference system (CRS). Useful because projections can be defined by EPSG code, WKT, or projjson. ([#548](https://github.com/stac-utils/pystac/pull/548)) - SAR Extension summaries([#556](https://github.com/stac-utils/pystac/pull/556)) - Migration for `sar:type` -> `sar:product_type` and `sar:polarization` -> `sar:polarizations` for pre-0.9 catalogs ([#556](https://github.com/stac-utils/pystac/pull/556)) - Migration from `eo:epsg` -> `proj:epsg` for pre-0.9 catalogs ([#557](https://github.com/stac-utils/pystac/pull/557)) - Collection summaries for Point Cloud Extension ([#558](https://github.com/stac-utils/pystac/pull/558)) - `PhenomenologyType` enum for recommended values of `pc:type` & `SchemaType` enum for valid values of `type` in [Point Cloud Schema Objects](https://github.com/stac-extensions/pointcloud#schema-object) ([#548](https://github.com/stac-utils/pystac/pull/548)) - `to_dict` and equality definition for `extensions.item_asset.AssetDefinition` ([#564](https://github.com/stac-utils/pystac/pull/564)) - `Asset.common_metadata` property ([#563](https://github.com/stac-utils/pystac/pull/563)) ### Changed - The `from_dict` method on STACObjects will set the object's root link when a `root` parameter is present. An ItemCollection `from_dict` with a root parameter will set the root on each of it's Items. ([#549](https://github.com/stac-utils/pystac/pull/549)) - Calling `ExtensionManagementMixin.validate_has_extension` with `add_if_missing = True` on an ownerless `Asset` will raise a `STACError` ([#554](https://github.com/stac-utils/pystac/pull/554)) - `PointcloudSchema` -> `Schema`, `PointcloudStatistic` -> `Statistic` for consistency with naming convention in other extensions ([#548](https://github.com/stac-utils/pystac/pull/548)) - `RequiredPropertyMissing` always raised when trying to get a required property that is `None` (`STACError` or `KeyError` was previously being raised in some cases) ([#561](https://github.com/stac-utils/pystac/pull/561)) ### Fixed - Added `Collections` as a type that can be extended for extensions whose fields can appear in collection summaries ([#547](https://github.com/stac-utils/pystac/pull/547)) - Allow resolved self links when getting an object's self href ([#555](https://github.com/stac-utils/pystac/pull/555)) - Fixed type annotation on SummariesLabelExtension.label_properties setter ([#562](https://github.com/stac-utils/pystac/pull/562)) - Allow comparable types with alternate parameter naming of **lt** method to pass structural type linting for RangeSummary ([#562](https://github.com/stac-utils/pystac/pull/562)) ## [v1.0.0-rc.3] ### Added - (Experimental) support for Python 3.10 ([#473](https://github.com/stac-utils/pystac/pull/473)) - `LabelTask` enum in `pystac.extensions.label` with recommended values for `"label:tasks"` field ([#484](https://github.com/stac-utils/pystac/pull/484)) - `LabelMethod` enum in `pystac.extensions.label` with recommended values for `"label:methods"` field ([#484](https://github.com/stac-utils/pystac/pull/484)) - Label Extension summaries ([#484](https://github.com/stac-utils/pystac/pull/484)) - Timestamps Extension summaries ([#513](https://github.com/stac-utils/pystac/pull/513)) - Define equality and `__repr__` of `RangeSummary` instances based on `to_dict` representation ([#513](https://github.com/stac-utils/pystac/pull/513)) - Sat Extension summaries ([#509](https://github.com/stac-utils/pystac/pull/509)) - `Catalog.get_collections` for getting all child `Collections` for a catalog, and `Catalog.get_all_collections` for recursively getting all child `Collections` for a catalog and its children ([#511](https://github.com/stac-utils/pystac/pull/)) ### Changed - Renamed `Asset.properties` -> `Asset.extra_fields` and `Link.properties` -> `Link.extra_fields` for consistency with other STAC objects ([#510](https://github.com/stac-utils/pystac/pull/510)) ### Fixed - Bug in `pystac.serialization.identify_stac_object_type` where invalid objects with `stac_version == 1.0.0` were incorrectly identified as Catalogs ([#487](https://github.com/stac-utils/pystac/pull/487)) - `Link` constructor classes (e.g. `Link.from_dict`, `Link.canonical`, etc.) now return the calling class instead of always returning the `Link` class ([#512](https://github.com/stac-utils/pystac/pull/512)) - Sat extension now includes all fields defined in v1.0.0 ([#509](https://github.com/stac-utils/pystac/pull/509)) ### Removed - `STAC_IO` class in favor of `StacIO`. This was deprecated in v1.0.0-beta.1 and has been removed in this release. ([#490](https://github.com/stac-utils/pystac/pull/490)) - Support for Python 3.6 ([#500](https://github.com/stac-utils/pystac/pull/500)) ## [v1.0.0-rc.2] ### Added - Add a `preserve_dict` parameter to `ItemCollection.from_dict` and set it to False when using `ItemCollection.from_file`. ([#468](https://github.com/stac-utils/pystac/pull/468)) - `StacIO.json_dumps` and `StacIO.json_loads` methods for JSON serialization/deserialization. These were "private" methods, but are now "public" and documented ([#471](https://github.com/stac-utils/pystac/pull/471)) ### Changed - `pystac.stac_io.DuplicateObjectKeyError` moved to `pystac.DuplicateObjectKeyError` ([#471](https://github.com/stac-utils/pystac/pull/471)) ## [v1.0.0-rc.1] ### Added - License file included in distribution ([#409](https://github.com/stac-utils/pystac/pull/409)) - Links to Issues, Discussions, and documentation sites ([#409](https://github.com/stac-utils/pystac/pull/409)) - Python minimum version set to `>=3.6` ([#409](https://github.com/stac-utils/pystac/pull/409)) - Code of Conduct ([#399](https://github.com/stac-utils/pystac/pull/399)) - `ItemCollection` class for working with GeoJSON FeatureCollections containing only STAC Items ([#430](https://github.com/stac-utils/pystac/pull/430)) - Support for Python 3.9 ([#420](https://github.com/stac-utils/pystac/pull/420)) - Migration for pre-1.0.0-rc.1 Stats Objects (renamed to Range Objects in 1.0.0-rc.3) ([#447](https://github.com/stac-utils/pystac/pull/447)) - Attempting to extend a `STACObject` that does not contain the extension's schema URI in `stac_extensions` raises new `ExtensionNotImplementedError` ([#450](https://github.com/stac-utils/pystac/pull/450)) - `STACObject.from_dict` now takes a `preserve_dict` parameter, which if False will avoid a call to deepcopy on the passed in dict and can result in performance gains (defaults to True. Reading from a file will use preserve_dict=False resulting in better performance. ([#454](https://github.com/stac-utils/pystac/pull/454)) ### Changed - Package author to `stac-utils`, email to `stac@radiant.earth`, url to this repo ([#409](https://github.com/stac-utils/pystac/pull/409)) - `StacIO.read_json` passes arbitrary positional and keyword arguments to `StacIO.read_text` ([#433](https://github.com/stac-utils/pystac/pull/433)) - `FileExtension` updated to work with File Info Extension v2.0.0 ([#442](https://github.com/stac-utils/pystac/pull/442)) - `FileExtension` only operates on `pystac.Asset` instances ([#442](https://github.com/stac-utils/pystac/pull/442)) - `*Extension.ext` methods now have an optional `add_if_missing` argument, which will add the extension schema URI to the object's `stac_extensions` list if it is not present ([#450](https://github.com/stac-utils/pystac/pull/450)) - `from_file` and `from_dict` methods on `STACObject` sub-classes always return instance of calling class ([#451](https://github.com/stac-utils/pystac/pull/451)) ### Fixed - `EOExtension.get_bands` returns `None` for asset without EO bands ([#406](https://github.com/stac-utils/pystac/pull/406)) - `identify_stac_object_type` returns `None` and `identify_stac_object` raises `STACTypeError` for non-STAC objects ([#402](https://github.com/stac-utils/pystac/pull/402)) - `ExtensionManagementMixin.add_to` is now idempotent (only adds schema URI to `stac_extensions` once per `Item` regardless of the number of calls) ([#419](https://github.com/stac-utils/pystac/pull/419)) - Version check for when extensions changed from short links to schema URIs ([#455](https://github.com/stac-utils/pystac/pull/455)) - Schema URI base for STAC 1.0.0-beta.1 ([#455](https://github.com/stac-utils/pystac/pull/455)) ## [v1.0.0-beta.3] ### Added - Summaries for View Geometry, Projection, and Scientific extensions ([#372](https://github.com/stac-utils/pystac/pull/372)) - Raster extension support ([#364](https://github.com/stac-utils/pystac/issues/364)) - solar_illumination field in eo extension ([#356](https://github.com/stac-utils/pystac/issues/356)) - Added `Link.canonical` static method for creating links with "canonical" rel type ([#351](https://github.com/stac-utils/pystac/pull/351)) - Added `RelType` enum containing common `rel` values ([#351](https://github.com/stac-utils/pystac/pull/351)) - Added support for summaries ([#264](https://github.com/stac-utils/pystac/pull/264)) ### Fixed - Links to STAC Spec point to latest supported version ([#368](https://github.com/stac-utils/pystac/pull/368)) - Links to STAC Extension pages point to repos in `stac-extensions` GitHub org ([#368](https://github.com/stac-utils/pystac/pull/368)) - Collection assets ([#373](https://github.com/stac-utils/pystac/pull/373)) ### Removed - Two v0.6.0 examples from the test suite ([#373](https://github.com/stac-utils/pystac/pull/373)) ## [v1.0.0-beta.2] ### Changed - Split `DefaultStacIO`'s reading and writing into two methods to allow subclasses to use the default link resolution behavior ([#354](https://github.com/stac-utils/pystac/pull/354)) - Increased test coverage for the pointcloud extension ([#352](https://github.com/stac-utils/pystac/pull/352)) ### Fixed - Reading json without orjson ([#348](https://github.com/stac-utils/pystac/pull/348)) ### Removed - Removed type information from docstrings, since it is redundant with function type annotations ([#342](https://github.com/stac-utils/pystac/pull/342)) ## [v1.0.0-beta.1] ### Added - Added type annotations across the library ([#309](https://github.com/stac-utils/pystac/pull/309)) - Added assets to collections ([#309](https://github.com/stac-utils/pystac/pull/309)) - `item_assets` extension ([#309](https://github.com/stac-utils/pystac/pull/309)) - `datacube` extension ([#309](https://github.com/stac-utils/pystac/pull/309)) - Added specific errors: `ExtensionAlreadyExistsError`, `ExtensionTypeError`, and `RequiredPropertyMissing`; moved custom exceptions to `pystac.errors` ([#309](https://github.com/stac-utils/pystac/pull/309)) ### Fixed - Validation checks in a few tests ([#346](https://github.com/stac-utils/pystac/pull/346)) ### Changed - API change: The extension API changed significantly. See ([#309](https://github.com/stac-utils/pystac/pull/309)) for more details. - API change: Refactored the global STAC_IO object to an instance-specific `StacIO` implementation. ([#309](https://github.com/stac-utils/pystac/pull/309)) - Asset.get_absolute_href returns None if no absolute href can be inferred (previously the relative href that was passed in was returned) ([#309](https://github.com/stac-utils/pystac/pull/309)) ### Removed - Removed `properties` from Collections ([#309](https://github.com/stac-utils/pystac/pull/309)) - Removed `LinkMixin`, and implemented those methods on `STACObject` directly. STACObject was the only class using LinkMixin and this should not effect users ([#309](https://github.com/stac-utils/pystac/pull/309) - Removed `single-file-stac` extension; this extension is being removed in favor of ItemCollection usage ([#309](https://github.com/stac-utils/pystac/pull/309) ### Deprecated - Deprecated `STAC_IO` in favor of new `StacIO` class. `STAC_IO` will be removed in v1.0.0. ([#309](https://github.com/stac-utils/pystac/pull/309)) ## [v0.5.6] ### Added - HIERARCHICAL_LINKS array constant of all the types of hierarchical links (self is not included) ([#290](https://github.com/stac-utils/pystac/pull/290)) ### Fixed - Fixed error when accessing the statistics attribute of the pointcloud extension when no statistics were defined ([#282](https://github.com/stac-utils/pystac/pull/282)) - Fixed exception being thrown when calling set_self_href on items with assets that have relative hrefs ([#291](https://github.com/stac-utils/pystac/pull/291)) ### Changed - Link behavior - link URLs can be either relative or absolute. Hierarchical (e.g., parent, child) links are made relative or absolute based on the value of the root catalog's `catalog_type` field ([#290](https://github.com/stac-utils/pystac/pull/290)) - Internal self hrefs are set automatically when adding Items or Children to an existing catalog. This removes the need to call `normalize_hrefs` or manual setting of the hrefs for newly added STAC objects ([#294](https://github.com/stac-utils/pystac/pull/294)) - Catalog.generate_subcatalogs is an order of magnitude faster ([#295](https://github.com/stac-utils/pystac/pull/295)) ### Removed - Removed LinkType class and the `link_type` field from links ([#290](https://github.com/stac-utils/pystac/pull/290)) ## [v0.5.5] ### Added - Added support for STAC file extension ([#270](https://github.com/stac-utils/pystac/pull/270)) ### Fixed - Fix handling of optional properties when using apply on view extension ([#259](https://github.com/stac-utils/pystac/pull/259)) - Fixed issue with setting None into projection extension fields that are not required breaking validation ([#269](https://github.com/stac-utils/pystac/pull/269)) ### Changed - Subclass relevant classes from `enum.Enum`. This allows iterating over the class' contents. The `__str__` method is overwritten so this should not break backwards compatibility. ([#261](https://github.com/stac-utils/pystac/pull/261)) - Extract method to correctly handle setting properties in Item/Asset for ItemExtensions ([#272](https://github.com/stac-utils/pystac/pull/272)) ## [v0.5.4] ### Added - SAT Extension ([#236](https://github.com/stac-utils/pystac/pull/236)) - Add support for the scientific extension. ([#199](https://github.com/stac-utils/pystac/pull/199)) ### Fixed - Fix unexpected behaviour of `generate_subcatalogs` ([#241](https://github.com/stac-utils/pystac/pull/241)) - Get eo bands defined in assets only ([#243](https://github.com/stac-utils/pystac/pull/243)) - Collection TemporalExtent can be open ended ([#247](https://github.com/stac-utils/pystac/pull/247)) - Make asset HREFs relative or absolute based on CatalogType during save ([#251](https://github.com/stac-utils/pystac/pull/251)) ### Changed - Be more strict with CatalogType in `Catalog.save` ([#244](https://github.com/stac-utils/pystac/pull/244)) ## [v0.5.3] ### Added - Added support for the pointcloud extension ([#176](https://github.com/stac-utils/pystac/pull/176)) - Added support for the version extension ([#193](https://github.com/stac-utils/pystac/pull/193)) - Added support for the SAR extension ([#203](https://github.com/stac-utils/pystac/pull/203)) - Added the capability to more flexibly organize STACs using `normalize_hrefs` ([#219](https://github.com/stac-utils/pystac/pull/219)) - Added a 'generate_subcatalogs' to Catalog to allow for subcatalogs to be created by using item properties via a template string ([#219](https://github.com/stac-utils/pystac/pull/219)) - Added 'from_items' method to Extent ([#223](https://github.com/stac-utils/pystac/pull/223)) - Added a `catalog_type` property to track the CatalogType of read in or previously saved catalogs ([#224](https://github.com/stac-utils/pystac/pull/224)) - Added a tutorial for creating Landsat 8 STACs ([#181](https://github.com/stac-utils/pystac/pull/181)) - Added codespell to CI ([#206](https://github.com/stac-utils/pystac/pull/206)) - Added more testing to Links ([#211](https://github.com/stac-utils/pystac/pull/211)) ### Fixed - Fixed issue that can cause infinite recursion during full resolve ([#204](https://github.com/stac-utils/pystac/pull/193)) - Fixed issue that required label_classes in label items ([#201](https://github.com/stac-utils/pystac/pull/201)) - Fixed issue that caused geometries and bboxes produced by Shapely to fail PySTAC's validaton ([#201](https://github.com/stac-utils/pystac/pull/201)) - Allow for path prefixes like /vsitar/ ([#208](https://github.com/stac-utils/pystac/pull/208)) - Fix Item set_self_href to ensure item asset locations do not break ([#226](https://github.com/stac-utils/pystac/pull/226)) - Fixed an incorrect exception being thrown from Link.get_href() if there is no target_href ([#201](https://github.com/stac-utils/pystac/pull/201)) - Fixed issue where 0.9.0 items were executing the commons extension logic when they shouldn't ([#221](https://github.com/stac-utils/pystac/pull/221)) - Fixed issue where cloned assets did not have their owning Items set ([#228](https://github.com/stac-utils/pystac/pull/228)) - Fixed issue that caused make_asset_hrefs_relative to produce incorrect HREFs when asset HREFs were already relative ([#229](https://github.com/stac-utils/pystac/pull/229)) - Improve error handling when accidentally importing a Collection with Catalog ([#186](https://github.com/stac-utils/pystac/issues/186)) - Fixed spacenet tutorial bbox issue ([#201](https://github.com/stac-utils/pystac/pull/201)) - Fix formatting of error message in stac_validator ([#190](https://github.com/stac-utils/pystac/pull/204)) - Fixed typos ([#192](https://github.com/stac-utils/pystac/pull/192), [#195](https://github.com/stac-utils/pystac/pull/195)) ### Changed - Refactor caching to utilize HREFs and parent IDs. STAC objects now no longer need unique IDs to work with PySTAC ([#214](https://github.com/stac-utils/pystac/pull/214), [#160](https://github.com/stac-utils/pystac/issues/160)) - Allow a user to pass a single list as bbox and interval for `SpatialExtent` and `TemporalExtent` ([#201](https://github.com/stac-utils/pystac/pull/201), fixes [#198](https://github.com/stac-utils/pystac/issues/198)) ## [v0.5.2] Thank you to all the new contributors that contributed during STAC Sprint 6! ### Added - Added support for the timestamps extension([#161](https://github.com/stac-utils/pystac/pull/161)) - `update_extent_from_items` method to Collection for updating Extent objects within a collection based on the contained items. ([#168](https://github.com/stac-utils/pystac/pull/168)) - `validate_all` method to Catalogs (and by inheritance collections) for validating all catalogs, collections and items contained in that catalog ([#162](https://github.com/azavea/pystac/pull/162)) - `validate_all` method to pystac.validdation for validating all catalogs, collections and items contained in STAC JSON dicts across STAC versions. ([#162](https://github.com/azavea/pystac/pull/162)) - Additional test coverage. ([#165](https://github.com/azavea/pystac/pull/165), [#171](https://github.com/azavea/pystac/pull/171)) - Added codecov to CI ([#163](https://github.com/stac-utils/pystac/pull/164)) ### Fixed - Fix bug that caused get_children to miss some links. ([#172](https://github.com/stac-utils/pystac/pull/172)) - Fixed bug in ExtensionIndex that was causing errors when trying to read help() for that object ([#159](https://github.com/stac-utils/pystac/pull/159)) ### Changed - Remove spaces in CBERS test library ([#157](https://github.com/stac-utils/pystac/pull/157)) - Changed some unit test assertions for better error messages ([#158](https://github.com/stac-utils/pystac/pull/158)) - Moved PySTAC to the [stac-utils](https://github.com/stac-utils) GitHub organization. ## [v0.5.1] ### Added - A tutorial for creating extensions ([#150](https://github.com/azavea/pystac/pull/150)) ### Fixed - Fixed Satellite extension ID, using `sat` instead of `satellite` ([#146](https://github.com/azavea/pystac/pull/146), [#147](https://github.com/azavea/pystac/pull/147)) ## [v0.5.0] ### Added - Added support for the Projection extension([#125](https://github.com/azavea/pystac/pull/125)) - Add support for Item Asset properties ([#127](https://github.com/azavea/pystac/pull/127)) - Added support for dynamically changing the STAC version via `pystac.set_stac_version` and `pystac.get_stac_version` ([#130](https://github.com/azavea/pystac/pull/130)) - Added support for prerelease versions in version comparisons for the `pystac.serialization.identify` package ([#138](https://github.com/azavea/pystac/pull/138)) - Added validation for PySTAC STACObjects as well as arbitrary STAC JSON ([#139](https://github.com/azavea/pystac/pull/139)) - Added the ability to read HTTP and HTTPS uris by default ([#139](https://github.com/azavea/pystac/pull/139)) ### Changed - Clarification on null geometries, making bbox not required if a null geometry is used. ([#123](https://github.com/azavea/pystac/pull/123)) - Multiple extents (bounding boxes / intervals) are allowed per Collection ([#123](https://github.com/azavea/pystac/pull/123)) - Moved eo:gsd from eo extension to core gsd field in Item common metadata ([#123](https://github.com/azavea/pystac/pull/123)) asset extension renamed to item-assets and renamed assets field in Collections to item_assets ([#123](https://github.com/azavea/pystac/pull/123)) - `get_asset_bands` and `set_asset_bands` were renamed `get_bands` and `set_bands` and follow the new item asset property access pattern. - Modified the `single-file-stac` extension to extend `Catalog` ([#128](https://github.com/azavea/pystac/pull/128)) ### Removed - ItemCollection was removed. ([#123](https://github.com/azavea/pystac/pull/123)) - The commons extension was removed. Collection properties will still be merged for pre-1.0.0-beta.1 items where appropriate ([#129](https://github.com/azavea/pystac/pull/129)) - Removed `pystac.STAC_VERSION`. See addition of `get_stac_version` above. ([#130](https://github.com/azavea/pystac/pull/130)) ## [v0.4.0] The two major changes for this release are: - Upgrade to STAC 0.9.0 - Refactor the extensions API to accommodate items that implement multiple extensions (e.g. `eo` and `view`) See the [stac-spec 0.9.0 changelog](https://github.com/radiantearth/stac-spec/blob/v0.9.0/CHANGELOG.md) and issue [#65](https://github.com/azavea/pystac/issues/65) for more information. ### API Changes These are the major API changes that will have to be accounted for when upgrading PySTAC: #### Extensions are wrappers around Catalogs, Collection and Items, and no longer inherit This change affects the two extensions that were implemented for Item - `EOItem` and `LabelItem` have become `EOItemExt` and `LabelItemExt`, and no longer inherit from Item. This change was motivated by the 0.9 change that split some properties out from `eo` into the `view` extension. If we kept an inheritance-based extension architecture, we would not be able to account well for these new items that implemented both the `eo` and `view` extensions. See the [Extensions section](https://pystac.readthedocs.io/en/0.4/concepts.html#extensions) in the documentation for more information on the new way to use extensions. #### Extensions have moved to their own package - `pystac.label` -> `pystac.extensions.label` - `pystac.eo` -> `pystac.extensions.eo` - `pystac.single_file_stac` -> `pystac.extensions.single_file_stac` ### Added - `pystac.read_file` as a convenience function for reading in a STACObject from a file at a URI which delegates to `STACObject.from_file`. - `pystac.read_file` as a convenience function for reading in a STACObject from a file at a URI. - Added support for the [view](https://github.com/radiantearth/stac-spec/tree/v0.9.0/extensions/view) extension. - Added support for the [commons](https://github.com/radiantearth/stac-spec/tree/v0.9.0/extensions/commons) extension. ### Changed - Migrated CI workflows from Travis CI to GitHub Actions [#108](https://github.com/azavea/pystac/pull/108) - Dropped support for Python 3.5 [#108](https://github.com/azavea/pystac/pull/108) - Extension classes for label, eo and single-file-stac were moved to the `pystac.extensions` package. - the eo and label extensions changed from being a subclass of Item to wrapping items. **Note**: This is a major change in the API for dealing with extensions. See the note below for more information. - Renamed the class that enumerates extension names from `Extension` to `Extensions` - Asset properties always return a dict instead of being None for Assets that have non-core properties. - The `Band` constructor in the EO extension changed to taking a dict. To create a band from property values, use `Band.create` ## [v0.3.4] - 2020-06-20 ### Changed - Further narrow version for SAR extension [#85](https://github.com/azavea/pystac/pull/85) ### Fixed - Fixed issue with reading ItemCollections directly. [#86](https://github.com/azavea/pystac/pull/86) - Fix bug in `make_absolute_href` [#94](https://github.com/azavea/pystac/pull/94) - Fixed issues with `fully_resolve` [#98](https://github.com/azavea/pystac/pull/98) - Fixed a bug when root link was not set [#100](https://github.com/azavea/pystac/pull/100) ## [v0.3.3] - 2020-02-05 ### Added - Allow for backwards compatibility for reading STAC [#77](https://github.com/azavea/pystac/pull/70) ### Fixed - Fix issue with multiple collection reads per item [#79](https://github.com/azavea/pystac/pull/79) - Fix issue with iteration of children in `catalog.walk` [#78](https://github.com/azavea/pystac/pull/78) - Allow v0.7.0 sar items to fit in version range [#80](https://github.com/azavea/pystac/pull/80) ## [v0.3.2] - 2020-01-28 ### Added - Add functionality for identifying STAC JSON information [#50](https://github.com/azavea/pystac/pull/50) ### Fixed - Documentation improvements [#44](https://github.com/azavea/pystac/pull/44) - Updated MediaTypes to reflect correct GeoTIFF and COG names [#66](https://github.com/azavea/pystac/pull/66) - Fix utils to work with windows paths. [#68](https://github.com/azavea/pystac/pull/68) - Modified output datetime strings to ISO8601. [#69](https://github.com/azavea/pystac/pull/69) - Respect tzinfo in the provided datetime [#70](https://github.com/azavea/pystac/pull/70) - Set asset owner to item when reading in items.[#71](https://github.com/azavea/pystac/pull/71) - Fixed catalog and collection clone logic to avoid duplication of root link [#72](https://github.com/azavea/pystac/pull/72) ## [v0.3.1] - 2019-11-04 ### Added - Add methods for removing single items and children from catalogs. - Add methods for removing objects from the ResolvedObjectCache. ### Fixed - Fixed issue where cleared items and children were still in the root object cache. ### Changed - Moved STAC version to 0.8.1 - LabelItem reduced validation as there is some confusion on how segmentation classes ## [v0.3.0] - 2019-10-31 Initial release. [Unreleased]: [v1.9.0]: [v1.8.4]: [v1.8.3]: [v1.8.2]: [v1.8.1]: [v1.8.0]: [v1.7.3]: [v1.7.2]: [v1.7.1]: [v1.7.0]: [v1.6.1]: [v1.6.0]: [v1.5.0]: [v1.4.0]: [v1.3.0]: [v1.2.0]: [v1.1.0]: [v1.0.1]: [v1.0.0]: [v1.0.0-rc.3]: [v1.0.0-rc.2]: [v1.0.0-rc.1]: [v1.0.0-beta.3]: [v1.0.0-beta.2]: [v1.0.0-beta.1]: [v0.5.6]: [v0.5.5]: [v0.5.4]: [v0.5.3]: [v0.5.2]: [v0.5.1]: [v0.5.0]: [v0.4.0]: [v0.3.4]: [v0.3.3]: [v0.3.2]: [v0.3.1]: [v0.3.0]: pystac-1.9.0/CODE_OF_CONDUCT.md000066400000000000000000000024501451576074700156440ustar00rootroot00000000000000# PySTAC Community Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the [STAC Project Steering Committee](mailto:stac-psc@radiant.earth). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. PySTAC follows the [STAC spec Code of Conduct](https://github.com/radiantearth/stac-spec/blob/master/CODE_OF_CONDUCT.md). pystac-1.9.0/LICENSE000066400000000000000000000010641451576074700140520ustar00rootroot00000000000000Copyright 2019, 2020, 2021 the authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. pystac-1.9.0/MANIFEST.in000066400000000000000000000002651451576074700146050ustar00rootroot00000000000000include pystac/py.typed pystac/html/*.jinja2 pystac/validation/jsonschemas/geojson/*.json pystac/validation/jsonschemas/stac-spec/v1.0.0/*.json pystac/static/*.json exclude tests/* pystac-1.9.0/README.md000066400000000000000000000056101451576074700143250ustar00rootroot00000000000000# PySTAC [![Build Status](https://github.com/stac-utils/pystac/workflows/CI/badge.svg?branch=main)](https://github.com/stac-utils/pystac/actions/workflows/continuous-integration.yml) [![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) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) PySTAC is a library for working with the [SpatioTemporal Asset Catalog](https://stacspec.org) specification in Python 3. ## Installation ### Install from PyPi (recommended) ```shell pip install pystac ``` If you would like to enable the validation feature utilizing the [jsonschema](https://pypi.org/project/jsonschema/) project, install with the optional `validation` requirements: ```shell pip install 'pystac[validation]' ``` If you would like to use the [`orjson`](https://pypi.org/project/orjson/) instead of the standard `json` library for JSON serialization/deserialization, install with the optional `orjson` requirements: ```shell pip install 'pystac[orjson]' ``` If you would like to use a custom `RetryStacIO` class for automatically retrying network requests when reading with PySTAC, you'll need [`urllib3`](https://urllib3.readthedocs.io/en/stable/): ```shell pip install 'pystac[urllib3]' ``` If you are using jupyter notebooks and want to enable pretty display of pystac objects you'll need [`jinja2`](https://pypi.org/project/Jinja2/) ```shell pip install 'pystac[jinja2]' ``` ### Install from source ```shell git clone https://github.com/stac-utils/pystac.git cd pystac pip install . ``` See the [installation page](https://pystac.readthedocs.io/en/latest/installation.html) for more options. ## Documentation See the [documentation page](https://pystac.readthedocs.io/en/latest/) for the latest docs. ## Developing See [contributing docs](https://pystac.readthedocs.io/en/latest/contributing.html) for details on contributing to this project. ## Running the quickstart and tutorials There is a quickstart and tutorials written as jupyter notebooks in the `docs/tutorials` folder. To run the notebooks, run a jupyter notebook with the `docs` directory as the notebook directory: ```shell jupyter notebook --ip 0.0.0.0 --port 8888 --notebook-dir=docs ``` You can then navigate to the notebooks and execute them. Requires [Jupyter](https://jupyter.org/) be installed. pystac-1.9.0/RELEASING.md000066400000000000000000000035751451576074700147110ustar00rootroot00000000000000# Releasing This is a checklist to use when releasing a new PySTAC version. 1. Determine the next version. We do not currently have a versioning guide, but has some discussion around the topic. 2. Create a release branch with the name `release/vX.Y.Z`, where `X.Y.Z` is the next version (e.g. `1.7.0`). 3. Pull fields-normalized.json from cdn: run `scripts/pull-static`. Note you will need to have [jq](https://stedolan.github.io/jq/) installed. 4. Update the `__version__` attribute in `pystac/version.py` with the new version. 5. Update all cassettes: `pytest --record-mode rewrite` 6. Update the CHANGELOG. - Create a new header below `## [Unreleased]` with the new version. - Remove any unused header sections. - Update the links at the bottom of the page for the new header. - Audit the CHANGELOG for correctness and readability. 7. Audit the changes. Use the CHANGELOG, your favorite diff tool, and the merged Github pull requests to ensure that: - All notable changes are captured in the CHANGELOG. - The type of release is appropriate for the new version number, i.e. if there are breaking changes, the MAJOR version number must be increased. - All deprecated items that were marked for removal in this version are removed. 8. Commit your changes, push your branch to Github, and request a review. 9. Once approved, merge the PR. 10. Once the PR is merged, create a tag with the version name, e.g. `vX.Y.Z`. Prefer a signed tag, if possible. Push the tag to Github. 11. Use the tag to finish your release notes, and publish those. The "auto generate" feature is your friend, here. When the release is published, this will trigger the build and release on PyPI. 12. Announced the release in [Gitter](https://matrix.to/#/#SpatioTemporal-Asset-Catalog_python:gitter.im) and on any relevant social media. pystac-1.9.0/asv.conf.json000066400000000000000000000013371451576074700154600ustar00rootroot00000000000000{ "version": 1, "project": "pystac", "project_url": "https://pystac.readthedocs.io/", "repo": ".", "branches": [ "main" ], "dvcs": "git", "environment_type": "virtualenv", "show_commit_url": "http://github.com/stac-utils/pystac/commit/", "matrix": { "req": { "orjson": [ null, "" ] } }, "benchmark_dir": "benchmarks", "env_dir": ".asv/env", "results_dir": ".asv/results", "html_dir": ".asv/html", "build_command": [ "pip install build", "python -m build", "PIP_NO_BUILD_ISOLATION=false python -mpip wheel --no-deps --no-index -w {build_cache_dir} {build_dir}" ] }pystac-1.9.0/benchmarks/000077500000000000000000000000001451576074700151615ustar00rootroot00000000000000pystac-1.9.0/benchmarks/__init__.py000066400000000000000000000000001451576074700172600ustar00rootroot00000000000000pystac-1.9.0/benchmarks/_base.py000066400000000000000000000002211451576074700165770ustar00rootroot00000000000000class Bench: # Repeat between 10-50 times up to a max time of 5s repeat = (10, 50, 2.0) # Bump number of rounds to 4 rounds = 4 pystac-1.9.0/benchmarks/_util.py000066400000000000000000000007261451576074700166540ustar00rootroot00000000000000import os from typing import TYPE_CHECKING, Union if TYPE_CHECKING: PathLike = os.PathLike[str] else: PathLike = os.PathLike def get_data_path(rel_path: Union[str, PathLike]) -> str: """Gets the absolute path to a file based on a path relative to the tests/data-files directory in this repo.""" rel_path = os.fspath(rel_path) return os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "tests", "data-files", rel_path) ) pystac-1.9.0/benchmarks/catalog.py000066400000000000000000000064401451576074700171510ustar00rootroot00000000000000import json import os import shutil import tempfile from datetime import datetime from pathlib import Path from tempfile import TemporaryDirectory from pystac import ( Catalog, Collection, Extent, Item, SpatialExtent, StacIO, TemporalExtent, ) from ._base import Bench from ._util import get_data_path class CatalogBench(Bench): def setup(self) -> None: self.temp_dir = tempfile.mkdtemp() self.stac_io = StacIO.default() self.catalog_path = get_data_path("examples/1.0.0/catalog.json") with open(self.catalog_path) as src: self.catalog_dict = json.load(src) self.catalog = Catalog.from_file(self.catalog_path) def teardown(self) -> None: shutil.rmtree(self.temp_dir, ignore_errors=True) def time_catalog_from_file(self) -> None: """Deserialize an Item from file""" _ = Catalog.from_file(self.catalog_path) def time_catalog_from_dict(self) -> None: """Deserialize an Item from dictionary.""" _ = Catalog.from_dict(self.catalog_dict) def time_catalog_to_dict(self) -> None: """Serialize an Item to a dictionary.""" self.catalog.to_dict(include_self_link=True) def time_catalog_save(self) -> None: """Serialize an Item to a JSON file.""" self.catalog.save_object( include_self_link=True, dest_href=os.path.join(self.temp_dir, "time_catalog_save.json"), stac_io=self.stac_io, ) class WalkCatalogBench(Bench): def setup_cache(self) -> Catalog: return make_large_catalog() def time_walk(self, catalog: Catalog) -> None: for ( _, _, _, ) in catalog.walk(): pass def peakmem_walk(self, catalog: Catalog) -> None: for ( _, _, _, ) in catalog.walk(): pass class ReadCatalogBench(Bench): def setup(self) -> None: catalog = make_large_catalog() self.temporary_directory = TemporaryDirectory() self.path = str(Path(self.temporary_directory.name) / "catalog.json") catalog.normalize_and_save(self.temporary_directory.name) def teardown(self) -> None: shutil.rmtree(self.temporary_directory.name) def time_read_and_walk(self) -> None: catalog = Catalog.from_file(self.path) for _, _, _ in catalog.walk(): pass class WriteCatalogBench(Bench): def setup(self) -> None: self.catalog = make_large_catalog() self.temporary_directory = TemporaryDirectory() def teardown(self) -> None: shutil.rmtree(self.temporary_directory.name) def time_normalize_and_save(self) -> None: self.catalog.normalize_and_save(self.temporary_directory.name) def make_large_catalog() -> Catalog: catalog = Catalog("an-id", "a description") extent = Extent( SpatialExtent([[-180.0, -90.0, 180.0, 90.0]]), TemporalExtent([[datetime(2023, 1, 1), None]]), ) for i in range(0, 10): collection = Collection(f"collection-{i}", f"Collection {i}", extent) for j in range(0, 100): item = Item(f"item-{i}-{j}", None, None, datetime.now(), {}) collection.add_item(item) catalog.add_child(collection) return catalog pystac-1.9.0/benchmarks/collection.py000066400000000000000000000025441451576074700176730ustar00rootroot00000000000000import json import os import shutil import tempfile from pystac import Collection, StacIO from ._base import Bench from ._util import get_data_path class CollectionBench(Bench): def setup(self) -> None: self.temp_dir = tempfile.mkdtemp() self.stac_io = StacIO.default() self.collection_path = get_data_path("examples/1.0.0/collection.json") with open(self.collection_path) as src: self.collection_dict = json.load(src) self.collection = Collection.from_file(self.collection_path) def teardown(self) -> None: shutil.rmtree(self.temp_dir, ignore_errors=True) def time_collection_from_file(self) -> None: """Deserialize an Item from file""" _ = Collection.from_file(self.collection_path) def time_collection_from_dict(self) -> None: """Deserialize an Item from dictionary.""" _ = Collection.from_dict(self.collection_dict) def time_collection_to_dict(self) -> None: """Serialize an Item to a dictionary.""" self.collection.to_dict(include_self_link=True) def time_collection_save(self) -> None: """Serialize an Item to a JSON file.""" self.collection.save_object( include_self_link=True, dest_href=os.path.join(self.temp_dir, "time_collection_save.json"), stac_io=self.stac_io, ) pystac-1.9.0/benchmarks/extensions/000077500000000000000000000000001451576074700173605ustar00rootroot00000000000000pystac-1.9.0/benchmarks/extensions/__init__.py000066400000000000000000000000001451576074700214570ustar00rootroot00000000000000pystac-1.9.0/benchmarks/extensions/projection.py000066400000000000000000000006101451576074700221030ustar00rootroot00000000000000from datetime import datetime from pystac import Item from pystac.extensions.projection import ProjectionExtension from .._base import Bench class ProjectionBench(Bench): def setup(self) -> None: self.item = Item("an-id", None, None, datetime.now(), {}) def time_add_projection_extension(self) -> None: _ = ProjectionExtension.ext(self.item, add_if_missing=True) pystac-1.9.0/benchmarks/import_pystac.py000066400000000000000000000002131451576074700204240ustar00rootroot00000000000000class ImportPySTACBench: repeat = 10 def timeraw_import_pystac(self) -> str: return """ import pystac """ pystac-1.9.0/benchmarks/item.py000066400000000000000000000024741451576074700165000ustar00rootroot00000000000000import json import os import shutil import tempfile from pystac import Item, StacIO from ._base import Bench from ._util import get_data_path class ItemBench(Bench): def setup(self) -> None: self.temp_dir = tempfile.mkdtemp() self.stac_io = StacIO.default() # using an item with many assets to better test deserialization timing self.item_path = get_data_path("eo/eo-sentinel2-item.json") with open(self.item_path) as src: self.item_dict = json.load(src) self.item = Item.from_file(self.item_path) def teardown(self) -> None: shutil.rmtree(self.temp_dir, ignore_errors=True) def time_item_from_file(self) -> None: """Deserialize an Item from file""" _ = Item.from_file(self.item_path) def time_item_from_dict(self) -> None: """Deserialize an Item from dictionary.""" _ = Item.from_dict(self.item_dict) def time_item_to_dict(self) -> None: """Serialize an Item to a dictionary.""" self.item.to_dict(include_self_link=True) def time_item_save(self) -> None: """Serialize an Item to a JSON file.""" self.item.save_object( include_self_link=True, dest_href=os.path.join(self.temp_dir, "time_item_save.json"), stac_io=self.stac_io, ) pystac-1.9.0/codecov.yml000066400000000000000000000002021451576074700152030ustar00rootroot00000000000000coverage: status: project: default: informational: true patch: default: informational: true pystac-1.9.0/doc8.ini000066400000000000000000000001011451576074700143720ustar00rootroot00000000000000[doc8] ignore-path=docs/_build,docs/tutorials max-line-length=88pystac-1.9.0/docs/000077500000000000000000000000001451576074700137745ustar00rootroot00000000000000pystac-1.9.0/docs/Makefile000066400000000000000000000012641451576074700154370ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) livehtml: sphinx-autobuild --watch ../pystac --host 0.0.0.0 ${SOURCEDIR} $(BUILDDIR)/html -d _build/doctrees .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) pystac-1.9.0/docs/_static/000077500000000000000000000000001451576074700154225ustar00rootroot00000000000000pystac-1.9.0/docs/_static/STAC-03.png000066400000000000000000003502061451576074700171100ustar00rootroot00000000000000‰PNG  IHDRŸš¯•MsRGB®Îé€eXIfMM*1$2‡iVAdobe Photoshop CC 2015 (Macintosh)  Ÿ šÑižµiTXtXML:com.adobe.xmp xmp.iid:F6DA6C9F500D11E88467C7F7667B81A8 xmp.iid:F6DA6C9D500D11E88467C7F7667B81A8 xmp.did:F6DA6C9E500D11E88467C7F7667B81A8 xmp.did:F6DA6CA0500D11E88467C7F7667B81A8 1 Adobe Photoshop CC 2015 (Macintosh) =¾£@IDATxìÝ ¼œW}üsf®dI–µ^YÆX`³ÙÆ)%ÐãÈKjÙ’ æ:ä%!²%”:iÈVš.I£Ð6i 4É›&é«ÚÆ ! 2[h(‹YÒ@œÊ‚±q¼H^1ÚîÌéÙ"’ÐHÏ]ž™gæ|çó¹Ü™gÎsÎùÏpGžß<Ï‚ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ÀÑñè MzœRj}üŽÏï¦ðÒãóBHçäù­Í?Kš4Os!@€öç6»òÏ—òÏmù=îƒíÖ£öÂuëöTØW @€ @€ ÐxF†ÏŸüê7×NOL¿>‡Í›²àéW4A 0I1ÜR÷Íê®»–ì^òó!t:„¸¨)HæA€jÈtü£§ßðÂu“ߨy,Ý @€ @€ @€Z>ìÎÝß“º­·ç*Ϫ¥R @€¦ ä#¡c+ý؋֭úý¦OÕü @€ @€ @€ÀÑŸo½ãÁ-!¥ßÉ“›8z‚ @€~ë®u+~òŠ;Ö®d @€ @€Q¡‡Ï·Þ~ÿ¿1þʈú™6¨E ưãg¬øAt-¼:%@€ @€ @€Z5ôY¹ËƒG< ž+{iH€å¤¦žxǃÛË©X¥ @€ @€ 0êC;òùãw<ô¼nê~,:Õö¨¿ŠÌŸêˆé'Î_·ê¿Õ7€ž  @€ @€ @€Àü %|þÔ]w-Ù;½ø³¹„³æ§ ½ @€±Ø×îv¾û{ΜüüØV¨0 @€ @€ ¡œv{ïô’—õÏcñRÔ,pR'¶«æ1tO€ @€ @€æ,0ð#Ÿ?ö•GNM ÜB\4çÙë€"Ð ­öÂ'-ÿÓBÊU& @€ @€Œ ÀÀ|î.ì¼^ð<‚¯S&@€¡ tb÷ C€Á  @€ @€ @€À >§”Z1¤M'˜“§  @€£b /ûÄß=pæQ›=$@€ @€ @€høü±;xQ®ü‰©ÞD @€À t&âÔM×T  @€ @€ @ 0†Ï)Å—æ«\ 0o)u½Î›¦Ž @€ @€ @`¾>ÇŸ7ßè”"C|~)µª“ @€ @€FO` áséìÑ#2c ÐùûGÖ4f6&B€ @€ @€pøN;llw  @€ ´;ÓOœá.š @€ @€ @€ :|^2ª B€ÆT uÒÒ1-MY @€ @€ 0⃟GœËô  @€ÀpZ)u‡;£ @€ @€ @€c Ÿíb+h¤Àt;~³‘3) @€ @€(^@ø\üKŒ’@g:~}”æk® @€ @€ PŽ€ð¹œµV)Œº@ ÷¾ä¬•ŽzæO€ @€ @€ã) |ÏuUŒ£@ ŸDzÔD€ @€ @€ã! |uT Bú@e*‘ @€ @€FT@ø<¢ gÚ Pœ@gÁôŠ«ZÁ @€ @€ 02Âç‘Y*%@€ÂnzÁS–ÞS¸ò  @€ @€ @ ÁÂç/Ž© @€C)¶~õÐ}¿  @€ @€ @€@„ÏM\s"@€‡ ¤nxñºå~Ø&w  @€ @€ @€@ã„Ï["@€G<ØŸ:b‹ @€ @€ @ Âç.Š) @€Cù¨ç«^¸nò‡ûM€ @€ @€š*0ÑÔ‰™(] ưíüu+ß]ºƒú  @€ @€ @`4ù<ëd– Pœ@zó‹Ö­ü¥âÊV0 @€ @€Œ¬€ðyd—ÎÄ  @`Lº)„yþ“V½aLëS @€ @€Œ©€ÓnéÂ*‹FRàëÝ^{Áº•ÉÙ›4 @€ @€-àÈ碗_ñ ÐGCˆ¿¼hbϹ‚熬ˆi @€ @€ @€ÀŒùש«o @€ @€ @€" |.d¡•I€ @€ @€ @€:„Ïuêê› @€ @€ @€…Ÿ Yhe @€ @€ @€ @ Násºú&@€ @€ @€ @€@!ÂçBZ™ @€ @€ @€¨S@ø\§®¾  @€ @€ @€ Pˆ€ð¹…V& @€ @€ @€ê>ש«o @€ @€ @€" |.d¡•I€ @€ @€ @€:„Ïuêê› @€ @€ @€…Ÿ Yhe @€ @€ @€ @ Násºú&@€ @€ @€ @€@!ÂçBZ™ @€ @€ @€¨S@ø\§®¾  @€ @€ @€ Pˆ€ð¹…V& @€ @€ @€ê>ש«o @€ @€ @€" |.d¡•I€ @€ @€ @€:„Ïuêê› @€ @€ @€…Ÿ Yhe @€ @€ @€ @ Násºú&@€ @€ @€ @€@!ÂçBZ™ @€ @€ @€¨S@ø\§®¾  @€ @€ @€ Pˆ€ð¹…V& @€ @€ @€ê>ש«o @€ @€ @€" |.d¡•I€ @€ @€ @€:„Ïuêê› @€ @€ @€…Ÿ Yhe @€ @€ @€ @ Násºú&@€ @€ @€ @€@!ÂçBZ™ @€ @€ @€¨S@ø\§®¾  @€ @€ @€ Pˆ€ð¹…V& @€ @€ @€ê>ש«o @€ @€ @€" |.d¡•I€ @€ @€ @€:„Ïuêê› @€ @€ @€…Ÿ Yhe @€ @€ @€ @ Násºú&@€ @€ @€ @€@!ÂçBZ™ @€ @€ @€¨S@ø\§®¾  @€ @€ @€ Pˆ€ð¹…V& @€ @€ @€ê>ש«o @€ @€ @€" |.d¡•I€ @€ @€ @€:„Ïuêê› @€ @€ @€…Ÿ Yhe @€ @€ @€ @ Násºú&@€ @€ @€ @€@!ÂçBZ™ @€ @€ @€¨S@ø\§®¾  @€ @€ @€ Pˆ€ð¹…V& @€ @€ @€ê>ש«o @€ @€ @€" |.d¡•I€ @€ @€ @€:„Ïuêê› @€ @€ @€…Ÿ Yhe @€ @€ @€ @ Násºú&@€ @€ @€ @€@!ÂçBZ™ @€ @€ @€¨S@ø\§®¾  @€ @€ @€ Pˆ€ð¹…V& @€ @€ @€ê>ש«o @€ @€ @€" |.d¡•I€ @€ @€ @€:„Ïuêê› @€ @€ @€…Ÿ Yhe @€ @€ @€ @ Násºú&@€ @€ @€ @€@!ÂçBZ™ @€ @€ @€¨S@ø\§®¾  @€ @€ @€ Pˆ€ð¹…V& @€ @€ @€ê>ש«o @€ @€ @€" |.d¡•I€ @€ @€ @€:„Ïuêê› @€ @€ @€…Ÿ Yhe @€ @€ @€ @ Násºú&@€ @€ @€ @€@!ÂçBZ™ @€ @€ @€¨S@ø\§®¾  @€ @€ @€ Pˆ€ð¹…V& @€ @€ @€ê>ש«o @€ @€ @€" |.d¡•I€ @€ @€ @€:„Ïuêê› @€ @€ @€…Ÿ Yhe @€ @€ @€ @ Násºú&@€ @€ @€ @€@!ÂçBZ™ @€ @€ @€¨S@ø\§®¾  @€ @€ @€ Pˆ€ð¹…V& @€ @€ @€ê>ש«o @€ @€ @€" |.d¡•I€ @€ @€ @€:„Ïuêê› @€ @€ @€…Ÿ Yhe @€ @€ @€ @ Násºú&@€ @€ @€ @€@!ÂçBZ™ @€ @€ @€¨S@ø\§®¾  @€ @€ @€ Pˆ€ð¹…V& @€ @€ @€ê>ש«o @€ @€ @€" |.d¡•I€ @€ @€ @€:„Ïuêê› @€ @€ @€…Ÿ Yhe @€ @€ @€ @ Násºú&@€ @€ @€ @€@!ÂçBZ™ @€ @€ @€¨S@ø\§®¾  @€ @€ @€ Pˆ€ð¹…V& @€ @€ @€ê˜¨³s}¨À;Þ±pñâ‰ç…^[éœnJ§ÇÐZBZ6ÐyŒÌ^`o á‘ÂîüûKù÷gÚí·¿¹þŠûfߥ=  @€ @€ 0áó`œR£À¢÷¼ó‚V7lÎÐ_žƒæ“Cþ´>å±w''Ñn @`”½súÝé´Óâ›ßùñ˜âï~kÏôï‡+®Ø3Jõ˜+ @€ @€åôÒ¹Ýnýú‡>Gؘ'(†ÛÏ_·ò̶Ӡq‹w¾ëü.¿)O웜  @€zîÍß­ú/{NÛõÿ…ò£êB¯ @€ @€˜€k>ÏÎÍ^ÃxÇ;–.ÙùÎërðüÑ< Áó0×ÂØ 0hSóI=Þ¼äî5µäæw?wЃ @€ @€Ç>OÇsX¸ó†sòu?•¡¿²q“3! 0 ”Ò3Sê~|ÉÍïÜ: ! C€ @€ @€ ŸOH¤AS¿ç†ç·CÌG;§³›2'ó @€CX˜RøÿóÙ@¶ q†&@€ @€ @€À·„Ïߦp§ÉKoºá¼Ð·ä9N6yžæF€-Ïò‹‹wÞð†Ak< @€ @€- |>ZÄãÆ ,¿ùæ•V¼9Oleã&gB @ ñ¿äSpolÄTL‚ @€ @€b„ÏÅ.ýè¾?íû!…3GgÆfJ€/OÁ}ý’o<}ð#‘ @€ @€ Ÿ½-°dç ò/oô$MŽ4C`ejMÿF3¦b @€ @€”( |.qÕG¥æmÛZ)Ä·ŒÊtÍ“4@àU‹ozç 0S @€ @€ @ @ás‹>*%/~î?šÊs}ú¨Ì×<  @€@R ¿Ð„y˜ @€ @€å ŸË[óQªxë(MÖ\  @€@b-~ï;ÏhÂ\Ì @€ @€²„Ïe­÷ÈT»ä}ï|BžìKFfÂ&J€š#Ãt|us¦c& @€ @€”" |.e¥G¬ÎîtzYžr>x˘©@Œéûgºö @€ @€˜«€ðy®‚ö¯I ž_SǺ%@€c/BxaHùêÏn @€ @€ €ðy€Ø†ª.»áÜê­µ$@€ŽX²è½ïzÒQÛ<$@€ @€ @€@­ÂçZyu>kÖÍz_; @€!¤®ðÙë€ @€ @€ ŸÊm°¬˜A[M  @€£Z¡µì¨M @€ @€ @ Vás­¼:Ÿƒ€×æðìJ€ò5ŸP @€ @€ @€À |ƒÔ6 @`@ÝÐP†!@€ @€ @€ÀAá³ @` Ú±½k ËR @€ @€ >7xqL 0Kôh\ø¥Yîk7 @€ @€ÌJ@ø<+6; @€š,>Ö¯ß×äš @€ @€ã' |¿5U P¼@ëƒÅ @€ @€ @`àÂç“ P»À µ` @€ @€% |> ÄC @€Àˆ |yÏ%¯øØˆ×`ú @€ @€Œ €ðyÍ”  @€ýbˆÿ5Ęú=o; @€ @€ê>×%«_ @€Àà¾ò­=Ó× ~X# @€ @€ @ á³W @`LZ1ý‹pÅûǤe @€ @€ 0bÂç[0Ó%@€[ ýÖ£—¼êOýœ­ @€ @€¨_`¢þ!Œ@€ P¯@üèžö⟭w ½ @€ @€ @àøŽ|>¾g  @€M¸mÑ¢é aýú}MŸ¨ù @€ @€ 0ÞÂçñ^_Õ @€ã-ðÁE‹:/yàÿ¹â¡ñ.Su @€ @€Œ‚€ðyVÉ  @€G ¤Â¯îYºêûÏGÂxD€ @€ @€Àð\óyxöF&@€ÌFà/sðüã{6^þÉÙìl @€ @€Ô% |®KV¿ @€ù¸5†ô–o]rù!Æ4¿]ë @€ @€s>ÏÝP @€ùH!†ÛCHBüPêvnÜ»ñŠ¿›ïAôG€ @€ @€ù>ϧ¦¾Æ]à‘4ÝzÖ¸©>®@»ÕÚó肆õë÷ w&F'@€ @€ @€ÀÌ„Ï3óÒºlîÞW¼âke¨ž @€ @€ @€À±ZÇÞl+ @€ @€ @€¨. |®n¥% @€ @€ @€ô>÷±™ @€ @€ @€ª Ÿ«[iI€ @€ @€ @€}„Ï}`l&@€ @€ @€ @€êÂçêVZ @€ @€ @€ @€@ás›  @€ @€ @€ @ º€ð¹º•– @€ @€ @€ ÐG@øÜÆf @€ @€ @€¨. |®n¥% @€ @€ @€ô>÷±™ @€ @€ @€ª Ÿ«[iI€ @€ @€ @€}„Ï}`l&@€ @€ @€ @€êÂçêVZ @€ @€ @€ @€@ás›  @€ @€ @€ @ º€ð¹º•– @€ @€ @€ ÐG@øÜÆf @€ @€ @€¨. |®n¥% @€ @€ @€ô>÷±™ @€ @€ @€ª Ÿ«[iI€ @€ @€ @€}„Ï}`l&@€ @€ @€ @€êÂçêVZ @€ @€ @€ @€@ás›  @€ @€ @€ @ º€ð¹º•– @€ @€ @€ ÐG@øÜÆf @€ @€ @€¨. |®n¥% @€ @€ @€ô>÷±™ @€ @€ @€ª Ÿ«[iI€ @€ @€ @€}„Ï}`l&@€ @€ @€ @€êÂçêVZ @€ @€ @€ @€@ás›  @€ @€ @€ @ º€ð¹º•– @€ @€ @€ ÐG@øÜÆf @€ @€ @€¨. |®n¥% @€ @€ @€ô>÷±™ @€ @€ @€ª Ÿ«[iI€ @€ @€ @€}„Ï}`l&@€ @€ @€ @€êÂçêVZ @€ @€ @€ @€@ás›  @€ @€ @€ @ º€ð¹º•– @€ @€ @€ ÐG@øÜÆf @€ @€ @€¨. |®n¥% @€ @€ @€ô>÷±™ @€ @€ @€ª Ÿ«[iI€ @€ @€ @€}„Ï}`l&@€ @€ @€ @€êÂçêVZ @€ @€ @€ @€@ás›  @€ @€ @€ @ º€ð¹º•– @€ @€ @€ ÐG@øÜÆf @€ @€ @€¨. |®n¥% @€ @€ @€ô>÷±™ @€ @€ @€ª Ÿ«[iI€ @€ @€ @€}„Ï}`l&@€ @€ @€ @€êÂçêVZ @€ @€ @€ @€@ás›  @€ @€ @€ @ º€ð¹º•– @€ @€ @€ ÐG@øÜÆf @€ @€ @€¨. |®n¥% @€ @€ @€ô>÷±™ @€ @€ @€ª Ÿ«[iI€ @€ @€ @€}„Ï}`l&@€ @€ @€ @€êÂçêVZ @€ @€ @€ @€@ás›  @€ @€ @€ @ º€ð¹º•– @€ @€ @€ ÐG@øÜÆf @€ @€ @€¨. |®n¥% @€ @€ @€ô>÷±™ @€ @€ @€ª Ÿ«[iI€ @€ @€ @€}„Ï}`l&@€ @€ @€ @€êÂçêVZ @€ @€ @€ @€@ás›  @€ @€ @€ @ º€ð¹º•– @€ @€ @€ ÐG@øÜÆf @€ @€ @€¨. |®n¥% @€ @€ @€ô>÷±™ @€ @€ @€ª Ÿ«[iI€ @€ @€ @€}„Ï}`l&@€ @€ @€ @€êÂçêVZ @€ @€ @€ @€@ás›  @€ @€ @€ @ º€ð¹º•– @€ @€ @€ ÐG@øÜÆf @€ @€ @€¨. |®n¥% @€ @€ @€ô>÷±™ @€ @€ @€ª Ÿ«[iI€ @€ @€ @€}„Ï}`l&@€ @€ @€ @€êÂçêVZ @€ @€ @€ @€@ás›  @€ @€ @€ @ º€ð¹º•– @€ @€ @€ ÐG@øÜÆf @€ @€ @€¨. |®n¥% @€ @€ @€ô>÷±™ @€ @€ @€ª Ÿ«[iI€ @€ @€ @€}„Ï}`l&@€ @€ @€ @€êÂçêVZ @€ @€ @€ @€@‰>Ûm&@€ÀX ¬}ùŸ¼ÿ¤Å«cëÀÒvœX8–E*ŠÆJ NwRk¢ûУ±ûÈ#ïþ½ÝcUœb @€ @€ÆJ@øÄpnNš/8ŽyáÊ#@€NÎïkÏÉïhùËUÝŸ\³oÙî´qóö›w½kûßP¿  @€ @€,àšÏ ^S#@`æk.ÝòªÉ}˾’ ûÇ‚ç™÷a 0*ùëU«ó\&Lw¾ºzãæÿpÆÔÔâQ™»y @€ @€ŒŸ€ðyüÖTEŠX9µuùêWíH)íÈëŠDP4”,°( ýó{÷-»íÔK®úÇ%C¨ @€ @`xÂçáÙ™yX»aËYí}¿ˆ!¾jžºÔ U³»­øç«7nyå¨`Þ @€ @€£+ |ݵ3s²ÀšK6=½ÓÇòݧ!@€ œCÚ1¹ñª×ð @€ @€ 0Háó µE€À¼ ¬Y¿é´n«ýþÜéæµc @€ÑÈÿÎׯ¹tËúÑ/E @€ @€£" |••2OŽ˜šj§‰ÖÛóõ-Ï<ò  @€Ç&RJo?í²Mg!@€ @€ 0‰A b Ì·ÀäÞe?bxé|÷«? 0f+¦»×çš^’Ò˜Õ¦ÆTàô ['÷†´¶»«S ËSŠË[©Ûûüâ”ü‡¬ýí²[qOì¦}ùñ·b«ýÐt7=Ôšˆ÷/šxðŽ;wìØóívî @€! œyá¦E{Nn>½ N¦n\ÞÎïm¡Û]š§ÔÎïk§šZ ¡“ï?Òm…nè¶ŠíðP«îOÓÓ÷Þ÷¼3ï Û¶uµõ›4Y ¿§ îvë×hÞ^1Ü~þº•gNÁHUï|烹Ýò*mØæ¡=._1Àñ ÕG`Õ+·œÑšNŸÏO÷þ¡îF€œ@ ¥ðÚÝ;¯}Û šyšX9µuyû@÷»C§ûìØŠOÏó3òwdžš'ðÄü³p&òPŒáŽü!ÄWòWo¾cüBìtÿòÞÓ'>¶o?0ýë‚üƒÀÖ­ N½kú™V|v+†só{ÏÓó{ÐÓrƒuùgå?4œõ½^0}OþùjþbÖWZÝôåÔŠÓmÇÛî×5wκW; @€ù\ª. ¨W =~.ÿ^ð\/³Þ  @`Œró‹áÂm>´mzŒÊR #$°vÖ³º1õÎÂpAþ·üùa_§4çËÓÇ¿ “oÿ§wg¾nùˆéƒ_hþ®^‡ù2!H&ïîì7ß–·ÜRûCÝéo}äþ[Þþð| ª(C`Åe›V´;í cLç‡_îî<»ÛŠ {Gz=ö¾6ï½3œÞû‰)œŸòûgï­3œ&7nÞ•ï"ÄôÑÓGv/üæ§Â޽°Ú EÀ‘ÏŽ|Ê ïDƒ:òùDBå>¿ljóª…ûÂ7²À¢rTN€f.ú{õ}7^óÇ3ßÓ˜•@\µaËósÞûÊü)ü%ù’9çΪ—úwÊGA§æüæ|ôØŽ’[³~Óiibâê'7Â@Rº{×Îk_=±0ÈÚ—ÿðÉE'½gC aˆÔÝõðº—û’àèg8ä©—m}j§Û¹< ~qÞõEù§7ï–Âýù}í–œOïlíÙwó=ïۣ͛¤ @€À8 8òyœWWmÆP`Á¾øùÃ!Áó®­’ @ ^|Ôßä„Ïõ2ë@ñ+Öo~r{"lÉÌÿ¿ùßíO92Я½Ïx äï_š{i>zì-“·|$¦îÛ„‰?¼kçöo͸·Þ¡½0,Ê×ÍþÞ.ÁÔh…Û8ê÷;'Mœÿ¦ŒíësrÙןº+„/Žú:ãüáÜmÿP~+û¡n·óüf¿¥=¾1¬ÊÿyM>û5E ÜxÕMÝ®¿çuÈ-òñÒn @ ^ás½¾z'@`žòéŒ.÷ÏäyFÕ”"ðòU½f™ÓË–²Üê$0XÕ—^õ}±_Ÿp¾$<ŸÍC(Ï;}o>•é÷î7ç úúNjýÚ;·ýmm"@`€)¶Ÿ™OÒ?À;T¾î}ïìÂçÁ²w´•oþ®V;ütì†Þ–·q³Ÿ<9¿-ÿ`>ÉæÓsÿm~ŸûíxÒ¢í÷íøío6{ÚfG€£,ÐåÉ›;e œyá¦Eù¿5{§5r#@€f.ÐnM,¹`æ»ÙƒýòÙçŸOÆ?#ç ¹å¨ÏG¹"@ÿSíØùÛ57_Ÿžvt ¤@ÊG>ñ­¹—&côc—¶úÒ+ŸŸß×nl·Ãßä7´«r«Qž.ò©ùmú-iß¾;óÑÐoì]Úîè @€ù>χ¢>ˆÀ£ËÚ½o;åö@´ B€c);Ï˺E€ÀÀò‡Öÿ$8ÿá<ðÍùçùŸÀàœÈÇZþHþù| óô ['7´‘8$Ð ñ¼C÷Çñw¾6oïó·! ô¾d´úÒÍ7ÄÔúdžÆÆ!NeC/Ï!ô/,ܾ:¹aó¿.GxÇ Sc/˜ùœ„ÏÃz÷‚×<ÿrþÿ¹˜ÂåÃšÇÆ]žßÍÞôÍeíÏzéë¾Hs0,Œ¡€ðy UIÆU u»O×ÚÔE€"ÓiÇ Œ¥@]7NtÛ_ÈToË«•OǷ磾?´vÖ³ªí¢s¸pÛDÞÜ¿Œ~ΜŒì<+:_ƒ×Ïæàùßä̪“ñØé¬nê¾orã–ß_~ñÕ+Ç£$U @€À0„ÏÃÔ763ˆ1.Ñ @€G $ï¥G‚xD€@3¦¦ç°õ·sèzcn¿¶Ê>´¹ Ó_MnØòƒÔªDCX}Ê]½k®÷èq¾-]ué•ëÆ¹ÀFÕ¶uë‚Çvþ_y^ù:Èn ¤×,hïÿÌäe[.$B€æ" |ž‹ž} ¨@ qÜÿcs ž#@€bØ_`ÕJ&@`+Öo~òÞ}Ë>ž»¸zÝŒë®ËBL°fãUÿ-§Ò§òÓ/îÓÄæÇZ1†ÿ<¹aóöÎ~…ÌT@øèéÔ<ºƒŸûÓš@IDAT½dbçÖÉWn}Âè–`æš)Z±ˆ#‚S>×ù \½aËKClõN³}FãŒUß1ü@>ûÉû|¹j¬VU1ˆ€ðy Ì!@€ 0|Ò†? 3 @ é9x~UJéOBHŽvšÁbåàè»Ât磫^¹E°17M œP …"Ž|ÎçžÐBƒY Ln¼jcŒé}yçe³ê ì.hïë|ä´‹®\S6ƒê  @`&Âç™hiK€ @`„ò‘C·ŽðôMô> ÏÁóä¡ `¸qâ©­éô¡µ¯øçŽÇÕUÓ°Š8ò9„®ð¹†WØäÆÍç³xÜ»ö¾6{ßgM/h;zö~ö$@€@qÂçâ–\Á @€… Ü·ëÙgÜVhíÊ&@ ‚Àš WŸ? Gnêú ^ÇiòÔNçÀV]ôGØÉSª¬Ü°õI¹ÝÉUÚŽ~›x격ͫF¿ŽæTðøûÚŽ<#ïks^–ôœ|y‰[\zÎ: @€@Âç"–Y‘ @€¥ Äþ8lÛÖ-ÝAý[àÔ˶>5ĘOµN:v [g(ð¬Ö‚E¦¦Ú3ÜOsˆaº£ž+zÑ>§Þ>lùçtwò¯{Æcïk.!1'ÈÃvΗ—øž=û—½-oÊÿiáF€ú ŸûÛx† @€ÀØ´bgûØ£æU`ÍÔ-ív§oN!¬ž×ŽuvÑä¾S~³È\–r½çƒHù²E…í³eÏ| ‰SB§»ÓûÚñfólLáòÕ7¿q6ûÚ‡Ê>—³Ö*%@€(WàÆ{n¼þoÊ-_åO íÛÿßóALã!Íú¹ø³«7l¾lÖ»Û‘@ñeýmJ!žWü’Ï@¯ËÝ—±Îª$@€(U …_Ù½sûJ-_Ýôè¢ùè¥ßèßÂ3ó$°"Æ.}0O˜º)M •uÚíXÖiÆçûÕ¼æ’MO!½i¾ûÕßw ôNižÚík¿ó[ @€@Âg¯ @€c+?¼kÑÃoÛòF€Àœbk"Ÿn;,ŸS'v®&ÃúÕ—^õÚjµ"@ '°ljóªükMIù AO>cjjqI5Ïc­1µ†¡'ÏcŸº:ž@ï½íàTŽ×Ès P¢€ð¹ÄUW3 @€@ _œ8Й ;vtJ(VÌL`rãUóuž/™Ù^ZÏE ¦ð¦•S[…ýsA´oQ'í-ë¨çÇ7î;°Â©·gñJÏ_ðùá¼Û‹g±«]æ ¿0ñ¦å_½r]Ø•ÆP@ø<†‹ª$ @€â¾Øˆßw÷-o½¯x |‡ÀÓ.ú‰“rðükßñ„ 5 ÄSÛûºÛjD÷ÆF µŠ»ÞóÁµKtîØ,†é}±§÷Ÿ g˜#Ö,œØÿÜä”. |.ý ~ @€1ˆž8Ð}ñýïºæÎ1+L9Ì“Àƒ^»zÊÏð5ßÚßýWùKU§Îp7ÍçI ¥ð£k6n~ÚÏmÍúM§Å”~r»h:ÿí@;úyþ]õH€‘>ìÒ™8 @€ƒù3ÊpcéY»nºf›k<{U p<´wß?ÏÏŸv¼6ž«W ÿÑþ¡µ¶œUï(z'0±Ì#Ÿ[±ÌÓÏö›&ÚoÈûž<Ûýí7O1\±êÒ+Ÿ9O½é†F\@ø<â hú @€Å Ü“tþõÔŽçíºéÚËvÝtÝ‹•P8Õ¶n]bptX5­:[µ»1ýLè›À¨ œ¾aë’B‘§¨ÏGž.Ü61êk8ˆù¯¸lÓŠ<Îb,cœP ÆÐê}Àÿñ" @€ÀÜò‡ýñî¹w£ ÐG å![áÁâ¡›¾Ïk÷±{o¾î3¹uï¨g7TX}w÷rÃ3*5Ö¨VüÇûÊeS›ñá×Þ_ë@:'0¢bçì<õ8¢ÓŸë´¬Yúµ³î áËsíhÜ÷owÛWçõÜ…Ž)¼fõ%¯ýùÝ7ÿÞ72%Ó @€! Ÿ‡oXÆG _[è?ß·óÚëǧ"• @€Œ§@7H_j–Ó¸=yáÞôÚ<«_oÜÌLˆ@RHùô½åþ½êƉÞé‹…ÏÇ{-nÛÖŠyGïRnÍX[ _—§³­9S2†€ÓnCݘ @€ @`€½ë0Æ_8À! u"·œ¨‰ç ,Ð;ò¹Ü[+œ[nñÕ*_sÛÿ,·,òÔìÕ„†Õ*mSSían\h†€ð¹ë` @€ @ 6VŠWÕÖ¹Žg+pÞêK¯|þlw¶qH1öŽü-öÖJéœb‹¯Zx ¾ÀSÕj°íΘܷ¬÷Å7(X@ø\ðâ+ @€ŠÈç®WQéˆS˺ŒØš™î`òµc‹_;íø`¬Gq”•S[—g£‹Gqî…ÌùÕ…Ô©Lè# |îc3 @€5®zQ®cÝÔrO®á39IÿdÓ?œï:ÿ|.ÿ<œFõöê¯[:ª“7oµ\¸m"÷ûŒZú™NcÑáû‰–ibïô+r›…'j7ÏïÍsüRþùëÞûZÔ?þØýpgþÝÉ?£z»ìŒ©©Å£:yó&@€¹ ôþ1çF€ @€Œ©@ ñ•#XZïù? !½/¤ô¿WL/ý?_¹å7÷õ«£w\ko÷¹­Ð}A>]ïÆÜîyù'çÔ¿¾ú¶Ûÿéîþ¼ñ35A˜\ù§äØmÁ€†kê0§¬¾äµOÜ}óï}£©æ¼R —sü9Œý×)„­?‘îÿô½7]ÓûRÕ±oùºÉ«÷®|zŒÓÏ )^˜ßÑ6ä†kݸq[—îÙ»üåyV76nf&D€>„Ù  @€ @`H1]49ìAŸ;sØü–ü!ûµ»nºî‘Cb»ÝéóûÛÊO}ðñŸ_^{ñUO™žhýTLéuyÛ¢>»5bsL±wêXás#VÃ$š ¦»ÏŒ£ðÕ‘Ú±ôŽ~>åÜ;¢vï¾ÖËò—“Žz¦±÷穾5MÄߨýîk>_y–;vtò“¾Û÷~ÞÞ;KÆämw\ºágò{ä…•ûRÞ]ø<$à @`ØÂça¯€ñ  @€ @€@M§]¶éÌéîHœ¾µ› ÞÜÞ»ÿ÷¼ÿmΕãž÷\÷ÕÜÇë×nØòkÓ±û;1ÄïŸkŸõí×ç¾ÿ}}ýWë9jþàDˆ¿T­õ0Z¥ŸÊ£.ÆÈÇó‹ù‹tœç‡÷T·ûàðŸãÈ­ü7+L°8ÇbûïÞjÇóò³Ö¿E™ÏìÛ¿ü%9x^<Õ§ðÞv7ýÄãïIs›ò¶mÝüE¬¹““—nîý›9Ô~òÜ:­sïƒïmu oh°€ð¹Á‹cj @€ @`.Ó©ý}sÙ@û>ÐMéÕ÷ï¼îýó=Þ=;¯ù»ÜçE“·ü|+Þ8ßýÏSÏY~ñÕ+zÏï<0Oýͪ›ÿäú^X¹mV;`§¶lÊAKÓÂç/ìºéšÆš `Yj¢•¹¢çm:»àï4uÓ÷ÀEºù ?»kçu¿Ñ[Èù&ßuãµ;óûÆ­ Zû÷ñÓqÏ÷óÐ_zâê [ÏÙ½s{ïÈm7(L UX½Ê%@€ @€% \ÐðbN±{QÁóau§þ‡| å¶­Iwã‚Öô‹š4!s!0LÒ3‡9~SÆNÝÀáX‹CÓß×Ä Ï¿ž§?ïÁó!’Þ–v-zøy„?8´­i¿óõª/lڜ̇# |Œ³Q @€ @€À0šü!}7‡+¯Ü}ã[r½ãÝ;¯ý휼e‹pÂ1[Ýï=a ”#àˆßÞZÇpn9K^­Ò•S[{g?øîj­‡Ó*¦´õ¾¯¹a £çëBçúµy¬÷ d¼™’â…3ÝE{áóx¬£* @€ @€ÀkÖo:-õä#66èA>ùWvß|í@¯gºë´‰“è¿hÃcSIáù›“ ‚ÀªK¯\—‡=eC7qȵ+.Û´¢‰Öœâ¾îóòØ þ<;m¿oçu×Ô'Ð: 8býµŽ[e°zëåF€ 4øÍºÀÕP2 @€˜/Ï™¯®æ½Ÿn_¼èáÿ4ïýž¨ÃíÛ„Vëêܬ¶S¡žh }žÿî°m›ÏhúàØ\@jŸSPµ',uaZàÔÛ‡)Åž{ØÃ¦Ýýûî}oƤz§ànÅØÄKKœµljóªa˜“†+à?l†ëot @€ P—@cOMšO·ý wîØ±§®Â×ï®?¹æÓ9zþÃãµÂsKWú®g a\Ch”@Û©¦X”ºÂøÃER·±ás>Ýö¿½ÿ–·?|øty?Ÿêû½ù½í½ƒ³ÊXûbc׬Êüµ!@€Ù Ÿgçf/ @€ Ðh|Zëg5t‚wí^ôðuníø_‡:þ1o‡éóޱÙ&E ¤”„­‡­x ɑχyÄÿÑa›t÷ŽûyÒï{B1¤_öŽ¿ºM]³£§ê1Ì£€ðy1uE€ @€š#ÐÌ'‡âo ;vì¦ÓÁ£ŸCøô0çpôØ)ÆsÞæ1ò¢°õˆEÂøC[·.ÈwŸvèa³~§ß Ú6=ì9åëMßšçЬ÷¶}±jØ/ ã @`Âç! ’ @€Ô*ðØõƒÏ®uŒYvÞŽñ³Üu^wËaï óÚá;Ë¡näzͱ,»˜¡Àп4ó×3œpÝÍ…ñ ¯ºçÀÓóÝvÝà³è?uÒÄpÏæqĤcÃ.+1ôÿO¡ã F@ø<g£ @€ @€ ¬ºíö'æÁNØ€Õúú½7]ó™êÍklÙ 7ÖØûlºî+nŠX~ñÕ+sñk‡ ÏÌpó0Ç?ÆØgžyá¦EÇØ^ܦVŠ<ê9ÆðÉvnÿzS¤;ø‚×!|ªtïm‡0ü&@€@AÂç‚[© @€ P†ÀDj=©¡•~¤)óÚýîk>Ÿç²«)óÉóhêš5ˆÈTÆYà¤öþ¡åÛŽá–lÜisüÖÒ–³"ä‰)<¹Aëòí©t»áƒß~Ѐ;÷ßøÖ;ò4þ¶S94…5¾@qˆÂo”# |.g­UJ€ @€…tc3?¤O!~¼QKÂ'4Ÿ'„©©… š©¨@N=à¤ögsÑ·´ð Öm¹|(5õ}-ÆŸ` ‡ðtüÐí;ä#§,<³ï“ž @€±>å²*Š @€JHݰ®‰õ·bè;¹å0£Q×w]»wyïténŠÈáóyÃ-<ÝûÀŽí…¾0Üy=z<çè-e>n5ò}máÎ_5m=ò©À›qy‹C0­é3Ýõ›Ê>—±Îª$@€ @€‚b+L6±ÜöþN³B˜¾Ü$§évZÓ¤ù˜ ¤0ä5~ñ`½1~i uŸp°4ôÓ‘ŸpŠiÐÈ¿»î¾å­÷ ¤ü ’OÞ¨/zÅnË{Û ÖOSŒƒ€ðyVQ  @€ @à0|mÌ&~Ðûͦ}Hßí¯Æ6ô»­ù¥¡Ã˜@1œ;ÌBó‘׿Œ’7*|Η+r(?ÌU9bìµGÛ‡¶9u4ꈵnè Ÿ‡öj0ð0ΘšZœÇ?s˜sˆ!=vf†;z˜“9lì<¯³óõàÛ‡m*ônjÜßÇnJßhâbÜ?Ùº§Ióê†Ð¸/4ÉÇ\ 0ŽÂçq\U5 @€ @€@éK›CØÕ´9u:õ}>òyeӌ̇À öXqv'ÿ™Þ-¥ÖÁ#žS«Ó´£Y®Ù·ì¬áÉ4e丬)394b£¾Àth^aûöù~sÞs»ÝÆý›äÛVî @€@-ÂçZXuJ€ @€†*иéSHÍù üñ¥¹ÿ–·?<ÔU:zð”N>z“ÇJH4ÔSn÷Œ…Î÷ßøÖ;óÃo5É=ÿý,ûºÏÛ¶õ>Ã^Ô¤598—š>?õ@S¼Z1.oÊ\̃# |Œ³Q @€ @€À Nä`ÕÆŠjíÞêÑØoÀÖÏÆöñˆÝa‡ÏÝû~óÐ5àó• šu-ßÒ¯û¼â¯¾Ö¸/Tü?d+4õ}­7½Æ|¢ƒ÷¶ƒ/ÿC€r„Ï嬵J  @€ @ šx}Ð= åŸnʼòu1—4e.æA`9ívøüµ°cÇþÃj>x îÃõîÿeïNãªÊÆÿŸs'Û¤{“Ò²P¶‚ ‹ øB]PJ“tKD­-U_wE}_»þô}QÔÚ²¼jÒ¤m&- þ)*" âRTJY Xº@K›É6÷üŸ›Ò’¶I:Ë]Îù^ å,ÏùÜ´“ÜçžsänIÏ|vœ2;“—®±kõŒ½¿Kå#ÅŽCUfG$D –Éç°¤é@@@°Œ}ILÙ̵'¬áÓÄMÀñö|ŽòX7¸sI†ïõ|ð{Q<–x¦Eѯ-}V¸ý$/s>ښĸ|þscUÎç  €@¼H>Çûü= € € €C T õbįY¸xÄ"ûtï(C‚ež–€@ccB+irU’»{Ït6û<þ4D=3ˆE'0)=êÕ2¨òH¶o²9a×Ìg±[[?ÿHè@ÈR€äs–PC@@@‚&T›Ê P”Æq¢ŸÕköN6÷÷U¬· Û'ÒÙá¶y €Ø+@òÙÞsCd € € €ä'`T&¿ŠÖšhë4ޱ0FGž|6 w¯e··­úá ‚¹Ù*PmN°*‚A@† ù< /#€ € € [G½daì‡Y!!€@ÄŽV'ŸuzëÊ6 Áðð¯Eö’ì ÏÌçÈôé@r ùœ‹e@@@ˆƒ€Qý†9nâyµ0.BBŒ‰:ùl¼%¶e[ú½c^ßûÅŸ¥™ù¡?]#€ €@ö$Ÿ³·¢$ € € €q°qæ³ÒeÕÇÅ8@ $ˆg>eöZrû•Qëu¯<¶â3Ÿ­8  € ù| !ÞG@@@ vz›!;*s¢qD#P3ó’C¥ç1Ñô¾»W=ôòÚÎpIéÝõBÿóàñ³.z¯tˆ €ä(@ò9G0Š#€ € € `»€Vj«1ºZ¿ÎƸˆ ¢(~6¯zymã&†™••RN¦,z¯è†OÏ € ²˜ÄI˜ € € € ¥€ìUú¼’ ´m‡„ô&ÛbJèÌ[\]ž°!®þŒ¶ò¦lˆ¡8œ„>Qþ½ŠxpfÈåµ'öW=úby—+ÁY3y'á ì}oÄ`t €Œ(@òyDÞD@@@ ~F«Mæž=È7L>÷âQyóN[T7®¼q­-±¥'`"ß¾¿¿jÈÎÞvmOMý‚§äßÒ#m9/²?õ ¶ÄB € €ÀpÖܹ7\€¼Ž € € €¹ hežÉ­Fh¥ËÝdåÙ¡õFG `µ€Lz>>Ê%±¼eÛª¾0| CÏŠ¾|Àï¸*òd}À#¤y@(’ÏEp € € €ƒ´ÒO ~nÕccγ*‚A("M>Ë‚ß8x£†\’{Ä:A¾©Õ‰A6OÛ € €€$ŸýP¤ @@@,pMâ ‹ÂÙ+£ôlyÁÒUÁ÷ •'  ÀøY——æ§ØÅ›–- ÖXHë‘ß±r oyôyª ¤eE@| ùì$Í € € € `‹À–ª—X2¶Ä³wæÐIuóÏÜû5ž!€@© 8ýN¤³žwyë‘g6k3òÌèðOš³½<ÍÒÛá»Ó# €ä @ò9,Š"€ € €  ÖÖ^‰ó_¶Æj´¾ÄÖØˆ ÂH8Ú‚ä³;bò9ÓgÝÌgeŒ;-œ3D/ € €@~$Ÿós£ € € €v õO‹¼ ¶~þ‹ã#4X@–¼Ž<ùìöœ\~ñô©O CwÀ95o´sBN(Œ €„,@ò9dpºC@@@ mþJ?ùu2F(Ì~ÎÏŽZ‡€1QÏà55nõÈ{:77»²OýÈeÂ>Ñ»…=búC@˜ |ŽÙ #\@@@²x Ër‘ÓÆ|D57s]"}:EÀ}b”Q¥ž|ô¶k{ƒ£Œ]Ég fŒÈŒ÷@@ ´ø%¯´Ï?£G@@(RãöÿÉò¡]ûàÓM–ÇHx €À‘ç\V%Í@ÓY7éhõH6…]­Φ\ˆeŽS‰û£+@@ '’Ï9qQ@@@xlé¼é‰ô)Ë£ý¢:§¹Ìò |èí'MFz]ÒUz]VÃ2&«$uVmùS¨rrטH÷þ ƒV@@ X"ý!¯XQ € € €Vu·q ıµãž^0üÛ¼ƒÅ(à:úø¨Çå¸nVËi׺ä³Ê”E»dyÔçŽþ@@Àn’ÏvŸ¢C@@@ mîÊ¿rH5jžÔxåèz£°B@O‹:ŒŒv²ZN»¯¼ß¶™ÏJ¹_Ôçþ@@À^’Ïöž"C@@@  þ~ýË‚§ò·§çÓátE/ `‰@ä3ŸÝþìf4¿´ü¦-ʨ­–¸ „aŒŠÜÏ&bA@»H>Ûu>ˆ@@@ß^\½ôIi̾Y{ûŒP+uõ„ó¼fŸ—yŠE*`TäÉÓžOŸút¶¼ÚQÙímƒ–stôË–8ª.`vHÛìøˆ%ðÓ €€=eö„B$ € € € €@§´ù±Úõ³É²DB-QÍÍgÈ—ëgô…– 46&t96ʨ´Vësú·Æ ÜÄóæ(cÜ·ÉûÁáðØBÍKgZ!!€”ˆ3ŸKäD3L@@@ÒÐZ¯ˆÉÈß8éÏ®ŠI¬„‰y Ô7áH©Z™guªíJ&gß–Én‰îì,¸ä¸Ú9‹.¸@@`æs¨4‰ € €ñ?cÁeeê52›èUZé 2‚„Q¦GoQŽz¤·Üüm{ëR«ö|ŒŸ2G!°©bÛ=µ=c7Jß“£è?—>1ß”å·ï|aÕÒ‡r©GYˆ€ëöŸ¨”,¶í‘Ó2Úí:ùùÀ®£¿šôœ]A  €  Ég¾ @@@ TôAõ ÎpºX’ËïRFáA캾ëóžÇ²pE2µõ þ&åVÊr7oêXúh©Â1î˜ ´¶fLÃÂṁby•,¿ý‹ÃOÝÐÚšŽA¼„ˆ¹ —{Ÿkä8“Y;î#Ê$|¢°æŒvNÖÖ µ@@ÿXvÛSZD@@ËjæÏ–Dòƒ’S¾[²ÍïÛx>@Ø^.úµRþó’š^/õWLšuÅëP‡·°BÀ¸êÿ¬$» Nèîû½ìŠR â&`´ö’¦‘ÚI<’Kå;½ÎvÝ™–KÅ`ËFŸÄv|´Ž €1 ùÓGØ € € »À¤úGKÒøNmt»Ô.4qÜ`\÷AiïºÚúùcr†„'°5µä^I›ü3¼ î銚ºÜ €€uÚ¨ã£*íôä´ìöË+1<u܃ûׯ•åË9@@ûXvÛ¾sBD € à‡@KKbìX5®·WK$ÊÇe2fœã˜qÊÇ(]mŒªðºqU!Sªww©eW×Õ»–ùtL¿Þá½ç*ó’ãèmWm+O¸/îØ¡¶©¦¦ÞÝõøì˜Ô°ðݲŸì‰t´Ñz³¡? ‹u¿ã ™óç=ßyý_}l›¦ðW@ë¥2qïÛþ6\k²¼ýOjf-X·eÅÒûƒë…–@ o¯â(^Z~Ó–ÜÐ2[Úž{½ j訃í"€ €@ÌH>Çü> €¥&0¦½½&Sf^-û³¬Œ>D–¿=Øu͹@}¨X$_ÞŸãäkT_Ÿ¤ƒ$-äºîÀŸ’p–c×®ÞëÞ±ëµ]žËû»ß“ö÷¬­'¯*Y²TºU*“I¨dR¤Úºå¿Û¤ÙÍŽÒÏÈãçäñsÒã3J;¥ä3ª¿üétCÃit w)ÃÈ ÊOKâùv}´ëèßÉrÞs¶¬¼þ×öCÓä-PaœŸöêÌW¤ª¼ ·b¥vÕŠÚ9‹NÙܾX>_9@ î²RÈ!2ïgõÈù5 §YÏ»•ß–èß¾û¹2ñ¼‹Æn½íÖíÄB € €À’Ï{(x€ €¶Œ½½ebooÙI’>Z’½¯–ð1ÛQòõê~eÆÄ9Ê•ÿÈÿ÷$‹Ã€wñ¾J.`M6Ê ZöN^ÈjKô‰ŒJv¶÷êÎöG%ñõ¨Äû¸ÖúQ­ÝG3}‰uݳg?~Øôˆ@i ÔÔ/øŒü­üZ£#ËywÖÖ]þÎÍ©~Bt@NϦo–¿ÿ'æçT1Ú«þLÇ!u‹Î–ø»¢ …Þ@ PY„hZô÷dz3˜ó8´^¿ß«y4ãg]Qá-a~ŸŸmÒ € P¨ÉçB© €ù ´´TŒU~B&ãž$ Ù“${²\?If,²û¢Ô@Ž9ÿl©Y!‰ç$˜¼‰×2N¹n%s©Ë\UZ¶]–ÿ›¼·VæFÿÕ8îÚt¦b­jhxÉ–à‰8 Ô6,¸PþÊ…‘xÞÍT)+tÈÌ®Ó7w\Ÿ×ÌªÝ ñ'A$”þžl%§ä³Çpj¯Ê´ªÆÆzÕÚš Â…6@ $=ð3qH ÓVy%Ÿå§÷uÞÏñ6Ž«I>ÛtBˆ@H>ó€ €¡ T®l9Úq§Kúõ ™¯,ª×Ë’Øe»f.K 6´HìéHF=V¢9Ëû’|´Ð8*éô{Kz¯—g÷Jþ*£îí[³VMŸ.op €@¶’>N®/ζ¼åäïµþÅ‘ç\ö¦'îºÑ[žŸkžïXò·Úú+% k‚Ê&­fÔôŒ],›´.”âve²‰Ÿ2 0 à(3-êŸúµë>œÏéЮyĶ_XŒH>ç3ê € € |Œ–†@(q5kÊ’;7Ÿn”sŽÌè•d³z£|ÕîR)Å4ó®‘çð_o©ñcd†ôÅÞFÓÉ[»$!ýGIhýAÒwwue~£ššväÐE(5ù‡F{‰çQ üµ;Æ–]-}7GÔ?Ý"0¬€Üçô5ÙK9^Égü¥ž_[·`ÃæÔÒ/ ;8Þ@«$ñì­í¡ï&Ïœ§ödíƒO÷JÅŠœ+UÁ(oæ3 € `•Ég«NÁ € cæfgÔi¯=Ùu3o•ËÃoU;¶ž£”3Š4³oç´ZZ:[&{ím'LÊfÒm÷JbÿNW¹wv§Í=’Œö.†q €€Lª›ß(Uþ#Z sõ„ºE׿ZüT´qÐ;{ lY±ôþšúùwȲïÜû<Óêó5 Ó[V.ùF ¢%DØ_à¸ý_ ÷•ŠÊmy%ŸUs³«êxuO 7âzÓ$ŸGÐá-@ˆH€äsDðt‹ €@1ŒI¥jûtÏùÚèóe<Óe m™ÙLº9¤s›EGÏ”äÚ™Z9ÿLª´NµÿÎhs›¼ÖÑ=sîã!ÅA7Ø( -ô[°0o¥ã¸Ÿ ÿ´‰˜J[À§YVÒˆ_òYN›6æë5õ ·méXòÃÒ>‹Œx Lh\4Nõd‰8ê§6´¶¦óŽA«õòó…=Ég¥^­+Tk+7¡æ}R©ˆ €~ È"Ž € €@ö©eÓ’©¶OÉ×ïúUïFI<ß(µåëå%µ³o‹’¾ $2çÊŰkd6ôcÕíO¦Ú¿žìl=Cfið3Ÿ¯Ô4f»€ìõ|ެðâ”$ÙüIW޶!b@`°ÀÖÔ’{å3#5øµ8=ÖÊ\WÛ°`aœb&VJ]@w»Ñ/í% 8ä6ÛuT¢ª3¹wTä³Éƒm"€ €@|˜ùßsGä €„&L-?]–{~·|ÕK§G‡Ö1å-`ŒñöÓ;Açêä)'=/³¢;µ£ZwVOøµš>½?ˆ@ $ñ|±¶g†QnOï,a»%t„XjeÎ'TÆ}— »<–C7j±Ì€.gt,ÏA— @B¹ÓdÏçHG.7…=\XZ’ϲÎE‡kÓ$œµ…D( € PâÌ‚)ño† €Àp£;–(³g¿"3œSʽO.²|LÊ’xÌî×’YÑó]×ܖܱõÙªÔ²Vu¶¾…ÑvŸ4¢Ë_@k=#ÿÚþ×” Ý3ýo•(\`óòŸ<"9”ïÞRd-È}&æ:I@ ²è²0ÚñnŽŒøÐÍ|V®[XýFoØ÷9UšD@B˜ù\ˆu@(2ªÎ¶WK’¤If$\”Q²d­Lä(:IZé÷+£ß/3¢7¨T{‹vÝ[ºæý¹èFÊ€JR@–Üö–žœlÕàµ:ÛªxAý‰Ì—ÊÜÄÅòRl·Ïx9­˜=èÄòŒ™ñÄgO¥ e³ËUÙýJ~S²é0*úåÌmò @ˆ\€™Ï‘Ÿ@@ bÕ«+evó…²?ðo¯`¥ô×e1<+öJX¦º?Ì›Ñný |Î ydç˜<ª^¥¼×µ2®ÀN±ØÜ±ä'²üö]±v„ åC뛓ê|m„"¼… }Þ‡*¥ëWEÔýînû6Vm{j÷“¼ÿ4ꑼëSñ8¶Ó –V@@ ?’Ïù¹Q @x ´´$«:—Í—Y®÷dŒY+ çË@&Äs0D@Rió^­Í]ÉÎöG$ýQµú–±õE³ø.àcé÷«ãû`iÿŒlž|…Ì€Nû×d4-Ɇ!Ÿ©­_pôÎ TÑœzE`Híåio[Œ¨¯C®W­­…¯™í¶t÷@…½X5ùOYXÔF@üˆú‡>ÿFBK € 0¬À¨U-SªSm_N&Oi£—JÁ7[˜7xEàhID'™I>“ìhûßÊ•-G¿ò°SÀUªÚÊÈ\=Úʸ —6u,}T)÷¿Šäµu nQ‹•Éx±0Æù ´O3–^ùXö “Ð,½½ O@@ :’ÏÑÙÓ3 € ŒZµüu’tþ©ë&ž’™@ÞåÚÀ;¥ƒb-óÇ>ä8‰G’©e©ªTëÛX’»O3cBRØ\ùÒ÷dôŠÂA« kŸË´Öؘ,Šñ0b.`´ù~ϲ½€/ËeËóÛw:tôÉ}ûPˆ@" ù<Ý"€ ˜€ìÓ[ZV'û9¯q]÷Ï’t¾DúbæO`à%Õ°,aªgjåüZ–äþ‹|]Àþr%uþ,»€,Gë¸ú"æö¢ªVuÝ=cWM<ï"K—ã/ e@vÆDž•d×eìÈ¥´k|IbÜKnïÊÌòs«Ai@@ 8’ÏÁÙÒ2 €á 47;²—óìdgÛƒFéÙÏùœp ·8Yf?ÿ,yÊIWu´]¦ø178”Ø7ÃEâؘZò/mŒìÿ\4Çt§¼ò×Sλ|RÑŒˆ G­"O>+íÏrÙeºÌºe·e•"oOm@@À ’ÏVœ‚@@ /é,3P«O9é!i¥]f¦¾®€Ö¨Š@®ÇÈ­7$Ÿ«}¬ºsÙTK Ë›æ*Hy@À2M©ë[dyÚ%–…U@8ú´þrçw.ŸZ@#TE|ä÷©yò9Ñ›ñe¹ìgS‹»d<ò增V:úe̓m"€ €@,H>Çò´4 €"°fMYUªý’¤³Ì@•åµÇ¦ÊŠï×%“‰Ç«;Ú®”$tE„±Ð5 €@‰žÞH/°›ªç˜Äjf/äç%›Î ±”„Àä?>u¤ ´2âÁnû÷m7lò/ë–ÞPýÂÉþ–@@üH>çoGM@"=g%wl]«•ù)IçÈN-0ÅhõIBÿCnޏˆ=¡‡FâU@Àv¿¼yg"cê%Îl5ûøÌ¡:c~[Ópù³¯CI(T ã$,˜•ëw²Xû²t¡¶ƒë»7#öà1 €D'@ò9:{zF@ gäʶ3eOç»eOçåR9ò¥ërJIà(¹9âÙúÏÕí3KiàŒ(«®Ü5æ[,c’qÔjãÜ9±~á;ŠhL Ë\ ~oñ9YlüNf~ Kò¹pEZ@@?H>û¡H €,P‘Z6-™j[®u·ìÁxfÀÝÑ<~ œl´Iy7M$SËO÷³aÚB^`kêú_Ê*+Ÿ ¾§P{¨v”Y5©n~S¨½Ò%*`”±`æ³~ÄO~í8¾¶çKlšä³/Ž4‚ € |.˜@N`L*U[ÕÑöã„ÒI/³‚뉖X`ঠ÷¾êTûÕ··po4 à£À–Ž¥ßÒJßäc“64Un´þYmýü÷Û 1 PÌŽ£#Ÿù¬ëë2ÙZ;¾¶çÇù׆䳎´ €… |.Ü@ð_ ¥%!Iºö«ÞõZ«EÒAÂÿNhðdæË¥¦O=’L-»Z­^]~ôˆ ‡€ÙT¹í ¥Õÿ—G]›«È5ýÚ†…WÛ$±!wcTô3Ÿ _g*?_þÂr^úì:7&ò$¿]Dƒ €Q |ŽJž~@F``_ç¤ó€$é¾/EÆSŒ—ˆ³Àh¹Øÿõd&ýwÙº!Î!v@ dZ[{3‰¹rSœ·KQÚ˜¯OjXð ”.ª1,˜4ã²)Ƹ¨Cqºº×ûCkkF)ó˜¯mÞØaÏ»hláÍÐ € P˜ÉçÂü¨ €€o£V®œ,û:ß4°¯³Ò¯ó­aBÀ^£d?è2 :•Lµno˜D† à ¼Ðºx[F¹3$GûL±‰ÈÌÌO×Ö/¼NÆEºØN.ã‰TÀ”9ÇGÀ®Î7lüåÍ;ýÃß}¤ýˆO—UçG;´ €"@ò¹=ê"€ à‡€1ºº£í}Æéó–‚»Ø&ix 虲²ü?$ ýa%KÎÇ+v¢EJK`ëÊžNèþódÔ›‹oäæý’€¾Y56òYT|'—E$`´ý’ÛJù;ëy·¥V¾.å½»Ù‚þÔ® Þ Ê € ’Ïñ?‡Œ@ Æ•©eÇ$;—ßi´ú‘Qš%Òb|. ½`Q2Ùì»ÉªÄ½£V-gæÁœ4€'°qåkvåÆ!µ=¸^¢jÙ\TÛ=ö&µhQyTÐ/E&`ÁL\½.S횇ƒh·6­l˜i^Ȩ‹ €E @ò¹N"C@ˆ¡€ÌîLv¶}ÜQú¯²WØ91!#Œ€V§º®û@2ÕþÿÔšª‚é„V@ زò†ûdñ–ÙrãPºÐ¶¬«¯Õ…µÏeÚdt…u±1ÐÊD?W›`f(kÌŒêαl!0­€êTE@| ùì # € ½ÀèÎö×$“‰?(£¾-µ’Ùפ$%# ËšO&wŒûKõªe§–̨( 3-©%w*å^ a÷Å,ô‡«UÌ€^FúÀT”@`D££O†š`–ÇÖ*˜Õ#zøÍè“ýŽ‘ € Pä$Ÿ‹ü3<@‹š›ÙÓöêŒ1’¨N³(2BAÀRsœqõ½Õ©¶fµfM™¥A PÒ›;®ï†æ  è’þN`ðì/0ñ¼‹d[!sèþï„ûŠã$YûùŽ%e$¶m?ðjnš ÷û‹Þ@@`.âíoÂ+ €ø.\±bªJ¸7±Ä¶ï´C5è-ÿ¹A¾6*cž×Zo“ý´·Ë’/Êò ÛŒãn“÷z£vì®ìÊ{»ËRècä±Ì¼•¹\Z–ÿ”Ic6ã´Ñãµü)ËÙÓJ—"‡¥¦ÈŸ¼ò$Äø É[gfRËÞÛ[7/‹‡DN£ €@‰x èÚúù’€ÖËdÈŵWòË3 776ÎS­­½%rJ&¾èʪiÊõ¥©Bé{þÅCž,¤Ôõ–Þ>åeÂ|;1¡kì±/(õP˜Ò € 0X€äó` #€ €@²cY“Ò™KÓ^²’£p­²dùãJ«G%)ü˜kÔcJeþe\ó\OzF55íI*ÞU–-ÈÞÄU;Ç¢•;E’ÕGGí}”Qæ(iáùšœeK^à”„Ò‘Õ>ž9÷•Ü0|QÞA[ ØÐ5=cnÝÒØx$ 3aÛÒqp2æ£å–ÍhÇÕ]ÍýÁ… åÆHcSòY•;æ8/ÉçàN:-#€ €ÀH>ˆ·@È[`åÊ1ÕNÿ÷%CvIÞm”vE/‰ü$p×J’y­,¿ü·ŠŠþµÛßÕ´Õ:–é—ww+Iˆïúºg¿øî¸iTuõñ’(?Yçdyÿµr‘ê$ù³f¿²¼0’@¥Ìªûn²³ííå··\jå÷ÂHÑó PäÅœ€–½]çÕôŒ]¼E©…r¹ªÈ¿—žOŽì÷ýß–u>f˜f\™ùy‚}¯Ø\GŸ(/´íõ"O@@H>‡ˆMW €¥#Pݹücú[äZË«KgÔÔ›Eôùº_–´þƒIèûzfÌ^¿ïìVIðÆóxç%;»”z@‚÷¾öÉTË¡Z9§É²ào–‹VòeN•7“{ ð`=³¯/ñ·dªý‚tÝœ»‡)ÄË €x 蚆ù3äf+o/è¢úL“ôÒüÚºÛ6§–~,ZºD vòsýñÑm¼e±C’Ño“×Ζ/ö“ÀÙï?‡JRã.Y†û¿dîoî{£Â~¥y@ 4-+¯ÿuMÝ™²Ú®$ Õ¨Ð:£#­>Z[¿ð…ÍK¾Fwô@ÌNˆ<~í-‹Ü!7¥¬‹~r÷>ãÓ*z÷}Bâ) €”–ÉçÒ:ߌ@ H––du²ì‡’8¼4ÈnbÚ¶+q? ËgÿÊuÕÝc¶Ý£d©ê˜Ž%¸°§Oï— üƒÒ÷õ}ÕÒ’¨å¼Þ¸êí²ÜçÛäÂÖYòzUpÄ®å„$é¿^•j?»<•ºø¥ººÍ±#€E*°%µäΚ†Ëß&3 o“!ÙTæK“ê<³©céõEzú… 46V¨õªÂ*°×[;À£²r½êñéÐÇ©æfG¾¼ßÁ8@@ÐH>‡NN‡ €Å(P¹²åhÇI´IâÙÛÏ—c—ÀNe̲¤tª¬,³jÇŒ¦MÀä(ÐÔ”´\÷7”·wtoõ;d–oƒ,c8CZ;(Ç‹²¸Ì¬{W¿ê}P–»ŸÝ5söŸŠr ˆ¡À–•7Ü7¹á²³3&ñK J ‡0lÈrCØ'ÖÍß°5u½76ØG`rï¨ãd_¹Q0ÚCgL Ëboj½nGmý‚ge”‡D;ÒÁ½›ä”¿Qdc+s´n=hæü×Ù¸¾¸&1Í—† kdû¦Õ7J6àèGî!çæ3¦Œ¥·sV£ €ø%@òÙ/IÚA(=ctUgÛg6+dvïØÒØ5b­ÌvYú§ çtæàî™sß¿óü9·«3l[®¸N‘,£—žÙxOº~îÕé™sŽ–$ô›e êïÉ Ÿ+®æ4oIò[%ýmoÉòœjR@ 0YžúQS¦ß"ü=°N¢ix¬ëèÎÚ9‹ަ{zEÀ^¹!õøè£Ó†ƒ6Ö%Ÿå¦L’ÿ¡ðÓ  €Ø'@òÙ¾sBD €qX½º2™Z~“6ê«q7€û¤Í6YþynW¢ú ®º9— $œe™èú¢É hm$ }oºnÎGdFôar_ÄÛd)꥚¬Ú]’ÇÇ««·½½ebIŽžA#€ lm_²¡'Ñw¶„vŸ…áÒaº?Ó®¼ým9@àcAòÙ˜‡_ (ÐG.í_ä3Ÿóƒ£ €ø @òÙDš@(-Q+WNNfÒk”6ï-­‘Œöeô'}ú°tÝÜy2ã¶Ζ}ÈŒèîú9w,ÍH,Iè÷I„´,ÊÀÑ=±ßÑ×—¸¿"µŒYkÓ ÀKËoÚRao5ÊÜ‘]x”’%ÅßTÛ3öûñˆ–(M ú™ÏaÍHÖz}hªYwdø8k+ "€ €€ß$Ÿý¥=@¢•j­ëôß/ƒ”%ŽKæè–„ó-F»ÿ!Éæiéú9ßÞ1gÎó%3ú8tÆ{·Kz±Ü(pºãÙ“Òü@†³#ÎCÊ1ö£JßSÕÙê-õÊ `À³©Å][*_ª—P~nA8~†pEmÃï†/hn–ëúØÈ!ŒÊrØ:“ k†u.¤Ì|ÎE‹² € à«Ég_9i @ ˜ª;–Ÿç*s·Œñðbç ±m”™<ÿHd—„óÅÝ3'ËlËKqØÙ0ïoéºyWUUe“› >¦´z"ŽãÈ#æ Ú8¿–} ßG]ª €!ÐÚÚ»ù S/ÒJ×la£®­©»â´ Èh8 LþÓ†#ä¦Çdô1gBY{ÓŽ#ÿ%cí~¼{E0aòì÷´×+,÷r2ØQ!'»ü ?Ç[pÀ㤠€ŸÉçâ;§Œ@ Tªz`F¤Ru…4cg]³V+s¾Ìl}kWýÜ?Ú#QÙ$àÍ„–™ñçíþ‡Òê÷6ÅæS,§÷©Þ5£W·Lò©=šA(P`KjÉò¹ó™9¼¥À¦"¯nŒ:wRÝüË"„Qà ú…“¥» !v9tW:ä™Ï®k]òY`¦Nj¼rôÐ@¼Š €'@ò98[ZFˆ™À„_µŒKšÞÛ$ìé1 ý@áný¾.Mÿé¡×uÕÍ+ÖY¬2àýºg6þ.=sîYÒÄIB?Q@SÖU•äÆkÜLâ7ÉÕm‡Y!€%*°eå ÷9:#?égâN`´þΤ—M‰û8ˆl\GY0ëY¢uœP—Á–•ž•^wdëV9ÓÓÍìç°°é@ö|ÞCÁ@RðÏÝ݉_Jbí?ŠÈ¡Gk_N§3Çv×͹I57»E46†€Ìš_žµýx£Õç¤û„H—²$áñ*£î"/"€y l\y£·bË9Rù±¼°§ÒS–¸Æžpˆ`ŒkEò¹ó$ý¯`G:TëÚºÙÏòs;û>uªx @ ù(/#€ÄA`OâY©Óão–1®rΉ]us?¯ššÒYÖ¡˜~yw÷̹_S*sœ2ú–WˆM‰£H@Çæ\(”ˆÀ¦Ž¥ºeúnÜÐÔpÅ%rÚf© h+’ÏO¨Å‹ûÂ?!/õÅ£™ùœ…E@@À_’ÏþzÒ €@Üdçît¢CÂ.–ÄóãZ«™2CufÏŒÙq¿P·ï¦’Š7]×ôLº~ÎÅÆxI겆B$ «oo;8À>h@ ­íK6CÚ5™ïʰeQŠ[@’ÏæáH”²oæ³bæs$ß tŠ €@‰ |.ño†”´ÀêÕ•IÕÛY$Kmgäzæ·Ò£·ŸØ5s>¯ >Tîú9¿I'ª^«µþ’tÜjçÁtv”Û§~9zuˤ`š§U@\Š#­O«i˜q®c§<ñ0ÇE³Ñë#‰AÛ7óY˜ùÉ7"€ PÚ$ŸKûü3z@ tZZ*’™t«L?‚Y«ó¦tÝœO)Y9þãa±˜1£§kæœ/d2™×ËÍ¿]üû,ÓÒ^“É$îð–äßç-ž"€D$P hmô×klLFDH·.P[?Œt25ðŽÔ£¢Y•Çq¢™q=²ÇÑjÑ¢ò‘‹ð. € à¯Ég=i @ ²Np²ºìF µ.áŽc¯1æ 郷œÒuþ¼F(Ç[„"Ð;«ééóç¼E)óé0î7B¼¾»;q›’¥ùCÁ£@ x èþ~õ6Yíå™¶³À!ÝÝcÞgghD…@áÆ$ì˜ek¢™ìö¤£™q=ò©KLÜØwÌÈEx@ðW€ä³¿ž´† dg۵ʘ÷Ä Ô‘Bü›ã$Ní®Ÿ÷%uêûúF*È{„* µI×Íû^F™×K¿ µoÿ;{s•êY¦d¥ÿ›¦E@|^\½ôI­Ì9R÷¹|êG^G;ŸP|®D~" 's| íæØ¨qû#Ù{yëm·n—Pÿc¸×¶ÜøHé@l ùlË™ @Pª:–}^fË|0”΂êĨï¦ÉÓwž?kmP]Ð.… ôÖÍ{8=zâÞì|i«¿Ðö¢ª¯•>/™LÜ 7¬ÈjÜ €6lêXú¨ãšó$/ѳÃZÛ3f~Ì‚&\²p\eCòyǖΛ¢\!’Ä÷H'ÈQæ„‘Þç=@@Ào’Ï~‹Ò €€µUË.ÕZÑÚØs®«ÏM×Ïý¨’=v\œD,0}z¿7;_ö$³DòxÄÑÒý…ÉÎöoÒu@üx¾óú¿Ê6³¤Õ®£?Ëìg¿hÍ£µ ÉçG£ÕÐÖ-½m”¶c9ôhO ½#€ €@ˆ$ŸCĦ+@èª:ÚߪþItÜsªLUœÜÓ0çW·D„,àíIžvË^'Ý.¹k?»ûx2µìÃ~6H[ €… lî¸~6æ½…µIí©5ÝcßIÏtŠ@°Ñ'ŸZìк1 Do3ó9tºD@ ¤ÊJzô @ $Fw,;ÑÕîr¹ã»<†v%æÏ¤gÎù–’½tc?!#°K ¡á%ݱü'F»³ãK¢¿“Lµ=•®›ç$z|ù‰B`Sêú–šº¯ÒZ}cˆ·­}IâýO îfk$0rðö2ïQGåZ-€ò‘&ŸåwÎõ²/}Ã*¨É㤶·…Œu4**(P[·àçÊQo±PXoºêÞÍ©¥„Õý €D/@ò9ús@ € T¯\yHF÷ß&]Œ °› šÞh”{Aw]ã]Au@»„* ]oùí8ÞªA·T¯\vVWü?Çy ÄŽ“À–ÔÒoÊEö“%µraŒÆuêĺ…oÚšZroŒb&T†˜Ø=þ¥ÝİBzC–þŽxÙkG’ß™F›u7ÕSf]vÄ¿WÜøDÖ5(­§(cްb Z?aE šËn‡FMG €¡ ÜqÓ(ãô­’~§†ÞwáÞ-ó´_Oâ¹pHZ°G@f‚œaO4yGRm½rÔª–)y·@E@ߪª¶/”Fð½á”Em®°yšF T2•±b_a­ÜH—½Þr°zLà­Ë>gÜ„ç'ÔoJ:C@ÈH>GFOÇ € £“½£nÕż}fcv˜¤GOœÞõ®¹ÏÅ,pÂE`x–– Y鯒ÏÞ§ºnb…ZsCÕðæ@06´¶¦Û7KúÜf¿…ô%kà6Mj¼rt!mP[\GŸhC,™Ê²hg>/^Ü'OØ`18£UôûqˆÇ € PÔ$Ÿ‹úô28@ tªVµFFß3ŒìêüÁtݼ«Ôôéý1‹pQ ™,;] $G,¯7ߘÜ1v©,eçíŸÇ `À–ΛžÑZ_jA(Ù†0Úíî™maÊ!`µ€«¼}…£>6¾Ðºx[ÔAÈÎÊ‘î;=äø]u¯ó" € €ÉçPi@ ZêÔ²’Äýr´QäÜû‹’ÂzWWýÜër®Ib `”9'aæâ…/ßè’k=Ê#€$°iå’ÕÒôwjÞÿfµ¹ÈÿFiì˜YûH#ß¿K­íˆcpdÚŠ›GÄc@(b’ÏE|r PŠ•«Ú•}eÿOƧϸG3ʼ¹{æÜ_—â9cÌ¥! •>§G*7º|eÔªöwãØ WÍ•Ûeý`â—ÏÇ·m\01±##ÈJ0:ò=…RÑ.¹ý2Äa_òÙ(+–Eá{ˆ·@@ ˆâta¾ˆØ  €@ «o›pÍ i{\ íÓè}eªâͽuó¦yZEÀâÚïy_PíºæÖdªåð}ßà9 €@D­­½™Œñ–ߎÃ6&eå½ffDRt‹€/Sf]v„R&òíU´2vüNeã²ÛZMœrÞå“|9á4‚ €@€äó€x@ >ÉLòz¹Ëüø¸DlŒº=­*ÞúR]Ýæ¸ÄLœä#PU­ß(õ"¿ ™OìYÖ‘k‰ej Éže Š!€*ðª¥ÉÏ…ß´ŸwŒ3˧¦hH2ÇŠßÁŒq¬˜qlœŒ3°÷ýfÈ”Ûqžö‹ç € P|$Ÿ‹ïœ2"@ $’©eWÉÀçÆeð².ÝMÝÝ™UW×—˜‰|ä¢ú;ò­£z§%«×Ä(^BEŠ^`B_õWdël¨Qæíêœæ2Ûã$>†0vì÷¬T™Ë]o]yÃÓbeÝïy²=UäK£÷=Äë € P\$Ÿ‹ë|2@ $F­Zþ:Ùcìâ3xý®™s.SMM½ñ‰™HÈ_@fž•ÆžÈF]™Lµ]˜¿5@üxô¶k{ŒÑWúÙf@m™4nÛj›f^Àhf>g¶”m{,øÁf݃³°G+7ºœ0ø9@@ H>%K» €á¬\9ÆuÝVé¬"œ ëEf|.]7çãJkÉÇq PücÚÛkd”§ÿH÷ŒpqEg» `÷Ä@ ”¶¤–Ü©ŒZm»lÇò6Ûc$>†ÐʆµO*Ùï}ØÃÃÂ¥· ç)ü3A € ºÉçÐÉé@ÀO¤Óÿiïh?Û ®-ó™î™s¿\û´Œ€}ýe¬4_2Ǩ„qÿO­^]Y2#f  €€å 'sµ„hùî[,g$<F8q¤7CzϪ%öå«âñÎVVÌPéÛn@@ JöŠRŸ¾@ ¨Nµ_!K‡½» FB«l>“®›÷к£#,ÐÚy§ü=µ$š°ÂЯKfº¿œVêSaõH? €à l\yãÚIõ o–Ï£K†/õ;úMû>ßÕÜu$ô@.Sλ|’|ÓNÈ¥N@e¯©_ðÝ€ÚιY­ÌIÞyø¤Æ+Goj½nG΢ € ƒÉg£¶çàEQ@K*SËŽ‘ ˆ×XÎÈahõŸé™ó®¹ï"P„ÍÍŽü=-ýž÷;}æUí·w×Ϲs¿·x@ óeéô½òeë pÕµcžÊúøµúDzæ\ÏÖŸ( B yÊÉgH»“ƒh;mjÙÚý¦q6ÌŠ!"€Á lêXú¨ôÐl/¶®õ)¶@uÂÐ,å>zþ=åX” ÏÔD@»ÂM>kõ”uFÛ“uH„Ø%L:Ÿ”ˆÞlWTûG#÷ºYÏÿ³ÿ;¼‚@ÉÌ.™‘=ÐC{Mχ~‹W@Â ß »Ï\ú“»–^—KyÊ"`ƒ€Qzš qC–ç+K)Š!€ €@a/»½Nbµê;G›‡ ð£* €@È£Rí¯u•ùbÈÝæÑþ^WÝœÏçQ‘*‘€™UDƒÉw(Umw×Ͻ9ߨ‡ä*P[¿àwRgL®õ‚)onÝÜqý·‚i;·VŸïXú{±ù›Ô:9·šá”6FɱÄKÀÑêxùÞ刉€ì‰s|LB%L@ˆ±@¨Ég™ö{ùytŽM^®«oS<Ä‚ 0‚ÀêÕ•n&}“”¨¡TäoÉçÝ ]3g4ò@^¾QäÕ†`M×Z«ïŽZÕò«ç7ýÛš Š]ÀKb޳a2+ò.âØƒ17(­¯ÙóÜ¢ò3¤-{çZ¤B(¶ Hâ™d¦í'iP|F¹œ¯AWó‘aÞãe@\­î¡›œ»0Æ”s%* …À¢EÞtGEÑ5}*00óYÚ õ@@á"I>{ÁœuÈħdUI(˜îá‚ àõç2®®ó–þ mšDðS`Í UFëkülÒ×¶ŒúmúàÍöµMC æÕ+—½^iõv ÃÜ,qx+'XzèÏW¯\yˆ¥Á PôÚ˜µ6ÒÑz¬qû LÜØwŒ¼V¶ïë<…À¨ u‹¦Æ"R‚D@X D–|ö´Î:|âï´«æÉÃ0’ÁÏ9Ú¼óì#Çÿ+–gŠ @¨Ú9öcʨ#-öÇ”5©Sß'û=s €ÀÇ±æ† ãè¶t:óq‰í¾=ñÙõ`´qú¿eWHDƒ”Ž@™“ñö}¶îpm]P„ÀÚ$¦ ñ2/ÅD Ü1,™“sE˜ €ÄQ Òä³væ‘W9®ûvy¸1@À‡dÆó™gLhåÍŽ›¦@X $S-‡Êž¨Ÿµ4xI8;sw64ù¹eéÐ áF··dÔÀ¶*à ïMÝ;3¿WMM½*“h”n·„×uN=]XµªÍ’™â9ÅMa@ öÿ.ßù´ "cÛ@´6Ì|¶í¤ÏŽ2' ù/ÆBÀu3$Ÿcq¦@x Dž|öØÎ8²æž¾Lßë”Ò·ùÌh¤Í9zÇéÌxöY–æ@ LÙ7¤ùQAv‘wÛZ}*]7ÛÊ=óðAÀ-7WI3>4UpZ«I<$Ò³f=í*÷BiÔ-¸áU€®UkÖ°de¶4‰Œ(ÐÚê}N<3b™(Þ4*E·ô‰@®Fif>çŠfSy­H>Ût>ˆ@"°"ùì™NÕAÿ>ëðñ3´ÖóäØúàü{í¸gJ›`g4iI ÙÑö&¥Í{Cê.ÇnLgúü9ß˱Å(zñË——;þþÓ–ÊÏ“ËÇÒS×øK‰ïKƒ_³èñÉU/m½Ô¢x(!cãJ6cJè0ÔX 0ó9Ö§Oi’Ïñ>D €€ÕÖ$Ÿw+9u|ÛYSǽÆKB+­VÉëý»ßËâϲ?è-Z©³Ï:|ÂYgVó‡,êP@Àc´üÛokr÷éòr÷R%k!ÚÂEØ"Ð[æ~DbgI<ÏïÜÙÿ«}céþÓÚ/ËŠ8wíûº Ïe¦ö—TKKÒ†Xˆ(%ù¡nk)—±"ࣀ\zSÇúØM…/@ò9|szD@ d¬\âOÏÞ²ˆmÞ×m—.So‘×N— sÓŒQ“åõq’œp%ѼM^{Z³ÎÕú÷ýý/Þ;ýU¯ê.™³Ç@@"Hv¶Ï•!ná°dYFýîíïj⥅'‡¢½ºeR&£>mƒ{7?ß½äöàWUs³«V¬¸D%2“×Çïõ^ôOI&§•ò¶à@I@îzäúAHÖtS\ãg,8\Fdç6IÅEähjÇ̾¤æ¥å7m ²ÚF@Ò°2ù<øTœzÔÄmò¼óå¯Áoñ@ ˜¼=Owl•™‰örÔW»fÎa5 ûN Y É8_0,Z"4qëp,ÞþÏÉTÛûåýŸW&º×õÕcÚÛòÒœ9\Œî$Ð3”˜€$Ÿ·¹«r(/SÇó7'73KW¹'¼¤ÔïlŒ˜@@ ÞÖ-»oN¢GÈW jç–K¤î´|ëXïÁ®)›¾`û4@lFw¶¿FVð’¹Vrô¡tÝìûG &]7÷²NäM#•‰è½qýåê³õM· €@‰ ÈÚjö’ â@ÀncŒ¿·ÙfatƸœG Ï‹!‹&¹|¶ùwbi ˆ…ÉçXœ&‚DŠ\`Í UÚèf GÙ“Éd.V§¾¯ÏÂØ hdöŒk~ A$¢ ä•Þåß‘ë_y6ü£®DúC²…Ë׈êsUÕª¶#¢ê~@ ¨ºlÜðSmK,ûÆ!{yÙ·l°V²Ý – h}‚å^6FÇfßg£«¬ù\Û‹V«ò½žÛõd´=áh>Ûì9D‚„"@ò9f:AI ¹s¬7srêHe"yO«ÏõÎjúG$}Ó)– Tw._( Üÿ°(ÌÞ2]~sVñÌxïv™ê6?«²áªp\ýÅp»¤7(Rk.ÒKò¹ÂVcYvÛ¾¤«¬9w¶ž7â²B€³Vœ†ƒÐ*6Ég·Ì±ôßFǾ›˜^ù¶`[céù³Å‡8@â ù\|ç”!€ñhi-[íý—…Aß~`í5ÆEHD.àÍΕ=2ÿ'ò@ uÛKuu›¿4Òãî™s×HzÉHe¢xO\ß[µ|ù‘QôMŸ PT;lQ:iK,ûÆ¡µ²/6m¬9wûzñÝ’ÑbæónŒ8ÿ£äó¦Öë¬ü·QkSeñ·Àx{bÓVž?{|ˆ(>’ÏÅwN +dÒ¹J®±,è^×Í,TÍÍ®eqÑ ¬YS¦ºU}0ƒ#È|ð³l'«2ŸrÏfS6Ä2 Uî^bt…E) _´gXfŠ=±ì‰kôÁ{¿bÁ3£wZ! 0¬À!u‹jå>Û~6^ÞAÀ¨#ä|Z»5‘oâµH_’ “# `ÄÎõ¤ßõM>ÛBå¦3@À’ÏœB@JV ¥Ef›èÚ6~cÌW{šÖÙñ `ƒ@rÇÖ¯ÊjgÚË+1˜¿¤g6ÞóÊóì½ðަm2ëíÙ•¯”$÷ç'S-‡‡×#=!€@ñ ‹’Ïö^˜×ʵ.i`´Ézâû¾eDqèÓf=ÇáDec¯ê‹ÏêZÙôÙ6 ,?·[yƒUmý|¹QØX³º‡üÎÅg[–')†‹Éçb9“Œˆ¡@2Yv…„}e¡?Ü]VýMËb"¬Hv¶_ |ÊŠ`a”󿃞æô°kæÜ©ðóœ*_¸\éħƒï†@ ˆž·hl‡I,í;¹KæÈs.“¥Rmš¶+.­.Ð[ôÍK(û ÈRúñIVî>¯ì+ õñû¾dísclúl`’¿‡Úè•Éè#lŠK¶â³Í¦B, €@$ŸC@¦ @!ZZ*äN\oÉ[«£ÝEjÆŒ«‚",Hv´½Is½¡ìÂsÝé~oð¼]®>&•_Ê» *ÊìçêÛÛì[6ˆ±Ò&ø. ™^›.òŽš2ë2«.‚{à;Ç—{ 4ë’âJ¹›|ÿ† A|$3Ÿ}ôŒº©xÝL múl8uZ™iêœæ²¨Ïã¾ý—%”U7£ùlÛ÷$ñ(r’ÏE~‚ `«@UUâb‰mªMñÉÞeKºg6þΦ˜ˆ*:Û—Ëó‹5K·íqÑú{ª©©wÏó<t½kîsr3LsUƒ¬Reú4³Ÿƒ¦mŠYÀùwÍž£ßMœhO4»"1nƺ˜¼Èd5«ÎmçxldGÑ8J[ùoáPÀòûºÿ>–×LØpÌPñFùšm7‰×µñÜEyŠè(z’ÏEŠ  `¡@KKBöüùŒM‘ÉËÛËúôçlЉX°A`Ôª'%ŒùµÄRcC<ûİ­²OÿxŸ×òzš>x˵2ýíŸyU¬’Y8®³sB`ÍÓ0­€«ôÓ6 Îõf›âñb‘ó­‹É‹ËÑêIïOlКe·m=7ùÄ%IÊãò©EmÙgÛníš3w?¶åOIÔ[õç8|¶Ùò½A €@X$ŸÃ’¦@=Éê²9òä¨=/Xð@~9ûâŽ9s¬ÛCÊB(aêTÛ\7s§b#ƒ$®{qöì}‰íÔ÷õ¹F_åK[þ52ª×t/ô¯9ZBRpÎ6Un:<Ǧx¼Xd©Yëbòâêï7Oyr `£Àäs/%yް16bÊ[àX—j4òYbçÍ9ƲÏY\õg eÑk}›ßpø¿#ê›n@"°nOŠˆè@ Lc>fwYôõˆ7ë1‹rA kÑ«[&õºeµåZWº¦¿ÚhG¶Þr¶«þŒë–ë­éäöÍjúåÝY7rÁêŽö™2âçÒí¨»Î¶» Uñl gS®»~ÎÉTÛ2);/›òá”ÑRkÖ\£¦Oï§?zAbèí+{²{J57{g9@JH€äs l†ŠØ P½jÙ©ÆUgØËî´q>¢dÖãîçü‰@ÖÍÍÎè7¼æø~íœ.K·¿AfâN“YôÇJýC3•HÈÔWÖ•E4•–?\ùŸ·ž¦’ËÉc•JµuÉ›OI’÷_òêãZ9ÿtUæï•:ù×m3g¾u~\³¦¬zÇÖ/HLÿåg³¾·eÔ÷_ª¯ó=‰aúOê2·Aâ-÷=æüœšÜ±unZ©_äWZ PŠÛVýð…Úúù²¢‹>È’ñ—õê~ïÆžÙOŸ›x|îZxèg6µ^·ÃÂÀ £'Èϼ6hܱ¹cé É%†‰ —Ÿàçï¹Ô §lÆÛÇÛúä³Ä¸.œ{™tPïø·Ë‡î9× ¢‚£. ¢ÙÚŒÃ÷Vã* €ÀP$Ÿ‡Rá5@ÀLÆùð@.°rkX.Ü‘®Ÿ}[nµ(]Êã—/ßpgɲoK¹f÷E¸<.ÅUKÓdY4ï‚\Ê“E@%QÝkz”ÌÀ]//ýQ÷)cîOwgTMMNc«XÑrB⥭?5ZêÅcññbeÆù†$d}?ºgÏ~"ÙÑö96­ÐàÅBòÙ÷³Mƒ»€#ûØ[’Ï‚­Ê¢O>ËcúÁ§/·òìkÃz+O Aí0fš·azÔ‡üÌýHÔ1äÓÿÖŠÖöŒõf€Úµ ¢VÇKL+òS˜u6w\ÿlmý‚mÒç¸0ûͦ/¹áx¾”‹<ù<~Öe㵫³‰9¼2|¶…gMO €€=vý°c ‘ € 0jUËI<¿;€¦ómÒ8®ùL¾•©WBÞlàÔ²YÉÔ²TO™»QÏ7Èè½ï嚎‘¶å®uó=¹È÷‡d2ñb2Õ¾Föaþreªõ\ÕÒ2Ú¯¾Ç¤Rµ’p½&‘HüUú²=ñ,$ú«¾íõ<b™®øª¼ì]زåx“œŸ7Ù q €@l$ùlÕqÊÄú…ïˆ:¢I>=Wbð>c­;ä´X!0HÀÑú„AO£{hâ™|–e™½IŸˆnèžåw/ù‹Cî}°í³íe7Ó8©~ÁÑQ#&LÙû%†1QÇ1¸¹ÑšÏ¶Á #,˜¢8HÚQîWeßÇÿ/²½+L·ú¢e,{„cþ²ç °PÀX2óY Zg!O¶!y±¿:Û¡”3ñI>Ë¿ßÊù·ñ¦Lo—¥¯Éùj åœ ÑÉ!u‹j{ÝÌ'mûŒ“½ºùlâ|ñ Pì$Ÿ‹ý 3>@ÀÕ«+M&íÝ…kË‘qÝÌÛ qØ%à%{î'{´û!¹Š`Õヤòø´—¿>®TÂ[ªûYyþ€¤þî*ý°ü¹>£ÜÊÊõ67íT:åªF–ö–Ù^îéÆèseÉð%¡=¨ÉX<¼Z–Oi·®¼&©z?(ýD}ÓÁî¡ÎÝÞ~ÐŽ9sd;9@ cþ¤d:›]‡>­æÏ>°E©@o"nÌ“zÆ~J¶–°v†ŸÌ{`¸ØyÈ-*WÿÎÈÏ‘Ñ&áÆrÙmONøëåÇó¢WÜ+‚-€özÅÖ'FýÉÖÐ$®ÆIuóÏÛ”ºþ¶(bìQýÿO¾·&FÑ÷}ön>¸|íïó €@‘ °ìv‘žX†…Ø&ìïj˜¬Ùwе´§¡éQÛœˆ'bY^[–¶þOYZûQ¹8ýY‰ÆÖÄópP‡Èõ’NþŒìCýS¹¼uOBéš>õ¬.sÿ%w? W¼~&ËVTÒ’xŽÙaÔoÓ3çü,”¨ëêº$ ð¥PúÊ®“òL™º$»¢”B”zùbomÚ˜o×ÎZxJØqÕÖ]~¶|>~!ì~³ïO§Ÿß~K“fFÉjžSGI—6LbéÚºò† !ß¿îÇÆÄùè u‹÷oÁµäè̃k½ð–åwÈÇÏXpDá-åÖBMÃüK$ñ|ynµÂ(mþª/î £'ú@°K€ä³]çƒh@¢ÐŽ^hÑàºSöÅ=ñ¬\9¦jùò#+W¶]ÑÙ~¼÷xìí-¶Ý1¼'\#\µìÉ[eÆ‘ì±ì^ÎÁ  ø[íϸ™È,>É„st¥û¯—žì¹¸©Író€Ü7Àd!°ëbïýY” »H•rMG˜{cNn¸ì$­6¨ ‰³¡½µ¹GÝÕÜ?ô›¼Š€N¿-«x7‡öó ïòó°ïmúРÖîq>4x+vz7él¼£¼;Е—«Î)ç]>)ï&r¬XS·ð­ÚèÅ9V ©¸þ]HÑ  €€eöþâeá €ä/Pµªí㪷çß‚ï5ÿaœþÉ ×cåºÅ‘JõWʺ²5’¬b,S¢U™Q}}K{wèþ[ÞyLióíª¿Él?ììqÿ.ËþÊŠÅE! KÂ'3é/)W}BÆÃyöžÔïôÎj wFXSS¯êlû¹¼y%,ÇV­ZvV·R\ıä„¶ È5¿••·ßbaœ‡¥ï’tÝ¦Ž¥2¾ƒ®8#cÜ•ÒGMýܶÑwÜ  €£œdû–{È®i‰ÁÆ™ÃÙ/¥\XŸPöý*™Pæ ïWY$ª‚­­U¿ÀûYøü¨B8P¿òÙûšL…sç亅õSKþu ò…¼/Ÿ£òwâfi£²v‚ª+üÞ.í"€– pÕòDx €@18FÏ—qØ4[ï Odš½»»Gú%­\ÞŸ*åΑë,WÊ|ùŽþk2™Ø,{ë¶Vu.»t¯ZÆIŽ˜ T¦–#‰çû$üOÉ?Y{õºôèí_ˆ"¼tWæÇү܄bÇ¡ÝÄ";"! ˆ…€QkìÓ*i¬{åÂù‡Õ9ÍþßߨX!{o~Ö5îoÅ Ö^‡—#3‹Ï•õz‚€$¸,™«­œ9œí)x!µøiùÕ8mù°ÊI’Ж™íY Y[ÿïå@Z›k\(òýZHmýü1“êç_>G[¤ý‘®idáX·¯JyŸÁ €%(ÀEÖ<é U ¹Ù1Æ\jŸÁw6^º˜'K[ÝØÝx^ÑË«SËf(kð]Óƒ_ÉŽes¥”ö^ëW›´ˆ€++\¦¦_.~#8ššäâ ùV=Ý¥6sÇ/_îýÄP`B¦Ú›M»ó€£+P!ο;iÜÓžÔ°pž/IhI:ËEù‹j{Æ>d´þª -Ýð²îy»ìÑ}OÖ¥)ˆ@Fy3c£?´Y}E ÿìY8­§4ª+g2掻+¤«ñrû­µ2S{bÝüs¥¡‚“Г¯]Ó°ðƒÒÔzYADþ´÷ÁÞ¿½uéV{#$2@ ü¿»8Èhi@ v£N;ù\×52{¸h Ù,ùÅoVòÔ“ž%z¿“.ßy½zç%6_è-Ú“‘ÕÀdÏܪÎöÏKÙæ¬ÊS(jo§ëçÞeé´»XV<ø¬Ä`Ã’­ÉÞ2s‘Äòƒ(Mèâ!ðèm×öÈEï_K´ 6GìÍ“dLkíØ Ï¨ú…­ò<åTUÜ¿©õºÙÄíÍstâ cÜ™¦Ç4ÉEùƒ²©gKY]çWj×ݶ„D %`IrÒ¬*¸8½&ç×k£N¶+æe·í i˜h^Xµô!™Qü¤$v¦ˆm/Ÿéh}‡|¯—›¢ZƬ®¨Üþà†ÖÖ¬fÀ{ûG÷•'Þ"[Õ™žž¹’ÔcÛ‡‰gÕ0¯ó2 €@ |.“Ì@($ñ|Y”ý‡Ú·QGJÿ›ìõ_’„þzzÔöE6[3ÔǨ³~\^ݹü'2ÝàÒE]Ê¡þ1ÎüwäMM;tªí:ù¾‰>Áe/ß+|Žüƒˆ‡€\¤î¿¬N>¿"i•Ç‘}ª?"Øw¡^ž?.ÿðmdÍN­ô‹²†,>£ÆÉãÑF¹‡ÉŸGK™£dyí—›‘Çíp•·'5Ö L¨[t¸R™Q6Øß_ë=Ÿ ]õpás`}?“Æ6.˜—™ª’¼OÉgÛU¾+Ûà1Ú˜ÏʧÕg»{Æfä3îaù|{R’Êäfö´Œi›|þ•ËûÕòçDù¼“ÏD}L¿l¦¥ ÷ÿ8ŽÎðÙ§F¬ €€Ï$Ÿ}¥9@A--£åYý WJåáAò‹á5Éc?¬:–}"]?¯­Tnõ8S©êäs½?—Äì÷Ía»€\`ÙžI$Þ£šæöÚ«v2×7qµÄâíõñ¦ªUmGtŸ?÷ɨ¡°_ §R­¨èQÞþõqûýßË";ð%v¥”åÊû Ç’x¶ÿ8Â^·*Ñqàb”@ :­úeÖ³ß6o[õâ“ð©g™ùìSK¾6SÙ=0ûùn_ ª1­[$9·äó` oKˆå¯Õ‰Þß­¿]ò/¿<ðx ÑlÅß¹Á1çòxÝÆ•7®Í¥e@ŠK€½)‹ë|2@À*duÙL (iUPaãÍ„ÖzYUªíöªåË ³kúÚGÀK<«ÞNy•Äó>4¶>•%W¯è™1û1[âÛy~Ó¿åòÏÏl‰G»æ=¶ÄB `·À®Ylæv»£,áè´ZýBëâm%,ÀÐc íØïYÉŒá"8d[—?..¼›^Øï%Ö§ãoéÅi¬ù½©ôì1 `‡Ég;ÎQ €Å)`Ü‹Šs`¹JVïÔeîCÕmïó–ŠÌ­6¥ hi©¨R½íÒÎô‚Û¢pŒú¦¬ÐNgÙ÷¢çšìK]R_t´Å# KyÞ\<£)®‘8®¹¥¸FÄhŠS@[±ß³1ÆÊùžóŒ[neòY–~–Y¸19š›]ùÅš?-=]2—ûVKC#,@H>‡M7 €@© Œ_¾|¼,un©{„ñŽ’´ó’«Ú–ëìœ0B9ÞòS ¥%‘L&þÏ»ÀÏfi+PUéîÌçí!ÏÆwž?û/Rõ7yV÷»Úk+Rˬ¸ì÷ÀhüØôÿ³w'pr”eâÇß·z’éž™@ œŠ. (—\AGå0™Édf’ÁY9u=w×sw5ê®×zû÷à— ØÉL2Ó“€€ETä”[¹É}ÌtÏÑõþŸš$’„9ú¨ã­ê_}>LW½õ¾Ïû­žîžzê}+½y¹ÔºÚÿš©±Jçžß·Ž)·«Dd÷´:<„V¦nÂqâ¿géåØÔáF­›ºÃ¡—ˆÍÈgOÆ)š‹å¿˜Ý 9ôcAƒæ×«û?AÃ4‰ `‘Ég‹¡ €I(¤ÜNéÏô$õÉ—¾Ý6l†nkÌõíK}T2©@&ú¦ð^‹,1‹îϧòïV]]E‹Ãýž-±Õ)ýn[b!°\`É’a¹(ð2Ë£¬¹ðäsïuá…#5×q:GK¦Ýv­1\É5ÚX7…¸\,mÉq.Mô¹—<*ï£7”VšRa Èl+„Õí €Ø+@òÙÞcCd €@¬Gëü+]eþ˜îë~o°ÍÔví ýK?¨´úxm+Ī÷O™”:EÍ9c“ÍQç÷]ãÝ;ü9b”aL½mà b"P4Îÿ“Pm¾¸'&’¾…9âº#?ô­6*B Ýž3KªÞ+ ê˪ÖÕ‰˜v{¬ÓZ[7Š[¹/߯õ¼†²JÔ…µþnÔ!ÐþNO¬Þü2ï–W, €5.@ò¹Æ_tBÀ›VÚõÖ êNPi­Õ ¹îE ê“5]ÉäzÞ$·×¶f„ª50ö²®X,ž’ŸÓù¤½!n‹löùÞµË-‰ó¦¾¥ñ¹7Ÿ%h„@­ ¬Ï]øwý|u­öß¶~ËëÖö_ñ”mq» Ô[s`wO·!9Ég£¬K>˱×#)s讯›Ÿ¯î½ø‰ï/6ÇXS±ómuã¢Ñšê3EW€äó¸,¬D¨F@¦•~‡ìŸª¦ŽZÙWF.~¡!׳X­ZUW+}ºŸM+³{Ë­¿²Òδ Û¢þê´2›”cæ ÏﺯúÚ©Af.ðî/gÅRÔªÕŠ@b!àýU ”ûcF´ä:M÷ÿ¢ƒ˜ZÀ÷°©K_BÞ¸þþÈ5? ¾¥°ZÐVN!.Ç;VSoËÑ22ÛÕWÂ:j´3±€Œœ_«ÓikþNš8R¶ €„!@ò9 eÚ@jO`^íu¹òeÎÎlY×£²Yî‘]9ãÖ=e¸óh1u™<Ù·ÚªØ?x/ñluJ~î‚[‚oÍ¿†Zx£n~ã_ÕÔ¤y¿­†}¨1u¹‹þ"§é^cݶ®»r‚þŠu½—Ææ¢+ë (\£·Á‰Z3V&k'ŠvªõuzÔÎQÜÆXq±ÁT~;n_óÚ¯–YÅîÝq?‡/`ŒùŸÕK~´%ü–i@ÀF’Ï6bBâ,pÛÞhÓÓâÜ…ˆboÍdRKI@W§ŸYÑó19¡;§ºZØ; ¸&ž·ÛmÍèçã·Žößÿ#€“ È ú/H ï,Ñ §œâ¢hš¦U*ÐÊŽä³Q‰J>g6*/ùlÝLFë¸|VjÑ"×uõUðêfÿþÞ´Ùý‰ÕQ €@ÜH>Çý? `™@ú™Y'IH»[V\Â!]Å‘jÌõ-§o¾^EìžÀ³rK¹æ¸xÞ‘§0èvËó×Eô³MͨmšE ¬î[üˆLQú½†žˆ%±óÍg—_öX":C'jCÀ–ä³ÖvŽ®ðUðØ—ä½øïîØnÚ¨Ø|ö0Öæ.î•¿/n †Š'ÐÆ|rì5=i)6"€Ô’ÉçZ:ÚôA@«÷­ÎÙK@_%Woó]Žc6›rÍØHT¦./Ç-š²˜Qç„ÁÖÎ?GÓ¼O­vuå•ÑË|ª­ªjd#SoW%ÈÎÔ €1_’^?]ƒ=ºËOÔ»ÎÿFí#PªÀ~­ç5ÈÅ/+µ|å䯾‰ùìYI¢÷¡ Í*¬ûPõ–EuîénÆ8‘˜Ù#ü£°juî’løÍÒ" €€ÍœØ¶ùè KCò¹úã¶ sìQß®¾šÚ©!“q>&#f×NcÛÓßL×õo,´·?Ûì¸vÌÕ;<òÇSÔÊ•õQ@Û /5}—l6Fýk¼¢ND´|:wá`"zB'jB`D_%•»ÚD¿¸£ÆÆDmµ06&Ô§íµÇS¯¬¶cQì¿6wáòrýJm×n›:¯ÝâùµÛzŽ 0‘Éç‰dX P¶@º¿Ûû#5–¨–ÝÙÀw0Ëôwÿ{àÍ$ t_ör’áË èJ» ¿—ßwÍÉÖ'¥£ƒû¬¹NúbCÓÅ’âJ?@ µ¹Å˵ÒW„Ó­c.]Ó·xÄIÀ(cËý ^àq²+)VcgBÝŒ;îó]âÎ…Öì36»Ä;¯åYPZ™Ï®î¿,QSâeE½ €@­ |®µ#N@£Þ`õµWµQßlèïf:Ý)޼Ö)ユ ScstJëwå[;>®fŸŸ¬ið¤?rRvit´/´ìèÔÛ_xÆO €@i#ÎèÇŒR•VšRUüÕŒ}¼ŠýÙ¨¼‘Ï‘/2åöÃr["7ò@|@;);Gs;*¶Éguá…#®vÏCÅ,>¿^w­N¦D¸~ußâïﺞç €x$Ÿy €ø& '/I~ø¦¹µ"™óÊé¹¥‡ù\mbªKç–¼M:ÃTïöÑÛ‹ÊÌηtØ2=uRæTZv•2¢®¹ìØj^`ÃòË6hG/ˆášÇ  gÁºk®Ú\ÔŒ@02;Âk‚©¹¼Ze9²R»vŽ|vŒ‰õߟëz/½OþŽþ@y¯2J—' ŸšfRï–}ä4  €/ ùübÖ €T"`Œw/0F>Wb7ù>3RÊY®V^¹ÛäÅjpë¢EŽV÷ƶóÐJ2ô ù¦YÇ·.{¯%w)äÍÒ»uôð *›m² B@˜ ¬Y~ñíÚ˜Å,ìØ„k´9õò‹îŒMÀŠÀ’U²" éjÈï“Ï{ÀãÂmÝÅ?M·¾Ã«±¼åÖ?•)¡XÞ^”.Q `t±óéÜ…kJ,O1@ ù\ƒ.#€A4®ì=BêÝ;ˆº©Ó¼*SÌ\ŒÃÎé×y–¬9jçµ<³@àVÇqŽ+Ì[ð%ÕÜÈ]•÷¡_ª™6jáè`_º&×üØyßg]t“1úyɶÓ©SúÏ>±š­F.¸ýìš¾‹¿U³t@ ,’ÏeqQ@`&ÙÆ&¢¹J­ZåMW»‹Œz–ûÉè,ëï„óŸä¼óå‡Eòøœœ‚þºVú9ùq“l[m}&p£$¿X?ê8ØÚù95çŒM“OðV黌Ŀт©z{gX! €@\d”ØšúMÿ"Ÿ]?k¢Ž[>ç¯X“Þt¦˨c¡}ªÐÊŽä³Që6/»bmU}±zgý á9ÊyµqUÓú%nœnœS]‰ÞÖ}äoÙO¯í½˜ÛsTNÈž €@Í ÔöÉëš;ÜtF ¾7{°Ô¼W0µSëNZÍÎl^ÿñ¼RßÜi} =iH§Î’?~mõ<$#~ F뾟Ÿ?ÿ‰ÉMãŠì>¦X7[Ê¿ÞUêõ’Ì}“”oœlŸ¨·IŒ÷­.¬q.ÛÐÞ¾a0ê€,i_^“9 åäˆÃqÒÎðë Jý*â8hâ,°dÉð¥Þ³ç¼sž—÷|ï~™,¥ õÕ¹‹ÿ]ŠËÇ ñ ë·á•¬eerÖ·£ëºËÌA¾UçWEƘDŒ|ÞîñtîÂ5ûµž×<¤F{ä"¡S·¯çÿ)\yu¾Mßb¹€š@ÒH>—nEI@ tÊ9Þ†„—¼ÕÚ|IþˇںI^ç¦è‘Ìÿgú—}Ââsº¿—Äì{ - ¢'c›æv=+?x÷ ÞzÏàl6ÕÐè¼Ö%!}¢¬÷‘_Ø¡•Ùd”î5Ú½(߲е­ä–ŠÆ\ŸRœ8ÔÎ$,’Ï;~DŠÌÚ¾Åß³õœ‡$'ò=©s“3ŽÈGÀGääü“c+10ÖŒ|Nî”Ûòr˜¦êVE _:1#Ÿ·ãJzP½eQËÞ»=ñ¹BèÃÛ×óÿ„ë]¥O_×wñõ–` €ðä0¬F(]@®fÊíÒ¹ü(™qœÔ…RÑ[ý¨,Nu4ä–Ÿ&éçWÙ³$/(4Îú°jn­8¾®®¢Œ$¾Mö÷ßQ’lŸÞßýª:­O’ç'ÈEo%Þ(„0²œOËôæ})­– º«TW×°´Ë2Àpë‚2¹î§eó~ eµ6†÷ãP¤iÚX›[ü£½ZÏú‹ÒŽ7 ÷¾µÑë²{ù´¼÷ž¾ºï’ß•½'; `·€ß¹÷DöëÐy#r÷šwΩo¦_uúSO²F>ÿÃäÆE£rï£ìÝzöíF;?’‹š3ÿØÆ; Ü™*šÎ5+—tQõŽ;ò3 €žÉg^ €Õ HB¬úJ¨¡LfItžoíüE™ûź¸«Ý…‘u-Iîå\híüLÙûMµƒÖF2¾2B= E/+.÷ôõ¦V–‹>Ž“ô‘²î5òðN¦Ç¶WöÏìöyÜ"Éýß›¢ûÇšY_™ÝŽ{­’'ïÙqEø?kÞÃG§E-°&wéodªÒ£†uq±tt^¢;[nçä­!gäÜdß¶\Ê'A`VÛYÊE3lè‹|÷І8ŽÁë£mßá^ºûÜî±qÅ×Ü÷Hª_»ä²=ÛϽEÕUÀk# ÂÚFÍ·fŽ4þç#×üÀû‘@ŠH>WÄÆN €ÿÈå”>æÏù!Lo¨l¶OF¤æÃl4ª¶¦ç–&'ŸNªý‰ÛÕßÎÏëð?ñÍòÅ7zú, €T#@ò¹=öEPwøµÊQ)("xY:“ú”$"¿Ië!7*÷Ó=7ä&§nΘž|kÇL]0øƒmmÞ”ÏÞƒ%*”Z¥Ü¨¡]¹øÀ;yIòù~BŸÖö^r…Œ‚^9¬Ý¯Ibê,©Öñ©ê¸T#j™ÅC©ÑÏ1Ú9.‡Œ8+Û®.ÕÙ°˜ÌFõ° ƒÑÒG;Àwì§DäÍ®”ØäóX_—,–Dë—öjÿÕªè~OÖ¶£Aü< 7uúÆÌᆯ3Ú¹FŽ8ÝDB¨µ?C ¥ @ ¶´£“2êyDŽÜÕòø–<~—£(#?Ù¸"»O\â­8ÎÛ.˜&û¾·âýØQFß—¯éë¨×Å@ &Þ(è5}Ÿ[,ª£%W²2&aû¦éwsôš¾KÎ'ñì'UX- ‰çÃ- ð‰Çn¼L®µMö¢•yÀÆe¼‘Ï5±¬YvÑCkú¿Cf‡:Y:|GMtZ©¢|Ž_¬êR‡¬é]ü%Ï5rÔé& ’#ŸC‚¦@ ©2•ïQ2ÍlÜ—»ä¯®w·.øÇýÞÏ2Òö Ò±wZÞ¹F×­ûŒÄøqËã¬*¼†göœ+Þ—TU‰¿;´vÞ©N=sÀßj©-”>¼2Ê~3v/ð(C m¨õ+ß+Ýœ»×üs•>'ŸÓíò<þß w>v’ƒ3ÝŽÒ_YÝwI­$#vàY­ X’|ÖÉŸr[^aòúql|ûÔ‡ÕÚ/€ÌðqƒôùؽÛÎ}‡kÜÿÔJ¿1éÓ¥££êëV.ö.žeA@ÀwF>ûNJ… €@m HâùȘ÷8—WÓ߸câÙë÷<ßÚù.£ÜfyýHÆI‘Í2¹ìË&-ór_ųìê‚ùüÀÜù÷ØÑØ!`þurê2îïËQÒ>”!°fùÅ·¯î[Ü™*šƒ%…òò´¶ŒÝm-ºZû†×§µ}—,”þ‘x¶õHW@®Ég[Gû^§ëlZ¼fF>ïrLÍêÞ‹WÊûÿ?ã¼Þhu™|¾åw)ǧʅbŸžnRÊ(ï‘xŽã!$f@ >$Ÿãs¬ˆ°O@†×IP±MrÈ^+óùâÕÚ:8n¡uáétñuJ›Þ‰ÊX°¾ÞèÔç,ˆ#ö¸>»»TlÓ½·îÈ7íù@:K¥ HÝjA'ö›ÑÓ³§qÔÀs+.yT¦ãþÔî# ûËpáv™ÊóÒ}otULI,ŒÅlÚfŽ4x'æ?íõ)&Á&¾ ì¶ðœY’h³cÆ!íÝ 9ù‹ÜÎÀû{ôI {zÐAoy_Ú¸B imî¢?­í]|V±ÞÙWn»t¶œ¹^wC  Ú†ŒZ'ŸmHÜo^óºYÛ·øÞí3ª­–ý@˜J€i·§b; €À„é=¯M°{í5}¡êjž*Ìõ'wmTÆ´gr=ß–É$­œÞZî:ü>¹÷ó¢¹]ÏNÕŸ¸mÏçSód„ýtkâvÕGTsó¨5ñˆUù}Ÿ¿#óÌ^^²%Òu#ÓŠÞ…A7Z…C0 PÛî¹\:»ü€… 3…ÂÌ·Éœ²säù)òø'Ë‘ï×I¢mEýô«ž\²dldgå-;JÛÂq]sžLÛ`UtŽûŒUñøLݖ¨©K{Óè[°¸6\ÔŽƒqÏ0ÊÙ#œÆJoexÆt. ×ú%n”ÿ.õ3ÚÏܳÞvª2Z>ÛÌÛeÝKåaË"ר«»å³íZI:¯\Þt³Z²¤8\Ÿ-! €@-|®…£L@€¹ß³L‡PíVû¬ž¦æ«Ó&ñü¢Öµ6r6òé\÷óÒ㯼h{ô+êåÞÏ“0>}(þF ô]rRØßJ+¯mi¾­óæÊwgÏÄ Ì>D庽éYOˆ²¯Z¥Ž’öoŒ2ÚF¶%sûEÂ{¨½:ÎÛWÝ“ŒkÞ íÇȪ×Êc¦·-„e½´q§<îÐÆÜ¢ŠîoW¯¼,qí…àYër—È…,A ¬»æªMÒ†w Kˆkr—þ&Äæhª ÍË®X»Y©ŸIÞCíÕþþCõhñD£Ù’Œö>×¼‹@úPæI2ß!÷ ¿S>Û~?êoÞ°ü² ^\, €D)@ò9J}ÚFb.àj}¤Œ¸ÛâívåO[XÑ(…BkçW%=]R¡‹ìë¸ù êíýŠjk“¿…“±xSn úKzStÝbâ’û–Ø&, -£tL¤Ég£Llo‰°ÝAÖô\è}ÿò¦ãöcËž-gî¯uê`Ñz°|·ßv“GjlŸýãÝcÚxŸi›$¹¼A.ØzVþF9êiãª¿É €‡Gê†öá/Ú• €X @òÙ‚ƒ@ €@\£–?œbµÈ}šþ'ß²ð¦j‚–ô%}€üÁxn5õ°ïî™Ôèûä¯ÔPw$UòΩò‡öD'›CINŠ_9ÔÖõH¨ÒX,ä—»µ‘wˆy½¾*Âæi(Y`mÿOIaïñ›Rv:ø©¤éY#Óˆ—Rž2 €„* Ó\?§ÔߤMïáÝ#zÊeï…jZ­VóÙ6%@b"@ò9&Š0@$ñ|°qMÓíƒ{|y’í%o*ä‹ÈdR¯”ÞZòN!4F'Í$&ù, ̵äSTîÿ†pi"Ž«ï‰8÷,ŠÆ¶ûª&àÈÒ°A`Û=¥m…@ðE`õ’mñ¥"*A@ÀÇ’8@ žqJnŒ¤´~Ÿjnõ…º««˜Jß)uýÝ—ú|ªDÆZ‘î_r¢OÕE[Í¢E2¸^͉6ˆ´Þ?Ôºàá<ã&ÔÓÿ"›£¾nb?•Íf& “M € € € €ø.@òÙwR*DjD ·w†ôÔ»'_,AûÕ--÷úì–9]«•vß%uý¬·Úº´›òF?Ç~É{ôléÄ^6tÄý]â †˜´¶J¤:Ú¦†º8] 5í#€ € € €ø @òÙDª@jQ Ñ)¾2Fý~t°qãWƒˆWîý{ÞèËT޾ŧM§Zyån¾ÕYEî["kz§†õƒ…y¿ÞiO˜Zàž©‹[ÂuÉç`‰©@@@@`’Ï»€ð@ 4׸±¹ß³Ö꣪ù¬Bi=+¿T¡i–w/à»Êß3°=2 ņÓ«=¼ŠO ¯©ÉZrO¶•mŒ' ³-Ü7Þú0×É…1qºH(LÚB@@@H€äs@°T‹$^@ëXŒ¨“{ ß0ØÒ¹"Ðã!÷‘ÖÚ9GÚ°fúm£ÌYö9èÊå~ÏÒăn¦„úGwÚ%”£; ¸&úi·å¶±xŸÞ Ž' € € € €± ùëÃGð €@”z¿([/±m£ç“%–­ªØ`KûíRÁ…UUâïÎ'Ô¯\ÛÄSãqǼF8öð—¤üÚdäè¯ÚÚž+Oö¨y£Þ@} D€ € € € PK$ŸkéhÓW@ÀO­öõ³º@ê2úª¹íwR÷8•N›Vü/Y½~œM‘¬ÒE÷]‘4ìC£Æuí˜rÛ¨%>t‡*jP@×™¿ZÐí—X! € € € €ÔÉç:ØtðWÀìïo}¾×f\3úEßk¤ÂM§u­SÊ|u’"aoŠí}ŸeÚðÃÆ§=wúôâ²qÖ³ )òïèxJ v¯ù)ØZà€ËQ @@@@_H>ûÂH% €@ µå½Îµu=vŒù¼ûÿ¤ÍgÃnw¼öä~×GL_ž}õxÛb°.ú‘ÏZýaë1Ð"Dû´6Z먧ÞÞ[#o, € € € €„#@ò9gZA’(`õÈç”1_޽«+¯ŒþV$mÓh]*»©·ÓË–$]‰|ZwcÔŠqHY…@É®1O”\8˜‚u}}L½Œ-µ"€ € € €Œ#@òyV!€L.0sÙ²™R"=y©è¶JÒðÚ-óü%ªò&u´½1ªöwl×(Õµãó8ü¬§™ãlˆS¾$ýÒ†8ˆ!¾ŽÒÑÏ‚àmŸ¥"¾˜È@@@@^$@òùE$¬@˜J`pš‰|Têd1Ê,³ßžl{àÛÚÚ6+¥ÞNi ÚпìØÒŠZRÊ56Ä»vðö{î´D„0b* <yèNô³Dn@ € € € €¡ |š†@ä¤Tq{{£š×~CÔñ3êÝûYrOÑ/ƘÑGQFZÏ.£tPEW©E‹Ü *§Þšˆ<ù\,š½kF›Ž"€ € € €D.@ò9òC@ €@ü´[·»µQks‘Ò:ò¤oa^×ßÄh¥Nf¾q”… [×Ê|Úmy­*!ZŠ 0¹€q#Ÿv[Þí}¿ž\­ € € € €1 ùÃFÈ €@ÔƱ6™1\g¦_µÏööµÑmÿ9âÿ«_ÑshÄ1”Ô|}÷ÁFéÝJ*`!mÌﬞªkEÀè§¢îª\ÎùïSÔ´ € € € žÉçð¬i HŒ€v­M>÷nnm]c ôà~«½‘ÏÏÛãÆcô³cTäSnke6 Üqï½67bˆ·@*¥7FÝÅÏÈç¨í#€ € € €5$@ò¹†6]Eü0ÚÎd†Œ4¾Ê¯>úRÏìóG”6¶ÄÔæKŸ‚®ÄQ‘O¹­”¾•û=} k£þâ¨yòÙÖ÷ëÚxÐK@@@@ öH>×Þ1§Ç €@ÕZ©™UWâëëÒ×ú_m•5º:[e ~í~BcoïKýª,°zŒ>:°ºK¬Øu{‰E)†À¤éÆáM“a##ŸC@¦ @@@@|þ? €”*`ãH:Iˆ/Wsæ •Ú‡°Êå[;n‘¶ž«½IÚÑÆmd»-›ŽŒ<G“|Žü $#€õ'wE?òYY{›„ddz € € € °“Éç8x‚ PŠ€LoÝXJ¹PËhI>Û¸hw¨ôB“@¬N>7®Èî#N{Gme”!ùõAHVû‘& µÒ Éâ¤7 € € € €6 |¶ùè €@©ƒƒÅëK-z9ǵeêí“ÕªKÓ¡÷¿ÄG]}T‰Eƒ,¶¾ÐÒùh PwÍ Dš|®9m:Œ € € € ©ÉçHùiˆ§€L»=ݲÈ­ººò–ÅôpòsxSoÛÐÌ4lžÙüÀ,ûÁ1©È“Ï2};£ž-{]$ ™t Ê…i·£Ô§m@@@@ ÖH>×Ú§¿ €€/®eÓ¸šë|éV•hÓdõ¥Ömtqn©eÃ.'3”v›»¶g Éç]Mx^µÀpÕ5TU–k*X@@@@G€äs8δ‚  €«Ô/¬Þ—ªëôûRQÕ•è–ª«¨zt@U—^­Vw—^˜’”$0XR) !€ € € € ’Ï 8ˆt]À¨TèmNÜà“C­ žx³[ …ÑßI$ë-ˆæåMý=GXÇÎ!¬ZU'+ßyeøÏ×Ü~«´ˆ@ MÖNå € € € €ì @òy ~D(U@Ï(µdàå´¾)ð6üh ««(Õ\ãGUÕÖ!X7õvÓÀúä_QßK|t`ÿµ÷WëËþì" “3Dºxv° € € € € ŠÉçP˜i@ (­Üx$Ÿ=­sA9”U¯1ÖM½]tÝÈï÷¬µ~HÍ>¤,K #0µÀèÔE( € € € €@2H>'ã8Ò @ T¹7ï@¨ NÒ˜6Îï'ÙlÕ¦ú}­dC"ê3zzö´ G;GE1枨c ýD D;¢_+yËfA@@@@ ’Ïá8Ó  (­-£C M{ü%.¸ÚÛ7ÈðgïÞÏQ/ÎH™u;·o"ù,:î÷¼óAáYŒÚ˜„nÐ@@@@x|ŽÇq"J@ñîRÍÍ6Œ$?ºñÖµb¼Õa¯ÓZµ†Ýæí=ÅöÀ7;Ê|\¹ÐéZì5}F@@@¨M’ϵyÜé5 P¥€3Te¾ìn´ºÓ—ŠB¬¤èŽ® ±¹ ›’™xOU·]0mÂ!n˜¹lÙLi·)Wë»ÇÝÀJª0$Ÿ«òcg@@@@8 |ŽÓÑ"V@ÀmTÞ†P´Ñ÷ÙG91 Ïïòb~¼œ}‚(k”Þ-ý̬“‚¨»Ü: )ù¨g‰y pÛ=•;å°_@o²?F"D@@@HŠÉç¤IúÔ €ëªûãÙmÓoCÜZ9ó­ˆC»ÇXÇ}jÑ"ׂ8!y3¢í’áuí u@@@@ ¦H>×Ôᦳ €€?F›jª®gš‰eòYk}Mu=÷mïNedòòˆ­tä#Ÿû=Gü:Hpó»GÙ7¹¿ûp”íÓ6 € € € €@m |®­ãMo@_dÚí¾TT]%#ùwtÛð~5í#€ € € €„$@ò9$hšA%ઢýYmA ‡ ¡¾ŠwöuGÓékueVÖ4°þ0Ù¥¾ÌÝ|/ž*ÖÝã{¥TXóÅ¢ŠtÊmïÈÌú$Ÿkþ• € € € žÉçð¬i HŽ€v#¿O°RzK¬A‹uý–Äÿò†þeÇF˨‰~ÊméûsmmÏEe@»ÉH¥ê,H>»$Ÿ“û£g € € € €€u$Ÿ­;$„Ä@@kׂ(m}]1Ã`[ÛÓ2×ómWàãŽÆ¸ïò±º²ªÒÊ}}Y;PX¦Ü¾3€j©ïÎ{GÍÀ´ÛQÚG@@@jK€äsmoz‹ø#`L<ïµìOï}«Å8¶L½­Þ­²Ù”o+«"ý†²ŠPXîNò9Wª£÷‹ÚÁhµ!êh@@@@ vH>×ᦧ €€ŸŒ:6i?;E])íä¢hwœ6÷­Ïè·³>ØU·]0M8&ØFJ¨ÝÑ$ŸK`¢H%&òä³cÔºJ"g@@@@* ù\‰û €Ø|nŒûa˜Ûî%=ÿnC?åœv Oïå%žëÃnw×öŠÆ%ù¼+ Ï}p•ÙÇ—Šª¨DfXx¾ŠÝÙ@@@@²H>—ÅEa@1­ë¢—Ð3£ÁôR?jñ¡ŽùM==/ñ¡žrª8®œÂ•λT7Õ"p@äZ?y € € € € P3$ŸkæPÓQ@ q{&¢GÆ,±¤ÓŠÓܳCE«Èï÷,ý½GuuY0’?Ty I@+µoHMMØLþ%kž›p#@@@@ðYÀ‚‘k>÷ˆê@0Fe$©õÒд2»÷–9]«£¤šöó­·dú{ž:¬¦öÕV«V}S57úSßäµ¥þyò¡lýS(­Ðˆ/+²û˜bJ~W´ÜKÙ쯴ÚW^GÞ…(³ä=iOù¹A~n”õ»+#ÿnÖ=³­qï"ƒÍ»üœ—÷³ Z«FËÿ®Ú u¯QÚZbYŠù$0sÙ²™CÓÌ[åýãíR¥÷8d,¿¼uÀ±O­„ZÍ ií¹˜â„­Ér-=ÜèMñýT¨QŒÓ˜£tä1Œ«@@@@, 3§² € P€1­`/_w‘±WúZaT•ySo›à§º.µ{2Zò‹jÑ¢à¾#˜± Éç[J5¡\u™åËÌä–~¸!×}ýP»ZÏÝRãåqHu5[»wJ"‹üb™ü1k… @@@@ ‘Œ|Näa¥S €@ðÚ55NÔSʪÂïi8-8ÆüÌÕúÓá´6e+G¥=ꌂRWLY²‚+—í*õÒ võuhÿ)Û}ñ·²Ýûû÷q‡ºäRƒ3”*¾É›>;¾ƒ›ýµ «6ýpXmÑ € € € €ž@p£šðEH´@}ƒkÃtů t„nˆGp mÁÝr‡Ö;ClrÒ¦$MøÕÛëM'ìûb\·Ã÷J+©û=W¢6ù>2ª=ÝßÝ,÷A^:l†ž•ÄóOdI<³D!à]$E»´‰ € € € P»$Ÿk÷ØÓs@ *õ'wm” ÖVUIõ;7Ö¿þèWT_%5h}¹%‘xaìŸqŠ_"õÜD½eÖY, ÿTæ>ŸH ›mÊäz>*÷‚¿O&‘ÿµë”Çô‰Š³>iNý#á´D+ € € € €l ùÌ+@ È©bñÈj:`Ó¾)§x•Ä3bOLæ£éÝ'ùO}oöU2Yû~ÖYa]·ª®®-îËnÛfär{É}œe2©Çeäþ÷dub¦ÂOÀA^¿±¥e}úA@@@@b$@ò9F‹P@ "ŸÒÕ5ê( ]* iËœ®Õ²ãòŠvf'­]u…wï^¿ªOéÔ™~ÕUM=Zkot.K…»]›%#¿1ª†“û8Aª™UaU씀Q‘¿?Õ5êE@@@°W ÎÞЈ @ÀvÁú°$ž"]$‰˜˜‘ϤQîZ9 #Eݹñ—˽{ªŒiUZ&T®fY´È‘{ŸQM~íë*s£_uÕT=Ùl&ÝúÄȈú”¼Zw¯©¾Ç­³Z=·‰@@@@ þŒ|Žÿ1¤ €@dFë»#k|{ÃZÍÞþcþ/´,ðFäÚ–4š›é_ö•j}Ó³|«Ôñ²jëñaÿ‚Üïùfê©©*ä~Îï”鵑KþW:NâÙò£/züÅò @@@@ $ŸxPé –€kÜ{ÂjkÂvŒ:¨±·÷¥nÛot±V?¶/ló¹·ï¹ÕÄ%=;¯šýýÚW†oÿFî÷œ÷«¾¤×3}yöÕ™\÷¯eôûÏ¥¯û%½¿IéŸcLôïÏIÁ¤ € € € €% |.™Š‚ €» Ý~¯wOÑÁ]ׇýܤFßv›A¶—wò—Hý›ƒl£’º%i{A:×óžJöÍä²Þˆçù•ìë÷>Z«ký®3‘õÝvÁ4¹à`Q*•ºKúלÈ>&¸S®cÁÌ ö¥k € € € €ã |ß…µ €¥,ZäJ±È§v5FŸPJ¸±)3çŒMJi/mÛâhe®¨,íü»tfš r‹ÅklˆÃæšú{ŽÈ<³×䂃/Hœu6ÇJl/ßÓM…¹¿x k@@@@V€äs°¾Ôž,B²ºCoðG@+mÁÔ®&Q#Ÿ½#c´ù¾üç%÷m[¼ô•é\÷gK ,ÝßýJI¦°Ôò—{d¨­ëÁ€Ûˆoõr%G&·ôcEcn—NߎÔvä2wÿݵ-@ï@@@@¢ ù•<íN%`Ýt³ð–©‚f;µ( IR ’Ïê8•ͦ’ä_hé|Tú³ÄÖ>i¥¾"÷þ¹¸7M£Üëù[RÆŠQÏ’_6U¼µº}ë³»gú{ºÅè»b0½VÑo­H>'â@Ò @@@@ ~$ŸãwÌj%âç,ìè3ÆDHD.`\}GäA(Õ”ÉLKÜ(MGé¯Z`;YïÌdRw¤sKÞ2Q!IPŸ.Û¬¸×ó¶I>s°W,;¦PHýY6µ³™U1Ð.Éç˜2ÂE@@@#@ò91‡2qyÀ¶ÉÔÂÙñ `ƒ@¡~ËmGäÓCå¾Í?chí¸Kê[ágÔu°VÎ*I2/m\±üÈ럞[z˜LÑ}áŽë"þù™üíwßq Ö5ß[Úêºî͘LÎ’­þ”ˆ~Ð @@@@Ø |ŽÝ!«•€}'Mµ!aQ+/?úYžÀ©gÈ‘O½-ˆ$.ù¼õ@8_*ï€DVºÓu‹wKúÞý ú»ÏK)}ƒÜ{v·È"Úµa£–ªE‹"¿Pb×°¢|žéïþˆ£åCC”qж¯ƒƒM³˜vÛWR*C@@@(U€äs©R” U ¥_…Ú` ûb*!lРЀ1Ê‚‹3ÌÕªKÓ¡t8ÄFò­í·JsKCl²Ú¦ÞàÝZ^HEûW[™ÏûÿÌçúâ[1:Ó×ýeÔ÷¥|Œï‘/ò?©ææÑñ6°@@@@ 8Ù´0õW$°¥¥ã^I^Ü_ÑÎìdîšÓþ×@ª¦R’  õ-èF&½¥éx âð=QüY©tÄ÷Šk«ÂGóó:mxF¯.‰çt÷”V>"@À‚‹èU"€ € € €ÄB€äs,Sm)çÆ/±¨ç‹-Š…P°NÀÕÊŠ¤ž£So·LJ€†ÚºQÊü؇ªj¶ ¹ éªšíüŽÏfS™þžËešú츚Ÿ“# ÷YÿCrzCO@@@@â&@ò9nG¬†âMgF/’în´ ËkóªÞ¦D¸$„€ÀÎÃsÛ5‘ÿ¾cZvŽ,9Ïò©ÂKožMNBí‰ë:Š‹ˆ¼©¶3©ËEþ½¡êÓX¨Úq­¸(ÔNÓ € € € €€5$Ÿ­9²«Àú“»$‘e¾¶ëúðŸë/©ÖÖÁðÛ¥Eb$ µÜâ׊ÑÏGgrÙ—ÅH®ôP眱Iiý‰Òw ä+ s;ßáyMþ(#ž½û;¿§&;_+Öê±¹]\¤R+Ç›~"€ € € € |¶ð Ò ù}×~Kk}ß kÂþÉÜ™oÚãGa·J{ÄQ@}ƒ qkU×jCAÄoé¸Z¦K¾.ˆº“\§ãèšoèïù¢ã'ù8Ó7¥´Q«p@@@@@ J’ÏQêÓöÔ³ÏÑÚy§,L]Ø÷E¥Þ¥š›G}¯™ H €v´IW™Ä&Ÿ½—I™sä¿È§8ËKXîõ|ÿÀœö_Æ%Þ âlÈõ|@¦¤ÿ|uS§]®6¿¶+"¢A@@@¨5’ϵvÄcØß¹óï‘©fÏ 9tWÚ[v@gbÁ–?6Ú¼WJ O\ªê-!÷ÎÁÖŽ‹ª®‰ ¨IK¦|uÝ…IçO§‹çJIz?«ëŸù¦êê ò3£ºð‚Ü[Fÿ;J÷I{Ù uÛ# #Ü™rÛžÃA$ € € € €@Í |®ÙCÏŽZ\)÷€~£Dÿ×zð€ã¤^/#ž—P7U"PEíØ1å«Ö-{\ŸÝ=ÉèëOîÚè8Ž—d/$¹ŸUôíÉ|ÞýqûÇz×L&õéÀa±îÁ—% #Ÿo(k #€ € € € €ÉçP©2XÁ–öÛóùâ‘r’õËÒ’I—£Õçó©Ì1c÷—6|jG ÑÃ-÷KŸ² “ÓóCÎ| â4„¹íwÊŒ´‘˜V.Ÿ_”QÏù˜†_UØ ¹noTü{ªª„ã&Pœ¦Ó¿‰[ÐÄ‹ € € € <’ÏÉ;¦µÑ#I( ¶v~¾NM?PõéôÓtüI­õ—¤Žƒ -_Vsæ UP» €À.r1Gÿ.«¢yjô»¢i8ÜVeFˆË•Q_·Uë[{`°iÖeÖG@€M}K_c”úAUÇ¥ÊQ ô9y<.‡åq—‡©M[á tßœh>« ú—^¯Œn‹ÃužÄüä³ÝÚ:˜¾>ÛZȧnÄØì¨í#j]ÊýpDmGÚlSÏEc>iþ4>,ßšWÉL&½®ë^[˜×õ7ª­®– íí¤†[·=Æ*“/ø½òü±'‘þ£jíô¦gA@@@@ ríSG €$G@’GVL+#Èä–½>9²“÷dýÉ]SuÅ9âïä%ºÕ˜ó¶Ìéòîã[[‹1ZÏI§§Ç·ãú&­Õùõ£ÎK ­§ ¶,ø±-‰çñLWd÷‘õsÇÛÁ:+Þo#è7M"€ € € €X(ÀÈg  !!€qH§­pQÉÊ܈£Ü÷KވŚX¼äkÓÊì[‹Eç:áu:•(2æ-èŽ2†¨ÚN¯è>KŽõñQµ_E»yy‡¸´8Züáðü®û¼z¼ùúã°¸®ó>‰3eC¬réSnÛp ˆ@@@@`L€ä3/@ßÚÚž“)iÿ(Ÿà{åeV(ÙïwªlörŸØ-eîÛâ^zë³o)R’€VÉùmÔm…›?ÛVEà»÷÷ï1l†¾VEQì: ¿—ßI©éßÛÜÒº&ŠªjSFš«þžsªªÃ¿WÍïý«Žš@òä‚·½ÝѺ7*m^+B$ƒ$5(Yò¨—GFÛ—ÍòÈ|<'eŸ}ž–{Ö?ê¸ún·¨ï*ÌŸÿ¸ÒÚ»x‘@@@ Æ$Ÿc|ð°Z@«Ÿ+}òYŒšÒéÔé¥[íåspÞÜ’t[:ãdµÒïð¹z›ª{Z¹©åÝk¼—a3üéöÞ1ézQ)ó“TÊýbœ§G¯ï[öv娃-1ÿ…\X#®, €@H‹9éÙG¾YÓ) äSŠEuˆ$‘·5¾ýÿ c™ám‘R’˜6‡{?H"ZÙ_וéïY¯ú—þVÖßP,º¿Þ>+Æ„µ±@@@ÀJ’ÏV‚Bâ/àèâפ¾+=q¢îÜKö#CM%ŸÇÌe´w!›mÍ4¤¾/'r?õqð»}­Ì&­œ9óç?áwÝq¨/Ý—}…œ²Ëqý½£ô‡Z;íd1:Ž+£ì½t‰ ‹þ… QÔ€@6Û”I×½_9æ£òâ €Þ÷Lt›h¶¥R)•Éõ<èÝžaZÝèE›NëZWÊt;V^¹[¦˜–ïœz¾x„<å±^¾ ÝîuuaƬËUsó¨Á'3ªLÿÒåÛÞ/“ÙÁ{%Y_>ØÚñ¾‹S @Bˆ&}LÌI!/ñluÊ@kGì“™•¾ö´N}YöVéþ!í7$ òOåóÅ“’p¬êsK‘ߥ¹!ÙMÕÌù–ö›§*Äv@ *\®!Ý·ôó™Lê1áüí­‰çªj,cgó*eÌ×FFRoÈuEn)²{;S|hèëiË3È÷Ÿ¯Juo‡—xö–=dò‚·ËE¾7lYwwc®çè­«ù@@ F£q@’+`Œù¹-½“?aK,QÄ‘oíø¾œ4:IÚŽý(a/ñìg^~î‚[¢°´¡ÍÆËŽ‘8ÞmC,“Äð7óÆ|ë‚ÿKÊÔÐrÕæ‡¥¿Ò- m~Á½Q-8„€@‚2}Kß•QÃj­¿(ÝÜ3®6J’ë³…BꙚ»#Â8hšÈäºß-Óâ÷H§'½Å‹ü~n”ûÛ†Kg×E@˜P€‘ÏÒ°@ Zz§Þ;Y1\m=>íßRß›µå>­>u©¼j$ý‡iÓŠ^Ò2WÞžV•~V;©7æuüƪ¨BÆuÝÿ’&íH‚ŽÛwý«éºþØÁÖÎ?»9Ž+eÊI!?ۖеJ]mK,ÄɘÑÓ³§$–É.?“ž`Qïö‘‘ÐÝ2›ÍOT6;Ý¢¸Ä lÉ|©t¬¤ó†rçöÝŒ«—Ï\¶lfâ0è € €@Ù%}‰,»Vv@@@6¶´¬—)w¯³C;:õQKb‰, ùÖÎyƨ³¼CY •5üg™jûø¹íwV¶{2öš¾<ûj鉵#¿äÞk—ä÷]ýŽ­¿ÿÉ0÷z!÷:$)Þcýa§?iKŸdzË+l‰…8@ 9Þ½]•«WIöµ¿WæÄá‘ÔošVf'Øþ~!ö ¤û²¯ ˆßVIdòå<™™ Uɾìƒ € ’ÏÉ9–ô°R ŸÊ,—ÀÖY\£\ÿqKb‰<ŒÁ¶¶§et‡ëêSäDѽ‘4~ëdŠÍƒ­çÈ}ƒó㩵™•Ýeΰ³Çú{ùyŸHâ}ˆ2ug‹ù>–¸»Z/µ$Â@„4ä–Îßvo×L\º$WaQ,:שÞÞq‰™8ˆƒ€ã¤N®"Î=꣪؟]@@ @ò9‘. €V Ì™3$ƒ4jKŒ’dý8£Ÿw>Cmךf½V¦±ü€lybç­Ñ=““ÊW¤Fôáùy º£‹Â®–uQ}H"²o4‰ÑWæ[Ú?a—–OѬZU' ›¦¼&ßÚõ”O½£@@¥ûzÞ,÷kõî#Ãóú˜LjôkFðS@ï_UmÚ©nÿªgg@@bøÇ¥ lÄ€ PŽ@J«‹Ë)pÙÝ ©â¿ÜFüªonœ×yA>_SZáPJ=—ßwM(-Ñ$_@ÞãŒSôF<ûñÙâÍœòS¹OìGmN+jýjÇ­Û§~ÔÙÃ{8Nq_×-â*÷T™çcRögòX-ÊczòMOÙÐÞ¾¡òJØ^,`ª›aŸÕíÿâ€Xƒ € 3º˜ÅK¸ €1Ñ´‹µV'Xþž™Œóa¹0Ó4Nt@-r”ºV6_»ÇõÙ݇ u]®2óe*ì·ÊºôD»U°þ1û 5êü,?¾wâšemœsä¤þ[#Y=* „Ó7·¶®‰¤õM¿îÈ3Eüðš*± }…š}þH‰…)†L*ÐpìQçÈmNœ´ÐäGä»ÝUr¯èŸäçtÞºë \•ôˆTyÝXµrÅYzÅÒ7ÉçÛäùByL[_Ú??ÎܨygK+N)(U è8¿rŠn©Åw-·vpȽ{וYr×FÙpÑØcÕ¥éôJ…â@IDAT–¦ã•Ñÿ,#È•uGÊã y”òBòÙê9Ñý'¹­äRÎèus»ž•u,SÔ¯\öOªèV“ ˜¢…J6›ÿ–QêÖÌhPI&Ý'›.I•E“– y£ëXu ƒ{Os à«ÀÊ+w3EóÕŠëÔ:kFô§óííUQ‡)(u“ìSzÙ²ÿÔÓÌW”1>£Õç -_žªÛ@ 2¡9íÍäz~%=¾­ÜäBÕ eF. )®Ìòùiƒï™98£œ vÊlá…â…:3W+så k&ÿIÝ:½¨7y)¶n¨«›à:'ê§@¨\ ”Å•×Ξ €l8õÌÓßý3mÔùÛWEüÿžÃ)÷“÷ ,ç@4ŸUÅ7Ê.Þcë²jU]zÆôôâKuÑÙ[F@M3JÏp”»I K¢zµ2uO ¶µ=½}þ/O UtÿŪ1ÏJÝ’Ï»ÿW^/âU:“©“‘xæå¶D-³G\;4·ã![â!ˆ·@¦˜ö¾íYA/ò2Âùì|KÇÕì;á.…­Iìw7ö/½Ü52˃R/§°l2ï/´,¸dœm¬BÀjLÿÒåò¸­” ÇyíÀÜö;K)T¹Gß¿ËØç[¥þée´ñÔôQçƒeì@Ñ äoë°î7Ð[: Û•¨«‹[6´/ +¼’㢠 €„+@ò9\oZCjZÀUú{)elI>Ëùõo ½½?$)Zå˲¹yTÒI-ÞƒÅo™–Ôô÷œéwµUÔ7Z,ÏNô¨–±{«.LI¥ô÷ª8f슼 ͦ¶Ýwù…u¥ý$3˜è“%ñü‡ÒŠ—_j eÁ/Wd.º©ë%ÕqÄ5äe6Š…ù–+vXÇ À@kÇ]™\÷YRýOå!¹èÉ»IJÍgV©ÉØŠ €ÔŠÀ”_ k‚~"€/0ÜÒq¿7z/ø–Jn!í:£_,¹4ˆ@ ae·7½¹5#p•6ßžßu_¡5™Î¤>.7ê.´viè9í¿ÜeO@ŠêëëÞ*;î_îÎroæ…ùÖàÏÛãñnÉQ¯ëO’çwm[·F’Þolé$ñ¼‰ÿA@n¯"³VéijõdÍÉ…"÷kåœ48wÁm“•c € €@í|®cMO@+RŽù®l BN–œ5½¿çp›b"v0E½`Ççÿül¾8íKÇhó2Â~ò¾ð™@)³r¹]Ádš[Ëf^/³GkR)szùÁ˜ïÎk¿¦üý*ÛccKËz3êÌ—½ÿìºÅ7…‘ô®,RöB Ùƒó:zó©üÁr+’ÏJOo‘‡Ì€0¶¬—ïK7È…Åç6Í:Ê)½m=ÿ!€ €  ˜v› €@¨s;¯kX±ì>cÌ«CmxâÆ$n¾)›çN\„-D( •5ÉgI~YµµmŽP#ð¦3ú5i¤1ð†Jo`Ã`ýÀ奧$ 0¹€$‹Þ>y‰mݘN»ŸÏ¿hu°+ÆîmÌl.¾ Ö™Ú˜R`Λä÷ßû~ä=$-7/⢸1 þA@_€‘Ïã»°@ (ïD…Q¶~žÓÐßMò9¨cN½ 4õ-}ìüOWà1ëB«´«¶L_÷ñÑ{íŠJ-V§ž¹}”‘e¡ÄM Ý—}…Ä\î­~ºþ䮑ô•W$ì4ŠÀ¤ü^NÊÃF@@¥H>ó*@]`0?z¥4*÷ï³g‘Q@ßW«.MÛ‘  TQ«Smq©¿¦š›Gm‰Ç÷8ÆFñØuaŒôqDSßó½¯Tˆ5+à8©#Ëí¼«Ü\¹ûP@@@ vH>×î±§ç €@t]]yIdý0ºÆmù•éÍ3þ}Ü-¬D "­õi5½k³Oæ‹Wìº2IÏÓ+ºÏ”þ¼Á¦>É4—åçϦ˜ˆâ- 7?¤ÌŒ 5mùm™ûP@@@ †H>×ðÁ§ë €@”ÓGoêí-QưkÛ’èûOI@•;å®Õðr¹‘’?•U[‹ù¡êê®¶[÷ŸÑÓ³§6Ú»÷»MKѸî7l ˆX@ þró“™eöâïªù¬B™ûP@@@ †H>×ðÁ§ë €@”ÚÛ7ȽŸmýœÑ®uÓîFy˜h;B´*œ Í×GÂö¦ u#ÎEÛŸ$ñÿâtã%ž÷²ªoZg‡Úº±*&‚AØ ¥g”Ù‰çÊ,Oq@@@ ù\ã/ºD)P§§{ Ÿ(c§íù™þž…ã¬g! 8o ¹Á‰šëÞÜѱv¢q_Ÿîïn–æï³­ÅÑÑÿ±-&âAhw¤Ì^Ô•Yžâ € € €5.@ò¹Æ_tˆR`skë¥ô¢Œaܶù7 ï¸ÛX‰@Hò%ÍŠä³Ñ겺~3«.MË´„ßð-jÓ;<¿ë¾)J±([@n1PîE/-»v@@@¨i’Ï5}øé< ½€žf¾#QØv/Á—§™oG¯C5+°jUQÊ›v;êåÉÂm÷ü:ê ‚j¿aónÿ-uTý•Ö«‹ê‹•îË~ €ÀdrAQ¹3Y¨²Ù¦Éêd € € €; |ÞQƒŸ@B<­óýlÝÈCIüÙ¸¢ç´ÐAhhÚ¼öUò_£ËÕ¢E®qøB&·ìõ’„ù´ïW_áÒÁ¶wT_ 5 €ãõü8k'[å4¤§8Y¶!€ € € °£Éç5ø@ Çýš4\î4ÇêºæÂ™Ë–Í ¼!@`Q¥_·ËªHžíf#i8èF³ÙŒRæ i&tSeÖ_tÝ╹Å@’´cþZrámcÞ[î>”G@@¨]ºÚí:=G°E``n׳ ¹îoËhco \›–‡¦ïžÔï¶)(bI¾€$^«dXnĈ ù}Ä1Ò|&ãÈ/Æ]nÕ¢åþÚCm]ZÁ €@¢G§=˜qFå+—*ýCƘ…Óû{¾<ÜÒq¢0¢êŒ1ºqåò£Ý¢iÖÚ)Ãû<:H»ÉcûçÞ-iòòX-Ûÿ¦•yD+ç~íº7 Üqi›•Dn7Ò0¸öUÔÞ¬$G£^¡µ~…|V{÷— ÆT½<¼Å{ínÚöxJþR^È÷ËÊ;är²?åçt>)ëX˜X@^k™Íëf+Gý³¼ÎÓJ¬´9H^Y{ÈNÞïßö 7Êσòø»|'ØhóHÊÑ· ŒþNuum‘õ, h\‘ÝÇ-:'ÉÇ£\lQZ^Jí+ylŸ‘jD~ö^Þëã)yÿyØÕêǘ{œç÷[::Ê]Dª‰çR¿¢çPé÷©ò»s¼ôàpy ½wèg´A<ÿ¦”{·¼·ß¥{C¾µëï;”ñíÇL.û2­êæ¸Ê«:F¾å(•{¿×Ó·5âÅã}®>&¿Óå> µó›|ÓTÍÍ£ÛÊ$÷?¾{”vlùŒ(ÍiœR™åËÔ)w®QæDy=RÞ_n”ö¾ÛzËù\3öžiÔÍ…|ñ«òY:¼uSˆÿò}3Dì­MÉ1gAâ!°×¼sÉØl‹VsÖêÜ%—ÙWìâ‘û f2©G%îÿ`±¢ò…éŒBkÇUVC5!Éõ¬’÷»·DÚY­³ù–ŽÓ#!€ÆÓýÝÍrBâ×T]m•’h(¾*¨2Õ§ý÷œwÎwåœÙ³œän_›[¼Ü¶¸ˆ§öú{þbŒyu™=¿5Ÿ/žɉ¢2õ£x&×ý˜ÔóòRêÊ·v–t^%Ý¿äDÇ8g¥æH½^RµÒe½ìx\°ô³Á}Ö\£fŸï%Cb·¤Wt¿\>çy1y“t ]m'¤ž¿H}ý’$¼rKKǽÕÖ7ÑþéeËÒuîß&Úézmzó- æûƒ7ÔPë½ÞJYî’ß…cJ)zù;3©k“ïÖï–_Ö7KûÛ‰•„R”þ$uõhwÚUƒmmOWRIœöiÈ-//+5fIê5ZÞXjù(Ë5õ-}MQ;gÈñô~gó!–?ËçkJé+ s;÷¡¾ª«(õó¬”ã¶õ=¡x¶$mÏ©à{Äö¾üI¾_P¨Ë\©æÌÚ¾²¢ÿÇÎ!9çH‚û=²ÿqÕ±u¾~ÉÍ^X˜×é߈¥+¯Ÿ|÷¨ðh·[}Føú:[ui:=0ãÝÚè3…ö$y”ô8ŸÊ¤«þýïX޳.Îß7Çéθ«ä˜ÊWà’–Ð¿«1ò¹¤ãB!@À¼+Èû»¿,WÎ~?ð¶ÊlÀQîä$ÓÍ…ööÇÊÜ•âT(`ü8ùQaÛ[w““·7TU…;ïvmvÖȈò¦Û¶oÑæÇù–`FØ×Y"B(Œk~%§†ÊM>¿^.\š_ui—j>Ë•ËRЀܿ¡¡ÁyŸŒ²úˆœl?¼Ô3CSTíä:]ê;=óÌ^ëT®çÂ:5í[›[[×L±_ô›s¹†´î’ÄùyÊU'xùd2Ö79ãùym¿¦ḩåDÜdÄø7[:{e£ŸÍDïH% x#3S®ú´ŒÂ’‹)M5 çÛóFFËHO}¼qF¿Þë¹¾èªoµu\¿c!~¶XÀÙ¸e]—¼&>YTZ.˜ðõíáu2kÃëäýíË™¾¥Ëí|i µã.‹5J m÷þþ=†ÍÐg†”û¯òÚo”ÄsIûMPè8ù 8.SÌ/Rý=Ÿ”‹¯ž Ü„«½$øpû)‰âƒRhæ„KÛà½7œ.§gú»o–O‹¶vþ¹´]--Åw’ Ÿ%1_ÈA¼yÝyfËØì•ûŒ_hµ&ðÄ3ß7'Ä{÷|[œö@&È/°@D¼©bdtÃOU6»}¶ˆ"¡ÙZØãúìîÒÏr¿ÀûN3ªÜ›|¯4Ê årö‘‘Ôb á€(ïm99¾©ÎÔe¼m¬Cüp³¢Â:[3›w»I¦¶Ü¿Âýkg7ùÌI÷u¿WöÈ?’äÞ”¤A,³$qò™Q5ü¸$[¿©V^¹}zà ڪ¸Nï»ÜbgQF ?)I‡K¥¢±ÄsÅ–¶ã ÞhÍLÏí2£Lí•¥¨ï;J~²ŽkÄóÙÒ _‰ç]c—<•9EÞS¯“önm\ÑsÚ®xn‘€÷¾œë9SÏJT2«™—xl‘Ýu‡L}GC÷¥ ½½ûÖRÀ§û—ž-‰gïͧäáçïÒʘŸK’¾{Ûß¿%õDŽá{dF†‡åsõ³²Cµ‰çÛ42¿R·eú—~[ÎýlŸ®{ç26?ã»GIG‡Ïˆ’˜&,äÍ!ÈTú?”B•œ·ònŸÈÂ÷Í@X«ª”äsU|ìŒ à«€wÏ­ÿË×:ý«ìM ™ÔûW5!0¾ÀаsÈø[B]»v¸¥Ó;1“˜%³¢çcÒ™ù6vÈhý¥XŒX³˜@ lA×›Ù♲wôvÐj¶ÜõnïäoEû×ÀN™•Ý4¬èù¥$Y½™6ÂJ84H[ÿž)fù2¢Ï’å¶ ¦eúzþ£PHýUNèA¢òFm‡½¼Vô7gúº¿£$ž°§½¼)xûº¿æ8©{¤Õ…ò(iúOŸ"<ÎuÍ5’„îM÷e_áSTã“@cïÒ£äo›ä‚ÏË¥ÊWúTm)ÕÈ]ÚÔû”3r¿wAR);ØRÆ],IØå2®wñnpïÝ’¤—ψ?z÷lž´ï2šW>O~*ÇðJ)·×¤e«Û(MèOÈÅc¿ó>Ï««*¼½ùîQ‚5Ÿ% M^D>ãÞ]Ôú6)õºÉKNºu`Ò­•läûf%j¡ìCò9fA(U ?·ýRöÖRˇYNNš}ž+ÚïͶŒ«ºçrÂüÏI𢲡Ù±2£Þ7¢v ý‡dÖ‡L°Õ €€ÿ]]EùNSÍûÎ,ï䯌d½¾¡w©$öX¶ ¤sKÞ¢ŠêI6œ¼}]Èÿï+Ÿß¿cs¹’)Cn{§æúºkxvï;•6ÿ'öÜicøOd¢úxæé½nhZ™Ý;üæi1hI:Ÿi¤³VŸ–¶¢¼È`žÖ©{e:î÷Ýgê/A@F‚zÀ¸Ž$KdTk {Rdl&5¹ Ifbø…Œ¨m ¤+õ. ª3”$¬Ü+=”En9•ºq¢d¯7šQ~¿¯—Ï“3B‰fk#ÇÉçùïåök…ØfEMñÝcj6>#¦6šª„$ž½Ù®’Gzª²SlÏO±½¬Í|ß,‹+ôÂ$ŸC'§A@IÆîÉæ|DÊÈyQë-W´_5åU¹Ö…M@ñ0“_õBgŒ+Éç„,ÞÉ cܬt'Ê‘jJçÊ›õQ Ê{Så=_M“òEííÆÑ–“QËÆN|VSYö•©Uçiå\'] rDVIRrlÎL«á[ÒýÝaŽðÛ›—ìéïù´LÇø{¹/è«K 8¬BZ4ZLýzF.ù1 «ËµÐN&·ô“’t¾Iþz<È’þ6ÈtÜŽ%{{gXSÍ…1–°ìï‘{¾]cÇ߯ty#jmž†ÛK¶Êrû%óª_4¯PE³Ríú;#R†R+"ºxà@]g®µõ–Þñá»ÇÔ¯R>#¦6šª„~XÊ|}ªr%nßRb¹É‹ñ}srK¶’|¶ä@ €À ùÖö[夙7½“‹Ü[/µ4–÷²Q“˜^, õ/^îùýûK¸-ÔšüA"Ó¸ýTjÿä{ ]’‘q×¶.XYBQŠ €þ Ì9c“ŒõnGàÇ2_’®«d´í}rrê3µ8ålCnéyO_*˜v$8$™oø¹!íÍ+–éÇA.©IÈÅKå>ž_“òu%ír!ÏEŒïò!ÃÑ\6›’Æò7£þ†ToßëÍK4:£76öö¾4ˆîSçÄ ×vï+üVJ´N\*²-Ggô·ò,*¯a•­ëÜkä§ý£‰C™I/ر팹 ¢Äó¶0Ì«ä–ÞtíÖ-|÷˜âð1Pi›åBÆfùœý^i¥K*µ¹¤R“âûæd:Vm#ùlÕá @íu©âçäç ÛŸ[öÿq2íÓw,‹‰p’"`Tä÷–’/ˆ$³aŲEÒO:y¼£Æ?îýÀ‚D!oé¸ZÚõ.Ðñeùÿì `ÅõÿgV>$'! I¸¡Pî–«%…r“–Ó±ìøˆz%$„–-¥ÿ–÷ '-øq&r¶ŽØrÂY…r5œIiËUîqîX’miçÿÇ Nb[»Ò®´+}K»oÞ¼ùÌjw´oÞL\:§~ˆ©7à€|*ÔÙò/Gw9Òh(ÝÑüi¤Tõj†L3õpy{SN"Ø*TÔPç[õª0´ƒœ›_ 45•!‚´Æçx¼ Ÿ5äå‹Úöö¸cÞèÖÖT"á…8ÄÃÚ[¤äCc;;Ý[K9ƒÆWTô’dPÔ¹"J}%ÔÑÑ ±NöÙ9Nµ=\;¦!“Á—‡;˜ý{¤¡Î{D@/ºs;LdÔ“/ó!b‰·¬Óns¼i±ÿ< æØ‰ã¶Ð  "°¡2²‹³]æÙ&)q>RÍö¬}4ÌÏvÈ·ñ¥Fùkù¶!Ûúáø¨EºÏŸf«Ç½òòšžšÈÝÓOÍ$@$ž@<ž:R¯¤—´-q$"•®At×{¸/ATôÏC ›÷ "%e茲M(w&F ºÝ}MÈÞãî«®×jÙ¤eükŒÞ‰t¯ß3 yzJ¨E*°GyÒ¯_%¢l’azñépH\‰É™ßG•¼0m;Â]"Ôî D³…‚Æ=Ð=ÕýÎ+ý¤‘2ïg´ó`·Ñxÿí£R¥HÝ,DæÎ~%– ¼Ž¦Ÿ.MõYY*vÙt ôÉR çI&\éÉòQ¼2t¤¨ýûTO“gî‹J g£=ùߤüSpaË'ÀÙ;þ•ú£—ÖëæØc„Ó”÷ˆàØ;T‘ ]ŠŽfæÃ¤±˜=+¶•æxs[&^Ýã½´4^%E»H€H€rN OÞˆíç¢âCs^¹… ñãì:¬qøJ"<ý â!«&XtI.±¶ªjµKºs¢¶lAÓ§PÑí9©,³JމǓ—gV”¥H€HÀA‘ÈsakØ0Õ? Õ­upÇCúÃ…):üà•¢£õ^ÄO,,ï“÷­©­õj–;÷µ# Ù.¼ƒõqS¨ #ðf™Yº*L&µžD™9V&Ån* öÁDÌ#q2ecT¹>šñ¶o__ ο("‘TÆZ,ŒW×ÿ0ÔÑR޼ßV³{tÁa}'œ wĪêžGä4š»å¶•gGó{¯f¼. µ·£ ñ#¤Ó®Ü²TÚO(bjÇÆ1i%·H¼øâÛã=ÔVÄb¢Ä¼ËªJšÇûJ–nU­¥k*Ö÷Yô±P0dÜ€‰ÙFد‚‡ñ{,ebŽa¾Y&ÊWoþúÆKY²+úNLy”TòÄ,×ÀÝÐ`=ÛcEMÍzã÷®éXn'ØÙÚÏÀÈ•ø~^gâÖÄÔú·¶.¿•§ä#_†×Â~¹¦¦P0hÀ5ì|¶U7.v'?øÒ„¿îוǤ”ß·X}/&ÿÜ‹ëöCøûœ!Õ¥½¥hî_¥æöB™{a)ÍA_“OÐû3Øv’¦x åÆY) ŽÿÂwù~Üwž’J½.Ôû›lê5Œ e˜»Ááõi)Ô¡¯¯ÑVôn%³3²ßÍÂýèê­öçë#ÇÃç=b0ìVR\’A±‹à~ºvD‹‹a¼i…§Åè|öt÷Ð8 (rúáX´õÛø¡ÿ˜GI”â‡U Ò¨ÑSYûºGm¤Yþ#àÖë$Þ·*èE¹1­­’ÕÛ2y¨“&áÁÇ·ððCN*c%$@$†@ÏÔºWF-l;Ù4͇!jË¡–FõP‡'ô§ÐT⌞•DDô#¦Tw&bfK\Â9qCl§®Ääó¶pölIj >j燞ðg}H§×D”Ó ¼ý^™FYŸ¬h'ǯ´N7·x¸îb¬„3@Û;Üö.Ž_ɦóÐ÷½Ã ¥Û¯©×œ¦"#Q5&†bý_[“(ŽFtÝñp6ý=]=[ol4ÑIºŸ,o¡Îæ>Du[’È’õ21ÃR{íUt´œGÓ¹vÊ ’EQÑgÙÝ1ó¡­'b ñ=üäÇ«ÿ{8*Úz¨)¾‡jöeò=<k@ß çUÊss˜Ò"k'‰ÝI(ÝÈ”ôëDÂü#·­æ¹X4åp]½kÝßZØÖ€¿Ú9¹‹ÅÒð—Š_à^x,\ÿœÕ2.ÉI£w-¾@W••¦®]wZdÕ0²úºø^Ãëwß™kñ9nù‘vï4ÒÁþcR6Rþº{jí #Èj›ôïÛgðºu\[Û¸ÞRõ}ôû¥ølÏ/¢Ä÷0‰ëÚ­¯Ðãå­hƺxpüT,µ Qß[ŸÆ„”Gñ]c»ÿ‰2ñnYÜ迦öF¹i¤>‰‰!ŸÁ0èD,è´$RðãMK¼-dï"ëí¶Ð:  $€ÁÄãÁhói^å³åÛ#Zçø›>¿úäˆ#3øòÙÖí n?øO×ÈÓ xöø¢EåI3ÞŽ>ˆRñìÖ 7D=k #(JúÁí¨… NÀúÀ÷À®9‚P‚§U'!â$dº¹^u´Ü‰úÿÔ;-òrŽêÏQ5ò1išÄjžÏ´Â Õ ÿBÙïã¡ù/{JS?†óB|¶ý<1Å?ÃDƒEiÔgjæÇ彜Pê|8 Ãoí(LAðêxY÷åâÔ³º?.”Ý»XU}ê;m|š, Sžy{ÎçìLeé àÜ= “dþ”Iq8ø2”ºpà»”‰ Ñ®{/þ2©z ¯á·ðÙ°©¬>mþv<ÜpÍr@ÅÂæÉÊWŒ ²í!)þ¡ÌÔ™‰êˆždý†ë<-ó1öá¾R¥3œjQi÷Âëà´>f¨Ìu¸-5ŒÔìî©‘úCœ-ÖÖÿil<14ù +qߺÈb1+b+°4ÃYÝSëî³"ãñ­øÆ¼Gäü”ìÒ×.Ã,¹%VS³MÃV#ô3¦'ñºÞQ+9Þt§Êì’ܰ:I€H€H`D‰@âR¼3¢P~Г(iÂ,زüšÁÚ}O ½=ÝŒó\4q›¹¨4ë:j/d&n‡ãÙvú̬붨Ïë°nÜw-ŠSŒH€rJ {ê´¥¸F}×Ñ|8â*<;ü+mYXÑÞü™œ6ÞÊzàxýf¼ªö„lσMÓÍãU — M:Ršf´Þq‰©ÌÜ8½ôÁ¸ùMØ÷ 6 "N×ßIÇó&ý‰ªú7Us >¯Ü´/ý_³V,^lÛ‘Ÿ^/%%°ä¦Ò”iÞå6õÆ”Tg#ªôälσë\w!Ýçw¥¡ŽÄ~åis“W–·7íc³Ň#ÐØh(SބÖ¿Çp˜Ì‰ïÔ5Å1Çó ÛÖ×Õ­LŒÞ¾ Žä¦A»Ó½=*ØÙöÕtBù8'ú/ãUu5ÚñœQýÈûÖŸ6ë>rb{ÕŽÎÄñ<¸òx¸ö™”P'bŸû…i|e°¾/αï9>Õ5Á`jŸD¸þgC9žsj Ç›9Åm·2:Ÿí£< @î Tž±$õÃ+ÏnXCèDíÌÃŒeü>ãF™@þ¾ŠÌJ:W ß5šÌw[¨³í·øþE¼m¸qiì´úåÞ¶‘Ö‘ 3}Š?·t "”túÒÞ|°À@ªRò¹P´¥5mÚ#68Pg—”Æ1±pÝnD²u×4¼''ÃÎ…¶mÅ$­PGs½ír™À:ñxê,½Žøyx(²éÈ×6í€Æd¯V+PBnZ¿J³äæa¡å/Âµá ›&¾òÈDUßm–³$›Ú°¤<Ù¿¶íbK>*ÇDg£¿>Ö]tï*&< þ¬†ß‡É¹[/`£|zÑ)S’ñXòLDÇÿ#½ðF \·.óÚ³¬ýsLÜø‰÷±„šØMšŒ0!M¼6xg¦ï{à ÿAö•³í”Çò'ۑϱlÑŽ=xÈÙ™ǵªÙ;.ðTæGŽ7svØ­ˆÎg»Ä(O$@$H¥§®Ý•—Ê­WúµÐ–?X§$ lI¡-÷äþæOlÈ}­ÙÕXmÅš’J;J¼¼={ö¥›½l m# ~:J©ºîJÓc}æöÞåwàðDÕå<*|SëãFè:¼gÓg ¥è“u‰ ÿ¶ÀA;âm87­h¤Œ#ÖŠ¾ùºM]—æsÜ‹%{?´aó Áަ½lÈSt€€¡„å‰Þ˜îNºf˽aF­ŠJ%O±*ë†2ÌŽÌV& ;vÄãÉg o÷7o‡@–;õØ–5ÅS–Ë(õ ˲. õ؃÷—Ï®õR61‹ÛȨ9ÞžÏóá  ÐiÍ”!΄y^_?µ‘™Ít@{ôDò YÉ>i)%›MÏ™Im‡ SvbÖüv9«4óоGÞ˼8K’ €· èµUõ¿BZî#„Hí†Éÿç#ÂëXÝç¢å{§JÕ_R®ºX‡]ÕÏ‚ÃCv 9*_Y‰5…­Èk<,kpÔ)3¤Xé1“hŽ ¡ŽÖ£!ng=îGÒçÚ¨ÅaÑHd¾Wz"ˆõM¸\“uZ%/.AÜx½Åbv/Y–Ÿ¨çKJÍ¿[´U‹hCÖqÑT@þÙq¥ƒF"½ÈðÊà]iß+Õ™V&K€a¼lCÅ6dÝ-ê±ïnžZë6„éî5áãª|ûŽãÍỎÎçáÙ𠀇 $¦Öÿ3áëa7™¶ÑÝÑZµiÿ’€— À©;Ϋöéˆg¡Rþ‰^µq]3&£5Íéà[ ð =Ù&VÕpRÞž ¦&aÞ,¿/øbœÝ ð¤ŠP ÑY­™kýóÿ2/í\É`yêVhKXרŽýNë%ü$išvÖßõSËŠÃV§Ûj©R×Û’wI¸¤OÞ Õ–#;ñ›Ö^;]²ÛOjCëWM¶<UÉÖÎgûÖY…ú­f¦Ø±|QÛÞy²÷õž©uöÃj*ó-;Åd™|ÌŽ|&²øÞ¾k£Üö6d]-ö±ï®Z›#kĺßÉ•›­ñîŽ7‡ï:Ÿ‡gÃ#$@$@'Û¹ër˜ø¬ÇÍÔæ•*©Z탞ʳ‰e¡¾uy6Á³Õkdz4Å–4å·%oÄÍ’Ùù5µ“ @þè,5‰êú¹˜„óDDï.”¼Ö¼ä¤Ep@ÿpÔÂ;©3C]ÉrY¶ òŽÓÜ…TvÖÓ UÄVæ¨TFÀÚò'ÙP•ˆ'¬§6¶¡×¶è†ºº°ªú£6 ~nüƒMcmÈSTŠã-Cæó–e]”¯[UH¥òroÃ}u±U³‘3„±ÜFù®Øiõväm¨þXtMmí|²šn¼\D£—ÎË»¢{ðáþy‡g/åsI÷[ÈÜ&€4%ÜH€H€HÀ§&Ÿ×g.lýªaªçЂQo…Ž€n F[ÏJ„ëîò¸­4/OŒx©)Jò:1_ǧynÍP{Ë1ÒT‹|âxF*9#"jjÖçé4bµ$@$à)Ë\ £® E[Åæ»x¯Ó=#à/«­Ä4S?‡†Ú¬´d_ø±µUU«³WãŒ8:ÖúZÎ)‰”éb‰3µg©¥©)4*T²Ÿ)Ô¾hǾhÇîp¦ï‚É ;C³ÎÌ¢túo9^Ü ™@¿c§w²&>èæ:µ6ìÕŽpùE‹åŒž„q dY”§˜GY…€5q1À ›ZaÕ SÈC!»Àª¼SrXGûy§t¤×w;}âz$ö [µ]ŸôyØ·ã’É2x«cà ¸ ¸Ç¼G¸†¡8øŸÉIE¹®„ãÍœ§ó9g¨Y €tZ¨PGó¹BJ?¤·ÅR êŽŠÎæítZJ7xP§¿ ¬)/‡RV'\»ÓV%MO9ŸË£óOAKÀñr§ÅNkUß‹UÕú!#ƒÓ §> HKë±>¡ÇË4}*èO›==m¡‘j‚ [>åXÞY̽£R‰'ÝÓn_³©ÄÅ´“׆¸£¢£6í”2KrX1ò‘pÖ}Žçþ }›g&ÀsÄ­øŒ2’û!¥SY=á%JRÉ'윺JÊa?Ï;k杻%iôÅ¡hË–„="„‹à'óaŠiˆç¦^ŽxË×ö÷rc“žƒ-Ö[µª×0*`üÏùÙŠ}ìÁ{DnÎ;iËrS“»µp¼é.ß‘´÷êGà1  ð:xuÃ=°ñ:¯Û9`ŸTJ^êl½Ô'öÒÌ\¨¬ìAuyõ>K!=ã|ÆÄ’¯ -['˜øÄñ,Äà ~¹åòÌf]$@$°Þi‘—‘’;çÌÉ8`9éJ6~€ÏR|mˆý¹Û%Õ?sWYúšz«êÿ © é%$ µ—eYõÀP´ù8ƒž3ÍÀrtà<<ìŸG„N3ËgT0.f*µŸvÀyëèý£c»t½€·–×}†õ;í-jYü˜†ãyïg°K>Ú§R©÷sQ¯”Öï*¡VæÂ&]îE)«u™¦YfUÖ¹"{ðáÊYµR<zk›>ÙÁñ¦7:Š{oô­  È’@<žúTø'%ŒR¿AêÉ«ES“ýYBbq¿ÈÙìa€xÂùêl¹p £Aé0vzk·o–Éòs¼e­! oHTÕ?„1Üa°ò®L-EôÏÔLË:QN ãNèqL‡„_Fë •ÜݱºGPTÑÞü8œ[áp~øÑÏŒ ÎCEN@r;”á±ï!–‡‚ý–#6ñ€/;í-fÙŠŽ†¿ Sïã¾[>ú8d–~˜‹zMSZžl{¬žœ›M‰µ¹©(ûZŠ}ìÁ{Döç% )[ë³[Ré¶Ç›n¶§ŸÎg{¼(M$@$àU‘H/&ªêÔùvÜÙ ¤.&Ñ¿^b-tå¹y™i¿¹Íˆfu´ü‘lWmÞçý7 <hðÒšŸÞGF I€H`€@$²QÐg áåÿËÉbñ­Á Ëf]Ì¥–LYWfYzÛ²èÆu”mˆÛ-_Ô¶7"£xPüJêõ¹9ñÒ¢”–¦ØÎNÃ{6ô½cG>'²R`¢…ÕM·*Yìr)Ñ£Óú–—¬OkúúÖç¬Ä7Ü‹›Î¥â“­ØÇ¼GäæDõô¬ËMMÙ×Âñfö ÝÐ@ç³T©“H€H /âáÈÛ†T:õ¢o~4 ²³.$z½¨iR^ ±RÏ@²|O éEy³hQy(Úv;ržù*-=BÌfs缜1¬”H €`Ù‚ß "öO4©¤"6æ  Ê9Rd}<î¹sˆ¢ŒÙhœ;Ž{=™ ËÌ)súµÊ†=%œ2b” )Ñ?ÙF‰ˆ"5´kƒ´ÓÞXïÝ*JJJG{×:Ç,ËÏù‰XN;MK aÚøndSSá–-ú±ï¹9¹1A47eQ Ç›YÀs¿(Ïî3f $@$@9$Ð]Õp?<Ï?Ía•NTuT*x¢|a«­µÍœ¨˜:¼G@ Ù•o«ÌT`Ï\Û0ª½}ÇP*¾XH…è7?mò‰êú;üd1m% ¯ˆ^ûCØf;‚Ñ4ýóÖ&:½à|¶ó°ÐqGÎø›ÆÂñÜ.°Ì úÅçvÞ:œç‚Îá16ê±s¾ÛP›(&”"3—ÕMñ{bUJ%휵zNÌËy[TäcÞ#Šè\¡©oŽÇ#‡è|öHGÐ   ç$ªê®€¶¿8§1'šö1LõtEGÛé9©•x—€ùN»-„!÷Ì% Qí͇˜FòÔyT.ëu ®‡ãñäÐC$@$@šÀ” D<þÑ. 8yò²>f¿MMevíu[^ eÇ¡Üí¤=:›O<x:ÃNꥮâ"€sØNú_;ç{Î@"3ŽkƒLäÌ0ŸW%vÎ Ÿ·–æ“À0Š|ìÁ{Ä0çEíæxÓMç³?ú‰V’ Ø! ¥Š^7Ež¶S̲ã”4;CÑ– RDzÀšæÛy©vP¥ø1·ï ®¾­ˆ6‡MCþ•ìájEÎ+ÿ_i)Ö™ÏQz:çͧF ð(d Å¾eæDûeœ)1&²µ6­3µŽ¬÷q;k¢:çôŠF+R)ã bÙBKGµSü)D7ýú~† ça|Ü  qB@©ƒTÒØË0R;—'ñý¤ß²T…¼G@ ;“"ƒŽ)¥kƒ²Ó^ïõW-J&ûâ9¬ŽU‘€' ý؃÷Ož—93ŠãÍœ¡Î¶¢’l°< x’"gŒ…MÓL3 £)w÷¤C¥'†ý© ?FgŠpØÎš}Ckä^_ÀÖ7ò½h9Ö~ÚuhFÅäC.Çzx?A]~›l±™ ¦­;-²ÊuN¬€H€ŠŒ@|Ú´w0ï=4{WËM·ahY«%ASôi;», çLHڙеÆ)³B¢÷FÜÒËP_/Æ?‹ád^h¤Ô£ÝÏ/[&Í uõ v´Äü6ÀȦ½…T“ Öa  ¼ˆW×]…T€Wç¥r*EZ¡óB¶{ª¼½ Ñ,ÜŠ€€å*n±ÀC³#Ò=ºµu‡`gk'"±®…ÎR§ôæXÏݱªºç¸NVG$@EIñŽ;Øi¸Rfޜϰó¸±ãíØë¦,ØUÛÔÿO›òÛˆ#R}Æ6;‡ßñŽˆcºkr°|…Úqx3xÄÓN=«ö-±aãÉQ6ä]5Â6*0ËËSzr&7«”õsC*ã«j)G>"PÜcÞ#|tª:g*Ǜα̕&:ŸsEšõ ä@ü¹¥ßƒóónHæj¤Gô(B92WÃ’^'€ÔÎË<`ãNØPÑÙ25Uª–â„­tB_^t(ñ÷x<5éA™½3/ÀJI€ŠŠ@SS.¶ûÚksöÑ»öêÛBº¤GõNÛbOž>Œ°i,²ýœj£úÞx øœ ùmD·»¯i{ì´û³ßh5 @&M8Î@š®3)î‘2˜Ñ/¯­èl} ´¨e7ØD3& „™ƒh 4F+›i;·V‡èL”¸³Sñl/‚mkUyþüR0”ªv3-hžÛÇêI€HÀS‚!y ²å|C g¢7o›ê[y«|Pʼnž€Ž@¶¾L‹ÿ••=ƒTØ~ÛÛ[r YùD¢ªþ!Û•d^à³™Í_IÓTÖ×ìΟ™®×ŒtÉ÷ÚªDÊómÉ»$œ,U³¡Úò5LJ¹Ð%S Vm )žDãL‹ ôÌÄ‹öRŒ,(汇Ä{„¥Ó¤`„8ÞôgWÒùìÏ~£Õ$@$@™ˆDzcªLG§<“© /”CDÐIXmY°³ù /ØCœ%` •÷ÈgœcôGPeд`çüãB¡À x퉇ñ4aS‘×}òdF£lÂÁ¿$@$à>C¾j³–¾îíÖçÕù {FçѦÝΊ/ZTŽ%f.¶©´Í¦ü6âX«ö3Ûì~Gûð‡œ=‚¥jöÆ=œÕJm¹$¨¬{ ÓÞ´Qç ¡hk~×`oj™ß¶a³H)óN;ò”b}]ÝJüÎø»e†ü®eY ’€íØCwïþ9Q°”ãM'(æ^ϹgÎI€H€òM Ž•ôÉJ8×òîàËÅX©ä¡hK;£ ³$é±â†Pÿò€I²'aoË—'ª¨(¹öÚYküÉžpë6ÛHqM@*ë×/%Ž©èh;àH ÐíØCw$ï…v:ÜŽ7GæãÑ£t>{´ch €»ôlé’@ê ¨%ß‘2N4´QÐ/‡:[¾#yow‚hžuÄÑ÷`ª<›*ÃrWytþ)ˆv~y ÚÙjúÍ|7q¸ú»}>%QùßpÜO$@…B ØÑ2Ó0”Nûzlņí®Ïk»¤q ê/µcƒRf.Ó8dÚá¡a7òx$}–•G›÷ÅMûç– l|‰û¼ƒc­Ök*sUÙ¬äúÇÃòܬt8\NTëéÍMÁ¥uø+•œ‹·}Ö»CW±°åÖå“ÝÙzRꇶ4J©¯yÜ2 `¤JšPÌò¹¡¤ù'VdP•#EBÍ_AdþâQ ›vrD!•ÀFE9öØÔù¼Gl"Qø9Þôgóµ?ûV“ 8@`Ced…í\{ÝuùV1¯ ~ð“£.88߯°þì :èÙìµd§ÑgiÏ÷µìŒh¹; aÜÚlE\egk¥» #ðF¡¸Æ—ŠI€¼B@)YÑÑrî7s`R¿ÃYaf;[~’aËyBÉ»u›†a=úÍ®rÛòò¡ÎùGÛ.–M8S !ï {N¥þ”Mµ›ÊJZŽ–7TÀ²£z“þLþ'¤S·2“²^(ƒ¬¾µÝi~˜ù6x̳£—¶++Ú›í¤ƒ·£~hÙEwn—Tê/8X6´À{_‰/yI;P¹e@ »¦æCÓÌ­nûeï­ ;)ж(MœÇêDÓ <\Øb/»”“ÆPW(¾±Ç¦Nä=b‰ÂÿËñ¦?û˜Îgö­& pˆ@¼²þ]8 O„ºBp@k*G˜fê98¯Ìt½^­„›(ñT¾­@øòA£ÚÛ‡N¸xq Rl_¬úÄaç×òm«Cõ÷ 4J¨@IDAT;ž»§N[ê>ª! oÀúÀ¡…mwÁ©ó£­ ”ˆ  F[.ßz¿›Ÿƒ­_€-×fPÇã½UuÿΠœ[E°î²± ,Ú|€[l¡·©©,$zþŠ}‡o±?ý‡wãc&8â´WRu§¯n£„)Õ§¬Êf*7¶³s<’5ÿ>Óòn•SÂÜ`]·<ºláKJ•úZÙk£¥Êë~Û(–¡(&€S¡&Œ›?mGƒò d­2픡ì–¤4lM¢Áýí¼Šhë·¶Ôâî§ÃM]$8PÓNÒãwÔ%ùLïn«©=ÇŠnì1˜/ïƒiî{Ž7ýÙ·t>û³ßh5 €ƒ Ð]<ßK$¯TD[f1·ƒ'KNU©¼;ŸusS¾Ó¶n¶vTlXõRlÿÇÆl}ܧŸéxöiÇÑl {Æ´¶N¥âá¡÷W†+ 'Jc¨£õŽ\¤(­èh­Á²}°ÅVºmm;œ77׆<îŸò1ŒÁ>ëª MM£‘æ»÷â*»õ(%Å”)I»å†’‡3Çò2!8¯*‡ÒáØ>„¼öªžÛ Ïsimá$[c½ê¸a'ÿYWR0’:² çÎïl6hd²ydtG³-‡°Í:„žì½°ïT›eŸHTÕÞa³ Å·"«ªÕ™¢lµ{ÄÈìt^nbD!‡ê‰H˜®³CMÜJe×îß…:[[9a|+2ü˜)¢{ †Ä{Ä`…ûžãMö-Ïþì7ZM$@$à0t@kB; }æ-HŽ$ÔÞrŒÃȨÎe>ãi—«°¤ÑCáM‚:‚Qõóá$øέ7í/€¿t<@'² $@é èëx²T=ÉcÓJKuœ*/£óOL+›‰À’›Já ý"ÚP<”Šÿ$FÿkårQd"î“OlL%Ž˜n‡7I þ™‘ãYˆe‰ç–Þê”I¦¯ØÐulhaó‘6ä­‹ÂñŒ”ñzÍòjë…r'©LaÃù,¦‘ú^î¬ó~M±Ñ뮀•oØ´t×””Ï£­gÙ,gI¼¢³ípLö}ÂS,øX(e˜ê›HÌ˷l ˜†¼:ìL¦1ôr­?MMp»³;燉Hú~»ó5LÃ9´dT{ó!#Èð X%P4c­ð±5‘ÂûÌñ¦?û”Îgö­& pÀ&4žz)}£-ýŒ0Äãp6ç, ¤V¹Žõuu+àU`85´`ÁîH w"Hôw£Á69iÏNÒ¤. ÏÐÑpîgÃÈ}¤0‡:›À‘ú9åF…“ð¤àò‰ÏÁór3sÎJùc§¢wG46óƒåp;ßж>âÔú³ãÚÚÆ¡/~¯—XY™¤öró;N¦ú…ãg‰DÒ”·:é×ÔÒiä¥ß°cK.e•a®²WŸºçÎQöʰô” %ͳÑ”ÍVVH¡þŒ‰.–-hr$íû˜htb¨£å¥Ì§aË'mÚƒ¥íÅϺkAˆ›z¦Öé 0´«K)õSLây |QÛÞv˦“ש½¥2‚Üøt²8¾®£O#ûl ²!tŠbì± Þ#¶ARh;8ÞôgÒùìÏ~£Õ$@$@.Ðh#:Oæ–¹TE>ÕÖcöõ2üžŠ6í‘OCX·5xˆªgËç{-)<Ô‘z}4Ò½¶÷L¡ŽæÏ…Ô¥l ÀPBÍLDÓ·Ný9”ø¶û”¬ÃäLDz^?T¯¸¯e¤H®mËë=H­#1îiÄû=óAC ¦ß‹¨µãSkuÄ´÷7)ŽÇú³Ï¡Ý-S"¾í­S£ü•=%æ;ð\}å3¼Ë«á騭$ù5·ßIfð1Œ±L$ŒGÊ£ÍûÞŸé{,rH/Œ”F>SÝŽ–Sê-›úÐÇjQytþ)VËõ§€Ž6ÿPÜû(«eü$—¨šþ¢…œ‰Í8ïN Ëð=jÕÙ|j&¯£¢­‡bBÀÕIÑû&¦Ì|vØŽšÅØþÄ’¥:Š››ƒâÐO¡.‡þŒ”¹ “ ~£'dkRE{û.8ÇÚ‘Úû:è*³¡/ˆûÚõünƒEG&Pàc¡Ï{ÄPT gÇ›þìË °ø³±´šH€H€¬ØPY‡7Ç"Ö½/´ˆƒ~ Ÿƒg%_EäÌ %ªüWëÃá.+\(“{¦4#íõ×s_ó65·Ùãÿ¯+•:¹§:ò?ÿ7…-  46p”ü?H8p-—‡õ?Tï×Á½ŽÐG¤”KM™úÆ{%Fj}IOiŸ*-•qÑ»S‰2÷Aš¼CãôD8Ž…3ÍÄZ• ÌôaÊÚ©XkyjhùÄ.my,Ǩ #ðf™Yº*L&u/&Ê̱2)vS…¨syõSêy >–åö\<üQ–:†,މw¢õ9fq“‡!â%8ò®GÚõÿKTÕÛK§Œs:8ùà/`Ñïï#ªÕîZ»mtV,`¨™¦mã aÜgÖBpúK‰0^ˆ²÷d_ŸJ*UÖ[&&”õIDp~ ëŸ1‘Ç£†Ð¸Ø˜íäø¶mU `Ò‰^'Wga¨ÏÀ eaš©ä4LXXïáÃøŽ=–2‘:Þ0ß,å«7}ã¥,ÙÑÖˆŠ•Ga<~"¾¯ûgPçà"oDéלÌ<0XyQ¿¯¬ì1.8Y!ž‡ ›,‚¸G^ŠIßF›ïF_Ï‹?·ô;ýìhÚKÊÀ7”Hêɺ™Nþ˜¥×®µi;ÅI ‚{ ÕpÞ#†¢R8û8Þô__Òùì¿>£Å$@$@9 °úäÈZžT½-˜…|ZªÌuA<0¾?²gãÁßi~«©y?×F°¾‘ HCþÍvrÁ‘UòèF/úäÉê" <ÆFÓ\ÔÖ€è.yœsí•ãýÁpF ¤íW›JDª6ÕÓˆ#ðã£s•B™šŸ6Ír¤­“U£ëu쇃Êò¦£ëàÐjг¼ó‰äÀÓ©”À H¿B‡Ø½+Í’°WöX¶Ò† Jn@–½F±­H?tæÅp®_ çêKhðJK ¥þm¦Œ¥J­H™ÒFßö2©v3dÊøcp΄“jW &"ͼÜõ¤qÊ'°…™ŠtO|€vêôÀûe c*·©)4:%Ð…ç f”|·ð=Ë@©/‹`äxSÓ™p£_m¯µ<¸É“ðáK8—¾dô÷¾Üò{¸é 6ð=ÜDz°›ï?‚côÔõ5œôk“›eqɨ¢£õK˜¨ÑŽB™|§Gá›t.¾Lç†?øLNø>?‰Kô¿1 áP°õ†ÌP2)ÇaâǾøNÆ)¢'ÂcÙÐ!Õâᆻ‡<Ä$04¢{ ‰…÷ˆ!±ÊNŽ7ýד™ÜŒý×JZL$@$@™‡c‰DªsnϤ¸OÊ`f¶º@É7±þâx}Ò'v…™ýë Dcps€|,L¿¡®ŽŽg©R €· ôTÖâ!¥ ÃÊ„·-Ù:Dx^¯nhY*·G¥©¦£FÏrEdð:Ã0ÂnN2Ü8@þ) ò‡ètâzm^Þ]–˜ÿK–ª.¤_Ý«zVaâÄkxžü…7ãõuÔcÅñ¼ºÎÀ8÷ƒôv©íÒËd/ß-Ùk¡‰Ä㢬 $ö ÷ #pROMä5ŸØë[3cÕu˜Ðr¾ Ø :¾†ëÍu¸öü …^Ñ×#ýR}âý€R/#“Òk‹Ÿdëx†ŽF8žï€ÍTQD8ö¡³yŽ¿q¼é¿þ£óÙ}F‹I€H€rI éUÕ‡E¿Èeµy¨«?ÔÏÃëDeÜYÑÙvxl`•Cýktu„ûìhFÊÑ“û3Ø/Ë$@$àkñpÒ<›5hDÜ àÆD¸Þsã±XMÃóïžáQ¦]J?¥{jí nÛ'Ž:¼â\3ÿUÛé¼]†dJ1U ‹:·¬ `’p<ž:ß½{²Öå¢8ÿ%R£tT®‹ÕPõ ±êú›à0vÂ=H«+oqz¨ïá¾ö3W´SiAàØ#M÷ò‘s¼é¯¾£óÙ_ýEkI€H€òA¿^cáúŸ"…×LTŸÌ‡ 9¬3€º¾¦”¹$ÔÙòx¨£9"/æ29ì€mª2•_¢:¶1Ý[;äŸâÏ.ý’ÀšpÞ²‹Ö @îô„§?€ô¡§¢Æ•¹«Õ‰šÔ5p$žïÕužãUupvè¤ÙÙ䑿÷ØøÔ†§sbP$²‘XÔËI}#T‚ˆÄÙ±êÚ{GÙâ<@züëú¦á˜Ðz»ëK˜$Œõ=*Ñd}÷6w@G(˜:ÆcËl6®ßĪnÀ XÞ@t{´p?k@Äó=jÍòŽ=Òtïiùô0Ç›¾ê8:Ÿ}Õ]4–H€H ŸU ó ©tŠ·õù´#gu뵫¤ükhê7BÑæŽnmÝ!gu³¢Í‚!ó!|(ôI›Ûë›"ú¿…ˆ¿ Ö=uA?U’ €¯$ª¦?¦Tês0úyÞ‹57¿‰ôxÕñ¼‰a¿³kÈⳜAdÈd¤ùýï&ûrñWGb!º¾uå+ 9‚‹Å¹‰êú¹›Û+ÕšÍï‡yƒ29I»­«ôÉKðçýaLán»0ëccä¨<Eß±[Ü%ùžcÏúfÛq‰°µX¢¡ë¤OÆä„eÄs)ò¬iÈÃá8lÍe¥¬«0 p쑦_yHÈŸ‡9ÞôO¿Ñù쟾¢¥$@$@ Ð]Õp?ÖÍ;¦¼çsreÂîX'òשRõ."¡[*:ÚN‡cˆÑ×­ðPô‘UWhÕèõOCú½ë ­al dC Qù_|ç®#¥”W@O_6º\,û*8ÇÆÂu7ºX‡£ªãáúæT*u”¾è¨bëÊôúÆ_ƒ_Áº¸¬sNRG×#õ$h\áœVKšV#Õv²éÔÖoJZI3?áãî¾[_W·RªN¯ÅínMÅ¥]/+ÄB«ÿ€W>'m.Dƃ8öôÆù×nøObôºÏá·Ô/`Q¾³ÅàÿQ|ôöŸï™Z÷Š7ÑŠB À±Gú^ä="=#¿Ip¼éãƒcô­$ ð½nža¤&ä'=dV.L)EÔQÒu. ~ð[­?+_Ô¶w.*.ö:°Îeg±3È ý¯!Úá(¤¸Ô‘ãÜH€H€¶&0ù¼¾XUÝep–†4Å÷m}8Ÿí,~½î8pþ™G;2ªºwZäe8ö?'T„ëÚŒ”Ø/R]çÛþx}·ýâΖˆ×ÔÿC–ŠC1~±œú: úÏ߀Àù²mªmØ6òu‹•gcƒ²:º^‹e–Û)GÙ4*ÏX‡óÿûÿ ɼàëËÙö,&LEýUÈ8ðZÎjeEé L™‘ÐKh™ãÓžW.Ï m_&ÃÞ„äþû#Jÿ×bÊ”|NŽÐöp+@{XèTÞ#,@ò—Ç›Þï/:Ÿ½ßG´H€HÀƒº§F>ˆÇS'âÖ-4/&í¦”ú©‘2_ E[ž u´^TÑÞ¾K.*.Æ:T2Ð^Œí΢͋KKSGêh‡,t°( ýÀiŠOÇãø£Ðà6¼òµDÁÆô"µ¯v8 |ÛpìÇ«ë®,O{b¼t9Úá–“q²Óü¦D”í¯j¸XàÁªW˜ÅN«_ž7TJ)jàéq+í퓆!O×ço¼²þÝ¡Ú.•´4 ¢¯/gÑÏÚNí€ÆdÖÏbbgÓPvs_æôøNàD ˆ‰,·B“[©ðáS”˜¦<õMŽ…en5KºM §²öuôS)¯ÀuI§æ¹\ç ÜW+R½1öÃ]£\¶ê‹‰Ç–z›÷K˜|#Äñ¦·»ªÄÛæÑ:  ð0H¤OEgWt´¼€'WÃÒR[ë¦iG"ÚæH%“E[ÅCž¿ 3Ы©ázvQOÔÖ¾‰u·—Ⳏäà6)®ïÔua?¾Gã1  - Ä«ëŸÂžºP´i¥Œ3¥4ÎD€Øþ[J¹òéŒnS©À-‰iÓ¼²f«# ]S[«£n./þUhýÊaˆ3ýEìE«Qö8OîŽíÔu¯ðøý.VUß!”ж~QšòëRšÕJÈlÖX~ù†R·v×4¼”–cÿšÏ8ÃÒlRôM„HNÇ®z2+êüR¨½åœàý4¼²ù=ñ,¢À¯[ó⋞™„€öämXóüÑÔôÝ`¨¤׳¯âL8ʨÊ"#ƒj•fé]ü½“É<Hy= çÅ…¡P f|¯)xes]ÞÔšwuÆCÊv\Ÿðúõy“Ñü[X8ö°ÖŸ¼GXãä)Ž7½ÙSt>{³_h €è5½‚ [–I³?×>2ÝiSñ½NíÎxÁk“SLG}뵊WàÞÿ?¤ó} }ðoišu?¿l™ïxI©Ð ½ ÆCpÄ—„6¬þ¼â8¥Á>=ÁaW¼&á…Ó­Ók³ê¨DÝ~´]·_.M™©Gì¦4Ž*®×ÓsãFµÃÿ»fܸ Ãu÷ˆN‰þ1®­m\o@« óLRµ'&j6ú{ÄKošÍ8àßC¼Ôë`ô¢Tæß¾»ýBNü³fÚ´µã,oEׄq[‘Ë‹ Ö?Çùw꾫ÿü[¿j2œýÇÀQΫ}ÀpOðÔíÔÎÇMç Ž˜×çàÛè‹W1Æ|-`ȧ»»“çk=uØ’—-¶óÊ…ãÞÙÁÒy  \3nû¼}—lÂy‹¬^¦àn8¢¥X‚BƒóadLØ÷¼=qL_—Bxmúþámÿy±çÌr|ß’JüÑ_R*õL¢:ò?-ॠË12®g”¥Ì§ú¦“‹ÍÎ9µ¦¯O_ór²!³]Õ¸ÒRK¾_Lò)ö±‡Õ³¦Èî^¼&Xíª´rE:ÞD¦%K÷è|ŒÕð[‡ øƒÀÄêYø~¹×¬•JÍXw›×ì¢=¹'Z°`wQ’º?Dñ@‘ÛVÞÖëñIC>T¢J¯‡»¶:ÎÃÿ`ÓØž„1è òµaÄŠ}÷+)ë7TÕ-+vl¿ªg^9:‚ÌS®µ+£sxÊ(Cvàáü¨rãÓ))?‹ïØ—ï‹”Á»C…žx§6úý&ç´vDi„Žü}/ŒÔ‘"ùY)SÏD|b·77,)ò&,û„ëÆÕ¹ç*Jgñ…+£˜72¾÷Éfx6Nil4|7¹Ã©¶SÏÈøý™fM€c¬º¯€÷÷çª^ÓsBÚÒlžœXÂJH€H€HÀçâ:UäâÅ'†Ö¯ú%fA_êóæ8mþH9.³“¢WᇕN“ø"Y.KžH åtþÔ×Ô4:Xø<"éOÂyô…DBކXš­îÏgmuKÜ,™!jjÖg­‰ H€H€†'‰¤°pª¾œê˜s†g˜îH±;ž52þ,!›áÙ8u¤Ð³08Å©õðûWŒ½^mæ¹m½Ÿy°ÎÊë’<ïsÒCt>ç3+! (Sý°¢£íQ%Í;Ðî EÓvë ÕB‡ê¢y¿×SbªŠhËðù)8[Ÿ2„|bÃ’—^.ЍÌœ-›|Èþ†Ÿ‡cþópÐBô<0à·‘$‘ññpíÕ|P=&# àÜgu$@$@$@$@$@$à-t>{«?h @ˆU×ÞZÔr˜HI¬#¥Ž+f¹Õ _R¯yx œ®3Rø'tøÁÝ"Ú²ij_Â3ì¥XgíÅP¹ziõɽþš?·Ewn2Ë’¦q0Úƒµ›C¤0EÒíúDg³~}U꫱©õKì¢, €»è|v—/µ“ 1xeý»¢©iJE(p9œ«?Š@ã°ÛôQ(0 Œå±"ÒO ¤ë~¾é×¥”¯™R½Ž#¯Ê”ùZÀ(Ç ëHkk×W&ö4S©½¹üIôû¾°qo´eo‘Ú¸v$š‚Mÿ£ð=Κ† ÔnIÄÍ‹E$¢×åF$@$@$@$@$@$@$@$@$à!t>{¨3h @Àúˆ1!~ZؼP˜òv´p¿le.›´+üµ»Ây¼—îwàR`ií˜îÅŽå¯÷ ÷!¿]J™k±üäZ¬/½Fr­aе2`ôÉTª/™ `ùJ¸‚ËÍdy±¡§¤o”J––ê}Êè+/)) )‘*1M5VÊÀXDbã¯ÚøW¨I(¹DwÃkG¼véfP˜x‡üÙz£k¹ƒSÿ¬„¢sá†6§R €³è|v–'µ‘ ÀâSžÑègB¢÷·øöBÜ™-2(øÄÀKû¥ûc‹µ x£/õ>x„• 1vÊí)Æ–kOãM`ó>‰÷¦–Ó嵞ç~g2tlt*oüWçæ.~H˜%_ÕÔ¼ïnMÔN$@$@$@$@$@$@$@$@$ :Ÿ³¡Ç²$@$@$`‡@8‹ ñ`gK;œ ·¢¨Ž˜åF$0<õˆ4¿k;߈Épûs#        ð2ÃËÆÑ6  (D‰ªú‡Ê“ÆÁ¼½­ÛÇ6‘€Zý^„¤«j¸Žg'ˆR ¸O€‘Ïî3f $@$@$° 5µµk°sF°£õtÞˆ÷ûn#Ä$PœV!Ãù…‰ê†;гùl5 ø—#ŸýÛw´œH€H $ªëŽ^wˆ”ò 4'YMbH ͆Yò©Du=ÏÙPdY        ÈF>ç <«%  ͦÌHÄ„¸ltgë_RJÝŒýGm>Æ7$P^“B] 7,*Žæ²•$@$@$@$@$@$@$@$@$P˜ù\˜ýÊV‘ øÀ†ªºeñg—‹µ Ïƒù+|ØšLv Ä•R—Å¡ƒèx¶‹Žò$@$@$@$@$@$@$@$@$à=t>{¯Oh @1hl4cUõ7ƒ©}…WSqóùPØmo©ÀþXÛù QYÙSØMeëH€H€H€H€H€H€H€H€H 80ívqô3[I$@$à3«Oެ…É•E›o2”¼ ÑЧù¬ 4—†$€õÍ_N¥Ä…=5u)À$@$@$@$@$@$@$@$@$@¾%ÀÈgßv ' (½á†ÿ$ªëO‡ó¹ í}¥ÚÌ6,÷¥çÆF?”Žç‚íc6ŒH€H€H€H€H€H€H€H È Ðù\ä'›O$@$àHŽ0>zûOkç,~×VÓJè'°VIñãx<µO,\?GL™ÂTò<1H€H€H€H€H€H€H€H€H @ 0ívv,›E$@$P€à´‹ 1G45ݪœ5¡ÿZ9¡[Ê&>!Õu%ªüWë«Â]…Ñ$¶‚H€H€H€H€H€H€H€H€H`$t>D‡ÇH€H€HÀ‹"‘x\ˆ?Œ°iNO"ð=%ÄÅ0s”M¥MEI ‘Îó¤Jý*^y»( °Ñ$@$@$@$@$@$@$@$@$P¤è|.ÒŽg³I€H€üO`õÉ‘µhÅOÇD£×¤DïÅpBŸÏcýß2¶À§zàt¾M;t:û´ i6 dG€Îçìø±4 äÀúpJãkkû]¢Ô<_ªþHh¦ãÎ{Ï !äM²Tý6~Zýò¢i5J$@$@$@$@$@$@$@$@$° :Ÿ·AÂ$@$@$àOkjk×Àò_aMèk°&ô,¬ })>ïäÏÖÐjÀù&oÓùªØiut:û Ãh" ¸M€Îg· S? äš@$²kB_ 'ôõÁ`à«Rª à$<,×f°¾‚%ðªêšxܼMà\+ØV²a$@$@$@$@$@$@$@$@$@¶ Ðùl €OD"½È‡|¬½-ØÑzÂF'´¨ÁgÃ'- ™ž" Á9tUlÉÒNÑØhzÊ4C$@$@$@$@$@$@$@$@$à t>{¢h €%JmÒ’dN…”}9­•‘@Õu¢Ø£Á¶¶=e‰ú"WÏÁçq¨b‘â"Ð-…ü«4äµÝSk_(®¦³µ$@$@$@$@$@$@$@$@$@v Ðùl—åI€òG@ÂùìIïs`mþ °f°G Q[û&J|)¹/CJî)ÅL|>ÁžJ<%–HCÜK•Ü#jjÖ|{Ù@         GÐùìF*!ÈS‰7 F>§LóÍ\´Ÿu€£"‘8Rrßw”G›÷5”œ‰¹gãóŽŽÖCe~"€‰4êNÃÌa”³Ÿº¶’ €wÐùì¾ %$@é”/‹¤J'•ëã}ÌŠWWçºVÖGè 7¼ u?‹_V±~õT%Õ™ø<¯ ƒÕP•7 ôÂáü€Æ]qUÚ!Âá˜7ͤU$@$@$@$@$@$@$@$@$@~ @ç³z‰6’ ôXÕ:ç݉53ßJ|ÂCHžyíÞk{ç¿h @VEoù×Äês¾ §ÌÍÏ®ˆ__Õ:çÝì”°4 xe½þÞ4 ¼ð®©¬¢Üø´‡He‚t݇ „÷ß¡øè>{)®iK•4_2¤x)Öm¾$"í€æF$@$@$@$@$@$@$@$@$@¾ @ç³/º‰F’ E «cÞ-kf¾ßê¸+û”ºõF]ÑM¥$PlàX ñ<š­_›·Qíí;¦Ƀ¥’ŸÂÜ[ ¹ÒìïèßOB¨t³ ÿÞôÁä·z÷5dÊ~](ãuS‰e%%É¥ÝS#:67         ð5:Ÿ}Ý}4žH «}î&VϵL]è: %.éŠÎ»ÚõzX 9îšš@¿ÚESS XZº»(Iîmc/SˆðÝߎé!·ËÀ ûòâ ÖŽemóûBªåp˜ Lõ¾!å¦H½!”z#‘o#’9™-¶ž->ñ ø—Ïþí;ZN$°‘€êê˜sÑÄð9oaÙßc—×µ"¿±2:÷B'È#8nB¼ ôkØmlgçøž¾¾±21.•Rc !Æ™RŒE´ñX|—õçþëR|Fšë÷fÚe¦P}pfw÷+ÇêòRеú=ÝIý{ÖÂ\¹5†)ך¹¶´W®]_W·rXƒx€H€H€H€H€H€H€H€H€H€Š„€Nš"AÇf’ x‰€ŽHÞ><ë)C¨yBгM‰gLܱªãÖ—ÓIE$@®X[UµèWN68Ĺ‘ €çL¬ž‰¹÷–¶»:æfI’B$@$†‚¸‘ @aXóT×ÎC¥:w¶ë§¾.•šÕuøîG­j§ã¹0ζ‚H€H€H€H€H€H€H€H€H€H€HÀMŒ|v“.u“ äžÀÍ7÷­âO»MŸ~s¼gl="¡ÏÂô¾aHizcdëÇ>`(uÇGë÷h4&E4})J ¸³6*¹’ @Þ ¼;>ÉâNýÚ%<»¢ÇH-MqÖlÝ‹¸n'”Ø뺮ź­«ál~ëE¿8¶/ôôk÷^Û“wãi ø#Ÿ}Øi4™HÀ÷£7ÇPâ¡×°…1ÍH€H€H€H€H€H€H€H@˜=»tâò¾ƒ•!¦Üú÷ÆÞÝðÚ¯ñxÃkëç˰o5&ý/WR½mHù:&þ¿lÈÀK+Ö캬?˸‘ .­…ÛR¶ŒH€H€H€H€H€H€H€H€H€H`HûœþòÕØ±†T'!kÜñAê0!r‰õÌ„²ÈP;GcçhÈsG(”Å_¡”)&n÷NLTÏú'ŽÿMêþ• æê÷Z;7   "@çsu&›B$@$@$@$@$@$@^$0¡úœûà|8Õ’mÊ<±+z룖d)D$@$K&Žyï4!̯¬‘±*ø˜·S6<Í6+¯€¯ù”9K£ý|bõÌåpK·¦LqתÎyOÚÔEq    óÙ£C³H€H€H€H€H€H€H l_3ãSRYt<£ÁRßÅ:Ÿ ¡ó٠ϘX}Î.p2SŠwfÁÈòdèΰá[†!¾Gô¥Rÿ§¤¸­«cÞú<ÙÃjI€H€H€ `8 ƒ*H€H€H€H€H€H€H€H`H†’ y`˜È¿Z3><{as7 @&UÏÜgBÍÌ[1ÕçMD9_Uùr½L)u~†Š'v-ùJ†eYŒH€Hv˜6{ï ϽýÖR¾}šÆZ’ê¥ÂçÔ³SI€H€H€üA€Îgô­$      _˜Ð36ƒwÉÔh8®¿›iY–# b'0¡zÖ¦™zA yt°mJÙ{¹wh ø”€êÂ,M?lRͬã²ÔÁâ$@$P\fÏ.ÅZÉ×â|>º/¥øñ„ð9sÅôé>I^HôÙ  °N€Îgë¬(I$@$@$@$@$@$@$`ÇCìp ¢#Š úù;# ð Àf«Ï3áƒdk%{óÎ{#¥œ1±gìŸÑ,F@Xß²9$@$@…C€ÎçÂéK¶„H€H€H€H€H€H€rëC|~Zò‡] æ<’AYËEV¶ßú4„¿¼}ÍŒŸ*€¬êk– oDµøÕÊàºËrŽÛ¬¾¸Äù}.®þfkI€HÀ)t>;E’zH€H€H€H€H€H€H È eÁ™B‰1Nc€§á»ÐIç³Ó`©HÀ×&öŒýµª2»FÈ÷ e^ðQt^Kvzì•^Õ~ëË(qƤð97*)õõ} V˜J±*:ï ²!  È#Oõ²Z       B"€g2l<Æ¢IÜ‚H·.D³Í·XF‹dJ|Ó†›¦Éèç‘ñ @A˜X7{g\k϶ß(ùžêK\d¿K @æè|ΜK’ @1@Ælu¡UJ¨{´£yHù€ÔÑÖ7)êûך¶^‚’$@$à[ª/¥—7(·ßuÞª{ïZg¿K @æè|ΜK’ @ј>ç4¤Ë>À*€‘Öv^¹`î3Ðó‚U]+Fé7lÈS”H€üI`úô2)Õ¹ßÞÕ1waåX„H€H€H€²"@çsVøX˜H€H€H€H€H€H€Š”€!-G=ƒÐ’®ŽyKF&%oùø–G¥çísúw2ˆÜR?‘ €— ìÆDŸlÚ¨R)q™Í2'   GÐùìF*!      â!0~ễ”§Xm±”òúô²æ=Ù^n³Ä¤Õ¥±/oþÄ7$@$P€LCža»YRܹzáÜe¶Ë± 8@ ÄTA$@$@$@$@$@$@$PD†¸ÀFsW—šÆ_ÓÉ#2zýÄêYw ¡,§Ó–B~zÿœN·§ŸØX2q»·C[Ž1•:TH±?Þï›wÄkSdwïµcþ#¼^Eä«JˆgU‰xdUëœw±Ïû[±´3MOLª<{'0ކü¬Pb_ôã>(²3^x(Þ‡¿1¼ÖJ)Þª¿¿_SJ--))}âöõyÀ­L¬>g ΓJ»M5”¼ÒnÊg@`຦”<ßÕƒ a¯—¾~‡ðÚt ÇW]èµ·×!kÇ{¦Pïâï¿•Ï«€ñOß\ÇÑ€‚ÞØŸþè^ôÓöcÞlHqŒPJ/ÿ²¾K{â;5ïGãhÈZüÕ÷Ò·ñÂØI½†ñÕÓ¢¼üñó¯·3Ùq@ÿþô9JKN@Û?ƒ1æÞhÿà1ǘõàï:%äR©—…!^VB=1¾wÔc¯Ý{­>Æ2"€ï$7     ( ªg^9vœb9i8"DkWFç.ÈIe¬„\&°Óé3&%K ý /h­*yuWÇœ‹¬ÈNšvîaÊüÿíÝ `œEÝ?ð™g7IzfÓ(ÈQ@ål¡Ü· š¤ŠK’†CQxñø¿yEQ”#MÚr+ѶIZ‚€rÉ ‚(Pi9š«msìóÌÿ;iSÒ$»™ÙóÙÝïƒ1ÙçùÍõyöê3ÏÌx/™ÄöÆ8Ò9¦¹aþS½3â7.œŽy¿X*y6ê«;–FÇQo\Lõ®ôîjoXøzù$>i®´s¹ñÅóö÷<\Ÿe³Nzälå‹èèZÜÝ­îZwÝ#ÇÅdÒ¬¹{„½À»ñç””ôšÆ0ÍÞ­¨¤¢ ½–÷Y¶ðq¸o™†á†cO«øL^@•(é” CçX$3ü,ŒZÀ¿ðš^'wÄ:_ÏÏA&ŸÏ­|O¨¤Bßü`²½‚÷ŽCLSSTvÉNª³£7c‹›ôЙºýf­Xªââ{Ósè`]ŒÄwãæÇcÉįiz¾s8ÞüûS>îG=uÇý£è¾£eØÆ¥¢¾¾k¨¼ð\[Š˜Ò¡âôqé8S[–ÎÙ$–1™)ÌÌj³Ö (@ P€ (@ ¤CÀÍs.D¹Û½[Më©/BáÂÕ3ˆ?Â4çyzôsFt>ë‹§^W×7¥Zõ-\@Õ£]±MÁUåï9ÊùFI>'¥s}KCퟑ±éÅæDÔa‡Jxa‰üð#~Ž×ÜC.f‹ho¬}(sdúÕ´ºÚ ½ˆïB|ß9¦âý$Ûdrº’òôPçèQR±P7¶.®ù(™3ìàšÏÙŽÙB P€ (@ P€ @bÊÊòq=ë晩G0¢ä óx\ Tʸ³º'_)Î,œyþ®6e¤#¶°´ü|\<]‰ö]‡òÕñܯ)r:¦f®GþÓf–Üï`JæJ;£`Jm½ê tݸdŽ"“x=ž5_*,­Xˆ›v‰R/Ê@œß/XV{Ó°aëõÈ3n WV5ËATwIw5:`êŽçd5 ]FA/Áûø ãg–'½¼¨•ɲƒ<Ÿ™sBñy¶^÷ ×ûnÝ*GÍ{—¥Ht#ðµLœâµe>‹º¾œè’ß„âò3C/¬z åÜ‹Ÿ©I*¯ù~G„Ýwô{¢¾#Iå0Û,`çsL6… (@ P€ (L®Q_Aþ“LËp”¸Ù4¶7.Oõ³ë{üJ'¨Gcûr]V1PÀÛ·£‚zÐTlG CòÙ¢âòKSQ˜.#WÚÍsBIåA8×o;×{E‹Mð1‰^蹸uãß…Åsœ7³K“À„YU{£h½þ»ñ†NšW××o1NÀÀÁªªòBÅW;Ý·1øøG7x`R÷Nuù$:z~-PŸ¤–”í™ó|fÌÖ3§`$òÏðyö**]†ŸDN2„nàâ|Ž7L,®Üsˆà´ÖÓèã}j¹'埠ô¹U3©ᆜWxsLŠÄ3¸v>gðÉcÕ)@ P€ (@ P€©@§Úeå}Ô¼qw=m¨ÕöaSÍf\ü[d•Hˆ Fe[¦Iz8FÐLÉë/  tŒ¤ÉÇH½ß–Vþå'õâm®´3ÊFêŽ*O¨çsL”¸d“w ³êú~² cþÉð‹ ûNõC¼w«ËPŸ‡'͸@<äf)Àói –ÆðñÅ•Gz¯b$òÕ¨F:o¸(q¥z #¡ç¥‘#jÑ…%•ç…½ÀkxŸ:-j`òNÁÍ1ÃwL‹Ù’WæìOv>ûó¼°V (@ P€ (@_ „Š/8²˜ÎOֈǪñ4BI¯Æ.œPØ1ëúgÓ²Xô1ôì‘ÎZa>ÉK K*~¬:äJ;#ùõLãZ\Ñ ;ª“΋å}ª¨ÎÆïOpî>$™ø§‡ÚV; å Û4Œß. 1sÀÕRzOaÏç·ïõÇÇ»ùÎ#˜ê6äêdD-x>3â4m­$>¯®r¤z<Ýß™úaZiY£oæBÝFõÙŸÞ?ËÊ™ý+)Ô¨H²¦"7mc¾cþßjš€q¹%ÀÎçÜ:ßl-(@ P€ (@ P 6é\n‘ÐU^×|‹øBÛ¾Žßaç0ïpʦ˜¢*b쬹c1bnF¬ùb-j\Ìý6FÉ\–â¿Íµk¢Æðà zýRt ü 3`º_4(Í;1ôLu»Ä³|¤™f@ñ<ŸHü»©¸A®ŸW×£’>|í©³…pò©ÝR?Ð@IDATZ6%ò¹ÒÓÇw޹ÿ9(õGð}÷{˜ýåÇ©/™%ú]€Ï~?C¬(@ P€ (@ P ÍO/ß U(1®†MmËîøÀ8~°@)nlwä}rºž²1òñÔ zŽ^ëz¿Ô•8tI%sã¸Ó+:Ò<"WÚ9˜ÈÄÙMa÷q;h°ã>Ù··V9ýât¬W낌®Æg­j¯ä3Vñ Þ.Ðå¸sÐrÆöþýãØPÇhÝAÎ-ŠÏg?Âr)¡ÎÑ ¸A®ÜOÕX5 Ëj<5aVÕÞ¥l }ä.ÂMè ÷á&ÅCÅ•_õaÍX¥4 °ó9ø,š (@ P€ ( á £×zÆõA³ÍSRw¾ÆµíñgØf“‰#½oÙÄ'#¶¨¸|¨üx.?àˆo'ªÍ¹ÒÎÁ¼&ž2g¤ëvßcñ\ˆÖëC_/¥,Ãó|šv ;î8ýäMt]q ŽÏD‡Ø÷…MxNm¬.ûöË tß'ª«y ÐË7! ˆº|Æ®>ž~Nq‹A µ¡î6Œzþy I{“èçw¡cè ¼žg(øœ'½Ý{_Óù*P$gª~½ã“ô—ˆ}£7¡õo).+œUq¸uºJÀó™'ïqE£ïEMOÏ€Úê*îåyîƒé]TZqÞ;Îõµ•T5ø¾²¯¯ëÈÊ¥TÀ‡S¤´ý,Œ (@ P€ (@(zM[Ùé^%¤ÿ¡•íµ÷ßiûxå7ub­½¸˜•yZy¦B¾¢uqÍGæi©¤óÔ9–Lסƒq9:@þª¤ø§ð¼ÂŽ·Ù•^`˜›7NH¹¯PjÖ‘žü§Ù€‘ßµ Ûp…mºHñ¹ÒÎAÚ/Ý‚üû°ßz=^œƒ6œ¿ß…Ãjáºûëþ;HÞ}w5ãÁkøY®wN.+¾¥côYx~è,ËV_,|qÕÕ¸“ã:—Íöñ!{¼?öå÷¬FNÝÀݸH~šI9èŒ;¾[†_5‰íS°Ùíî¿/[n>I8øÏb“Òy×"œ¡ýZ꾋Ϝ¼¿ê›­L¶V¼gß%”{gkÓ— â?ÞÜõ†xñ2~þ„Ÿ«&”Tã)Ü\bøZAšÞM OýŽéÝaú;—^Ϲp>MÏ»ã0âù¼`âm ¯ºG”#Ç´ôo¼ð{y2íæ@WX·¹À Žó„·«'åÞR‰£ðZ;»÷ÓÇbÜöÆèXúØÖÆcÌÃ:Ö¢Ÿƒö]m°%Ú•#u”xÜòõA½1ß5÷Áw•£ñ–v2²Ø³6QïãÏE9ÎC9&€ï½Ü(@ P€ (@ d‡Ö »ÿÈIØÈ¾D©à‚Á춦º¥‰ÊùP •¸.@é‘Z†›ºån0 ކ58§àâä[QƒúTâšÖ¦ºõߊǡY•'¢SàQ›²ðžÕ¦”úI ³{þšwn2I —©J¨kÐñ1Ó ~.Va$˜^'0![®´s0,|Î|çÌv„ä&t²^W¿þ†Õõõ±Ž`î­ŽÄù? ¯‹±c—Þ¿]¡¼ÃÑIö¢Al\!¡’ ýyWj’‰ Ú²t¾îãÖG@l•žx¦Ï.ƒ?Õt¼÷>oÈÈŸy˜¹C]9D¬Æñk[ 6.õõ]Q⌡#«ïåXóV„Œl ÂûÐ -u·IKl†¿žy>qÒqñ‘a´½ÒÚXwˆQdA¡ÒŠ Ñi|kŒY _T4àÞœ[›ó×=Œ× k“Ï„™å»Ž¼¯ŸJ¤i“¶77þ¹­¡î¬ÞÇÉü=±¸rOW*}ƒVLuÕuÃ÷Åñä÷­55V7mõ|¹bÌ¿Ž¬òt~‰ÚøùŸ(Iÿæãø·j¬(@ P€ (@ P€i8±:(¤ºÔ¢]r‘E|ÔP\X_‰‹òv£¨%: °Ž`ÔŒ“tPzêL›¬qõU ØÚ´àFÓŽg?\^B'S1.)ž‡‡¢”ù†§œ#Ùñ¬ËÊ•vöwE'Ña¸`}mÿýC<~2 ä- µ×& ãY¥pþë;Ýé ÊC”Ý÷pz‡h7¿ ”c5Ú\·'ÈßïíÊ€ú©ÖÆÚKPÏùƒÔ\êWŽ®Ïâý÷ÖDt<ë2WcÀUGàÏ·)3Ú®òhy¬G€çÓgO„¢YóAoèob«–|ßiÄwšÙÍ ó´íxÖe6/[ðJ[cÝe˜ %åMØåÙÖ£¨ÏÄhß´Mgå2Ðñ|;ÒÅÔñŒ™RžÆ{Öô¶Æ_†Y“mdz®oÛÒºgqCç‡xž-ô b܆àùJ(UÇ«ªò0Ç(ÓÛ[m›ñ|Ÿ‹C¾”¨ï46Õ´¶5Ô~ ß“"}‡ŠZA¼V©gç‰çÁ¢WÍEÇÅM7êwYËÔÝôôà ™ cmSÍû?ÇSêT˜éeA¸Q`Hv>IÄ P€ (@ P€ @n xʻܦåžôn±‰7‰mÞ9؈¸Mb{c¤ÝhíÞdqýÞ¥¸j2ØÇ4\H­k[vǦñ‘âÚšjþãå 8Þ;j.Œõú.ÇE³qÑqc¤t±îÏ•vö÷ ½¸º\mÖÚ¾EŠe¤Qÿ²#>~¬:ÜZ°~Ž?1fàÿ‡]¸.ÍÍמ7ܲ~áXFZ–‘;á˜Ê·u؆óu4Þ«È‚‚ƒÛ—-À(ÂämºÓù^`QÂèñ£VfŸ»¡<Ÿ¾8÷…»—ã&¨ì*#?p]qF+ßn—Î,ZwΆ÷PD?j–b{”î@ÇýÉÙ&—• Çì8×XçŽuÑáwfHù¨®¶Õ=TyíM V(/< £ª_*–Ç)ÀÎg>(@ P€ (@ P€ PXŽ[=_#åg²¿½aá*)åÏLb{b¤§×´åF %„‘ƒC‰›ˆ~>TLŸãIö·O9Ùñ'ÏgÚΣ »W£p=CŒé¶StcïJÓñÄaîÍ^AðäñºM>J© ÞøÆC£¼{¦Ó——Å~VJžµ¦aÑ«ŸîJÞ_z‰/(JQ‚ÕÌDÉ«sö£;ŸýxVX' P€ (@ P€ @ÆŸQ9×ÒÊ,ª°>°¥ë^‹x«Ðu÷×ý ŒGƒmÍ\ž¥GÚXG0¦KͳIîY¬¥m‘oÒ;góÙeÛ(@ P€ (@ P€1º==epÐ4)F~ܾfÅÉöUŠÛLë³-.Ot»Y¦‰9\J'l“šÅ6ñ~‰Í•vözv: ïÕû8Úo }z¨½±ö¡h1É:ÖþÀÝpçÁ}†ù :Å,ÃX†¥A@IÙaY¬ÍˆBˬžbÜÃb¶þ¬'ÔÈ×ÅÙ äüù”a·lÃ,èokª»Å">a¡=S|+a>“JÆÌ#—%¬ÈÈ•bžM~zÉ•ÖIε6i‹5³…@]¢òc>Ù%ÀÎçì:Ÿl (@ P€ (@ P .‰§Ì‰Ž»*޲í¶®ckþ=òy•UB)ªDYY¾Ušƒ]á¶Y&QTRñeË4iÏ•vn‡ö„ñ¨tÌÿj{ºtü¡D“i±UvŠi,ãR/à)Õn[êø_m›†ñþÀëóß&5“±éÐ3É’1IÈñó)qcÔ×mX=O]x$KÏ&‡ü%¯1/]0±¸rOóøÈ‘ãʪÆH!OŽ1ð ¾/jjºIÑž ó¿(Éö†©UŽÅ¤S€ÏéÔgÙ (@ P€ (@Ÿ „‡èõjÇ™WKþ kÎZ­‘gžwŸÈúz£+æ÷Ùcòç¤P×è³Lã‘ÿÉ»ÈÃêb)‚ÿ*)×#k3fË•vöœ¬»ˆMgžœ5ÍSwM˨çÞúuïý{ÈßRœ8d Ò&šm WΰÝmÓ0ÞŸè|²½™ÉŸ a­zrù|N(©8k“ã;å²O§ó©£—ÎÀL&¿³©ƒ+=ýÝ9îÍépK‰Í2 ¯´6ÔßxwÉ uqÍGؽpCÜ•ãì|Îñ'›O P€ (@ P€è#€Ùû”ÕôRy7÷IŸä?==µ:¡-6%¾esèêúú-èzþe©(W„J*ÿ7U#´-ë7 ~ÔêÃðËt4éC¢ºÚ–ÂêëÚQœéì'̪Ú;…ÕcQyÞûá=¡ÇÙÃ6 ãý)€ÏU›5gýÙÖj»@.ŸOW‰Û! þHíwÊÈry58j±œŠ´jg¤’)¾騠û¥HËôäýëâ)ç÷ý÷ñ1ØùÌç(@ P€ (@ P€=¡’ŠÓðǾkZv.±ˆ+kË}ˆD–™Q8«âpË4±…Kù` ±¶¶º&Ô9úuLÃ]®§=!Ô&É‘vJ©Ž7‡U/™Ç&1R‰·Msw]÷@ÓXÆ¥V`ÛëmJ•ÒãÍ6`Œ¥’.€;¿hQHGÁ°iÅÛ[×5KnmÆÚëë}lð{ºž2Û .jfÄ9*jÀŽÝ®|Q¿ã®ô1¾ø3WÚ‰‹æG™¾”p¬§INÊÉ”¢Å4_t®ŒØ¥¦ñŒK­ž¯c­XãŽ%Å¡©­!Kë˜\V6¼k˨}UÀÙëuïƒ÷ïÝpl¼ìŒ¬±ø[wJéßør3ÿ2+Ä ÀóÚ Iv)®Ñ%\=ƒˆÙ¦ÄC=³«˜E'= ‹U7áµh:Ù vyÇ R÷ÇZ±1§_Œ%oºÌoUâñm7*ÅZdbÓ)Õˆ÷;ÞÔ–XÕŒÎÏ}úXy P€ (@ P€ @b&–Î=ÐUÊf„Ч¤§§%LéÖ~èî‡^\õ ÝË¢às&”T^ÙÜX»Æ"u蚆E¯b çeèÄŸiøÓzäóy¸àyž:¢WâñKxü¦¯{ Ò/§»C:WÚ‰Ž#ã‹À˜®þNœ«;?=þÿK Çæ5äÿeY •RE&;Ÿñ|="Ë|Ûœ¢ÓæNyÁÊSG+)èèTû ÿánt<ºíðàÓÝüË_<ŸÉ9ݵ¯ðDÀ4wåȧLcS‡©¤ŸÂŒÆEáåÿ9ÇÜùŸÅ{¾yyR§ù°x P€ (@ P€ €\ü¶Í¨gÔyY{ÃÂU)¯;ÖÕU%·áòÜÏ-ÊÎó„wâl‘&¦PÇ ^æyîÉH<"¦ &š‚]SÐÞ²žÑp[;¤ß@‡Ç tx>ØÒõÈšwn˜,¹{r ú ðÞÉULoîx>í’Þ°ôhu÷,F3WE‹éwlßQ³Ï/ܸ䎶~ûù0ºƒRÌ !¿Š×ÎTÝѬû‰ð>œ€Ü™Eªx>S îºû Lá`¾yÏ›Ç&?²mgùrèãžuŸÍúДBçqì›{Ú¼à½ç¹ØKK|ÊŽ|ï¹üÎÄçË3W7Ír£(@ P€ (@ P —&͸ ÏçÙàrâ-6ñ‰Œ ò!¿n»<åE¢¬,ß.}tóÒš·¥Rß°Oi•b?”q).R6ºÃ ZBÅ(,®˜5eÆ¥FSºZ•!8ÛÛ‰ì;£é)óŒÀœäÝÞä$ÀìãðDðIÛänð˶i] ¨¤bjQIåb ¬FWóõˆž=úY€ç3¥gGß«Uˆ{TˆÙ´4-XTXZ9 Ä×E JØ5ï{çà­ïœuy›[QnMçüúæ45¹[6·S 9"ëÇ3J9<¹Ïæ@[SÍB%•èøÀ¿¥ 7̈0¡w†3,ŠÀ„YU{{^øF¼`…¬7ˆ"‘‡x>Óp9Úæ¥Óœ·6õ³é Å¢n:1ìVk6Ǿá{\È&5¾´¿oŸŠX¼Sê:¦¢,–áv>ûÿ±† 2J pæù» ™w:™¿Ò%܃ñEs[~áü'[FNV– @æ lûÔɼгÆH£€ Ü).Ic RW´ßBaIï|Ö jk¨ý:ŽÚñ ø&PÊ[!¥¼À´Š˜ªûË“Ëʆ¯®¯ßbš†q$f’ø–O¨ÆE ÞÓ~m>G?I{m¯€y‡x¼3=ÚuÖo¼ºéÞ«Ö§»,ß?ì|öϹ`M(@ d¦@uµƒõЮ Å븻MÏ»È ™y&Yk P€ rTÓ´^–KM—ž¸4•ím^¶à•Öi“§cÈ\”»2•eãVÐÃÒ}~béÜ“]nÖµSÊÉ6cþJ`ø°õ+cõ\T2Gf² Ïòø¤»Ü'ð¾Yl™”á>àùôÅI±yïÚÉ5X ó™k”è˜Ü|'³`ȰœiÙü”g}$;Ÿ³þ³ ’' ×t.zqÕ_0’ãz”2"y%1g P€ (@dà&“ïÁÉÈÛÇyž3¡¤2µ#½««½¶ÆºÛ[§í¶\f¢sèÏøªiq‹\øëÄâÊ=“~N²¨ÊsSu~’~ZX@æ èé³·¾_Ø´AN/,ž7Ý&E®ÇâÚÆˆpž³SÞ‹M˜yâø¹ïó?ÆÏ…ŽRgáñ žr:îž2ìîvÜq‘~Po$ 9›ϧON½6£sB/ã¿m´y•¤M{f«„ͨá13ðÁ)ãšzÜ-`(À;ˆÉ¬(@ ä’€žfkÉé»°÷Í¥v³­ (@ P «”¼<JÉó„wÎ#:R¼¡s¶Uˆå(u¹^—uKר/;Ê™¥„:û’Ù!^äJµDTUM55ÝIou´Ó :›œ0×-Lús… ) =o‘ÎÜ!û8Ž÷<Ôï+Ü º÷VŒ9ÔK‡Å²uá=üQG‰åR:kž6ù5÷ÀX2êMST\¾Yåà‡soûãýÍó¯`‚Ò{jƒÀö¦Û„îq»5 ñ¶i|*âð-`WãHµ6ž:a”(^÷æÛ˜Ó/·~ù-q•i^ši¤*2d\ö °ó9ûÏ1[H P áE§Í¤‚âd¼WÂ3g† (@ P€)(*©˜‚‹\3SR˜ï ‘¢#ö§)éˆÐv=¢‡–lûãN¯8 ”'{B/•<KÚì!i¬».ú(|U‹?5ƒXÒej;Ûë? …G‡Ñf³kgR\–îc1JW™—os;]ÕÌùr[›þ-TRe®ÄçM10rö=³Ekã‚GMÓäj\aIåyB©91´ÒÝÐ ß¹qÉmÛÓ7nÿ‹¤A€ç3 葊tœ·ñ‰ttÀ~W…÷ÄNÿt>WUåÉÝÉ*i‡'ÞtÈpÿøž°Ù¹;þðYç³ÐuâF³/ÐÄ¢(@ l˜2ãÒ‚uÁÍËðÏ|VP€ (@Ìø6ªo< #³›: ö;‡>vÏÄ(ä? 8’¦k—×½†¢õÏou gž¿k@ô„\÷gj[vÇ؇µ¡{~z:£¥ã£¸¤ƒ²€¬Û=üöuy›ŒÆM²hàÔ¢1«¯Æl×Z¤É©ÐŽÎ1ñ¬lLßw„,nnªýg’¡F%9ÿ¬ÌžçÓ_§ÕéèzÓ–ï¢V“šIOm—ªGzG›ÛF­¤øw¿¦ÀÍûZU`Ú2\äû .0^µfɭͦi|WSÓÝ*Ä•¡âŠM¸™à‡õÊÛ0*ooÄÿË"MúBÓØÎÎ`çÓxNyh¼ÉàÞ‹æ÷¥+Kvœ2°Öé©2Ö–¥•ßÁhfÛåö÷†Ü)ª«ÏÂ~>gÊ&C%D‡Æë­ëÿë+[ÌG;tó:$ÖsÅØFƒ ŸOé›a B¢Ä´Äd”â\ÒøzæùLñ¹6-NÊú]Óp%K›öÏQÏí®B=ÌûΔXnÚÆhq˜Öÿ |G?5ZLßcÊq.Äã+ûîKÇß…cfâýtçt”Í2ý+`òåÙ¿µgÍ(@ P eøòsqÊ cA (@ P€ÉøËÌ1 w¯eš”‡o\rG ­·,8ßuÃúÂ]Æn­MuÕ¨ü 6 À:†{ØÄû!6íìyN)ñwÓöcJs½Ž87 $M ¥¡±êÛÐy:»èÅ÷k›.ñ¡’Êÿ‡òËÐÞ…:G?*)ß%‘õ‘RL5ÏO6˜ÇƉÿ§ ‡ÝãË%÷Ró|úóœ·4Ö=Žë‰ï™×N —ü3O|dQÙ%;¡Îß´ÉY*÷.›øH±^@þ9Ò±Á÷ËŠ±³æŽüXêöJ©®H]i,)SØùœ)gŠõ¤(F=}Š?-U`Ñ (@ P€ (,½à\P;Ò2«{±ÖðFË4i ÇEŽ[í V‰ªª<ût¾I¡0òy¾Mm¤”™8Ú4-íÄ¢‹-l)*.ŸaŸ­¡[Œæ)NmŒµ5PzÞE ln¼-¼€¾*­øöµ¬VBà K*¾ŽNçkúdz<ªýòøâò®)ª&ôÉ?êŸxøOÔ€ô¤<+Ù%"+óçZZ_Ï<Ÿ‰8ÙIÈ3Ö‹;lòuùsħí}Jut^…ò'šÖ7><ݲl‘^¯9î­mIí¿ñ^ýªEFcƒ^ÐzÙ‹ü‡ Å犞nû¸!sì|ιSÎS€°(è_@*óéfì‹` P€ (@HϹܺGÞf&M šëžÄEÀ×,‹ß%ô±{¦e_…{®ú§M…<¥2bÍçþmJG;¥tôôŸÝýëé1F?ÿf—⪑Ž'{¨¸ò«˜ŠýÑ¢ÓæNJvY‘òGAg¤cöK1yÀ>îˆ* ;9ð>÷ݨA‘*q%:vkEYY~¤tï/,©<Ï¡ƒÔ£È‘ò/…Å?Aý±¾hœ›cŒspœ ƱñVW;X#w^}ê–Ÿ+uÈÌøs±Çá}7Ù¤~wzÅèö¶zUJ$tV ¼æn·k¹º]½áSÔýU8«âp¬¹¸õ=Q/aú^Œõù¦Ä^>¯¡/«‡é·oÂÿûc©:6ÊC£Lç ‘êt—c”ñ8éš5&n?HÄ4ÜÈÇø&åZtTGjœÁþÂWŸ‹°Ì}M¤ñõÌóiðKSÈÚ¦š÷ñ~5Ø %k„÷_â3ÌbjüˆY?ãk£žÏ{››sÞl¶[Bר;aÝYÿ‰qÅõ`!éÜ›†é·e ÓÕ3ïìnQW†æ@¤ò"`S)@ P`(å‰ÏÃã (@ P€þp”£×¯³-–1£ž{õU×Ýq±¹÷±ÉoŒZ9*4«òP“XӌΛã)ïQÄïïªÀ#ºƒÖ4­m¦´´º@ëHÑn[F¤øœh'F3Gjÿ û¥¸°°´òƒKÒN}ƒƒôÄrdßÛ¡6 Sìëurõô¡èoLå¦Ì/š+qx*k–Ee©Î`÷ù8³ÿ©MÛnPÀÍ1§Ç”>Á‰&—• Ljì:tP™Þ¸+ä+qMs/Å&ãfH•ôk"cN¿x:Üô´è>Û2äõÌóé³çÍŽÕÁèçŸbO׎{£>ϰeÛÖ@˜ˆƒzÆ™W ;‘÷·ÉsŠ_+ª«=›4CÅ®[ºh–°ýþ½oÐ ,ž2ãÒ‚¡òOÔq|~ü y•%*?æ“}ì|ξsÊQ€H¸€’b„gÊ )@ P€ R&0ñ”9#QX•eè@òîµL“öðöîÞ€Œ{l+‚Ž»ÄŒ~Æ´©E¥?Ã(¬;P‡ÞÑ3!LuýpQi¥åÈsƒVœXD{/1ˆÜ‚Q±ñ¯M˜+í„ZÏè)–n4øSçþc¡q‡W}78<ˆŒBý2ÃÍ&òzŒ&]Ü35f¿ƒÉzè)‰ 熛ÇM(©4^[Ó0לÛ¸äŽ6L ? µºÙ¦Žžš}¦i¿;tFÕÎ}ö§ôOÜ qXGçèçõˆlË‚C˜æþ~ý~+ôû åæ)ó›pP·Ó,³· —y®EH¤Ï‰¯¶Ly=ó|úêi3 2zô3:“¯p úŽ]°þñcã‹çYuGÏràQýùØ%ÝeRÈS¼G õT[Ã}ÃcÂ7G8úF”–Ÿ´.oóxOe™Î6\¢Œ_"Ñwl2>·Øùœ[ç›­¥(“þ¡5.¦„LD P€ (à ðð‚¹¨ÈXËÊÜÛÚ¸Àö—eI ØO½‹ˆ_™8û¢ ñÔ¨¨ì’B/®Z‚‘ÔWÌG WJÕ‡J*¯¥£d`~[÷„F­ÒsŠt|ý-­‹k>d¿ñ®\iç Ž£Ïix‡}Ñ8è«EÇÞ²>m„²pCÃqRºOápäÎC%fajÌçÑÉkó<‰PâлñïGóÎgÌÆ€!cW +#hY:ÿeì??±¼“â\vßÒ¸ý×ÿ†ÁŠMȾÑeã1CÖJ•ÿ@†Ÿ5Sý~[¸Óª¬Ó+ñ¦Ešc K/8Â"Þ&9•7#A‰M¢TÅfÌë™ç3UO‰˜ËµÁ½‰ß±Ë@íêHïÙÂÒòóíÒ™EëYoðùø"¢O2K±=Ê çbö´ŠÏ¨ÎÎ'±cˆuUhôª§ gW~n{âþÐÓÔbÊÃL»{¹er=B6æ-WÚÙ¨uÉ|ÝQuCÿýC>–⇡ÎQ+&̪Ú{ÈXË=µ7nhxÉLnžâ õ Fcϵ,Æ:£þÛí©ËÇÏ,?Ê. £{Zë–ã=>ÞQö#u'.Fÿ½‡&n(š9wŸÞüý[ßäƒÎ‹kò;Å;¨÷¥È#ôãÙäÛ–ÕýÕ:‡ ´úŒž³0Ñ3l}/¿ýWY×?E 2æõÌó™¢gDìż÷Ø¢,Ú>9¸–¹ŒJÞŽ›4_zAÌ7ªô-SßhÓsó‹§žÁþ½ú3û[þÄÿ4‹-ªu؆ßâõ÷Z ©?ÙPžÃ²(?Ñ7 Æ~`=Û ¾?à}ûut<ÏÀ=(ÀÎç&ÜC P€ý°îÐæ~»ø (@ )Õz‹p†R ¡¡>8 îk™éóÛ:p-“ù'·ÆP› c•¬×sÕ³(ï Ã2“®zkÞ4þŒÊɆizÂô…D\¼tKçh\ólÒöÄ*çÖi¶%È•vFòÛ=â‡8ÃÅfy²ç¹¯%jd):îvÁ xŽÿõ±¹Qx˜’ÎÍ㊫vÔÆDì÷<ëuˆƒŽ#ï_\~Šiùºßݶ¤€i²¬kiZ°ÓÓ^’€ŽÔ7´('ð&žcÿ@gωèìÑç £ôÏ),)¯wÝîÕè¼ø_ÔuLüõ•·¶6Öþ$–|Ú–ÔþéV§•âs!ùX¢:æCÅœ€éÆ1r]~Õ¸iÌ”×3Ïgž1ÙÒPû8ntùA I‘D}ÑQÎkXJb ¾œˬ"f–ŒõåoÔ7ÚÄzó :„W´N›|mlm°HU_ß ýþÐi‘ª7´õüêèü/>+¯Å÷†ýzØüÖ7 éÝ0«Ï0›ËB¤h“ž±¹-€™3¸Q€ ¢ àÿADL_T¢çÌ£ (@ÜÀ?¼¦µ4Ö½”­e+ý&€‹tcRÀ/ØÔ ä*Ñ™Qg“Æo±ºSÊ–ÿêeÕÁ¶Ÿƒ¶ßgڞ’Ê3p³æ½ˆ·éì›=¦l”WRüÅQJO¡»2Ðí­ï(PÝù^°ó:Ž—žš‚‹µŸÃ{ÉI8~<~FôÍÀøo%þÝzènˆêjÏ8ͶÀ\içP.z*KŒ(ÒÓÇv„Ø„´÷`Õí‡N~Öæ\L,®ÜÓ•=#$¿<ô:îö›_km¨»Ç>¡yŠ¢ÓæNºâ™§Ø!r9:Qÿà¹êeOä ó¶¨9,?,Â…¸a£ÓÔçñ‚ù‚^jxØqÇ­[ºÈfšï ˶zd:ô{w¢µ ÏgñóÜßRž|+p›Ñ¹±q“ô6”ãŽpóƒ^ k'Wäu=UäooŒràšžæFÛ+˜Iá£Èø‚daiE½TâÌø²-x-=¢©;´ß xá÷òdþÚÍ®ž%2 Üà8Ox»zRÂûЉ(/Þk›ïä«À6Õ´ÆYwãäø!ÚË •ýËø¾o>Ž÷íg…RoK'ðAgw ]Îê@¬2JJggüÞ³ªLî£ðsŒ>„ÓMÛM‚¥ãLݶt„I8c2P€ÏxÒXe P€©À´[ø’VœêrY(@ P [];­Yq§îpàF” è5^1Õî+–…nÄsvçlxÎꑟ¢öMËö?‹¯Ç™¦Ùf¬§mfš&]qJª3Ú,‰¥ü\i§‰M¨xÞL!½ÄÚ\,ëq!ø¯¸ñài¬ØûoG…Wuq1[ÐÉÇáÞXézûàáaˆ;¿õEà86õ¬ãþ‹820NŠÎtRZϸ`œo ;Ÿ{%>ýŽŠbtTèbb»AáÓ¬üü×]­æŠúz7žJbT÷nI¹yÄzóÐ?q­d…êUáþènéíŒWÝÃeAž;Þs»' )÷Egó1¸±â‹è¼ßu¨:£sèit¦GÜPeãó 7b$wË”×3ÏçàÏv> =å¦MUË>Ý›ñ}"”<ÏW£%x^'f“ù× §”dŒP{7£œk=w„̵;“^Ï<Ÿ‰:ëÉÏSWon-Ø8/g=SƒŸ·yÒ=jMâWÓXIÕZ°þëy}Gë¡h¹Åó¼²TNE¡"Üí3v>ûì„°: ü(Ð6lýø§]»ëÆ:Q€ ü. Ùè÷:²~Ù)àºaÝ1f9•¨z.ûÖ_SÖ£Ÿ±6àW'͸ È晡;÷0UêÏmÒ¤&£u\uÚúå·¬MDy¹ÒN+tÆß‚uºÏB¬_—UøägáœÝ`ÒžDÆèÎ3¬éËä‰l¥¿óÒ¯yœûtìèOüú5E|£o§¶6Õ&¼“ª¥þæO÷Ù¨ÈfÓÊ$+ÏU-M 0ÍŸ9‰]G;BÁ™ôzæùŒpýº»¾¾ ïS¸Q²ç>χÕlt ÇlUŸæÚa™–ÆÚ¹­o&KØ 8q¶ª[ï+íË<g>Lž…ì|ΓÊ&Q€H¸¾ JGü!áù2C P€ @ö ¬iY?yEö7“-ô›À”—àbõHWtºÞæ·¶Ä[Ÿ±Ý#ÿ„Ž…6Ë| Ü ýTÚ- ußÅåÀ«P–_. ®tç„D¢Ë•vš…1/ˆ€s(:ȧ°ÌŠê †õkáÃvòAª:vn :î(ظS3Õ•Œ\žÜ‚·Ó+°¾sR×YÅTº/yJÍF=:"×%©G<ÌU?ÏuŸ–¢Ö}ú÷àa]èуIüÞLz=ó|&þü'9Gý>õcÜ|qÊÁTø¾Ø:ñúúfZ(][_³Þ5ÚZ XÕVã ãT<\“æz雚fãÜñFë4Ÿ¿ÏÎg¿žÖ‹ €ß”ø5ªäÇ»ý&ÅúP€ ¶ ()oU‡·ïàH‘Àúü-çbdÄËâ6:ÝYwÃáÊnêTR,´´Àì®Î%âÄê m:¬Ë÷KtvŸ†t­¶i/ÅÒn7ÿðæ¥5o'4ßm™åJ;MìÚšjþ3r½;dÿ‡øN“4IŒÙŒ;¾ßºa·#[—Ì3‰å ™õÆ%w´)éÀ C3 ©/]ô:QNCgŠî`}+©…%.ó»¼ Ø7$b}硪ÕÞ´`.”q-CÅ&øøZtºÍÄTþzªú¾:Þ‡Ü ‡ŒHP@¦½žy>tâS˜ n¾xÂëîÀ2êW(6ÿ~ZŽïq´5ÕÝœÂæ[ÕÞXûP wnZ¹Ó*a‚Õ!ÆçÊò„eÉŒ²N€ÏYwJÙ P€ÉÀ£z ¤{’“;s¥(@ d¥@‹êÚâÛ‹Y)ÎFmð”¸|ûó?îZ³âN=Š!ë¶ØFt«]‹F½¯;ά7|wþK°Ûû<Æ?§áû³üþ*:2f'jªíH¹ÒÎHíï»ÿ½Çu “쇎Øûëñ“êÑïÝ(ñ6/(÷kk¬»Î/7>µ5,|ЧÀ㣾^ü;=èLYÚ:I?G{¦âöËþ¸WH,ÆÏ4tjÌi_\»ºïÁdÿÝÜX÷¤F9©%þ¼f|ªm9äÈgÔsäÖ™N’-³5ÿL{=ó|¦æy‘ÈRÚ¸{>K¯ÄŒ"âÍàÏÈ;•Ÿ¥/H)OÇ{ÏÌm×@Ù´„çµfÉ­Íø®w>n^9ï™ÿHxƒgØ…›«:¬`ã´æÆÚ½Ø*ÀÎg>(@ PÀXwµ]‰`Þµn,Æ@ P€Èe\)¹J_@Ée¶==…Å•'K¡´-]:NmšL‰ßzQ>l[_\`»Ô6MoüÇ,lÁè௠G¶íjRGñà=ç=Ô÷›Ã Öïƒ §)Áž+íì=¯CýÖ#Íá6¦¼þ,fÐSè&{Ù½Ö¸'½½ñ|»(ÕuCyèãºÃJ†Ýið¸Ï$ž1I¨©éFçέ˜Êz :ZÊQš:6âsë÷* ÷Çûõ™øy)É ³o]\ó^ؽB•&k:}äû4:‹f œ‘^³R*£©~×åmJÙèg–i¯gžÏˆOu_Ð3Š´5Ô…×áç”R QÙd݉¯Nb¦±>¯ÇÃZjï÷5Ì •Ó#Æñžy”PÞ‰8¬oœIF‡}Ï{´«û´5Ôþ`u}½ÉÌ ƒÔ–»rIŸsÜ(@ P€æ¡’ò¯á¢Á]æ)I P€ÈALw‹;ÑÏ@Ë“ñÿe“mð}­ ß×fÚ¤ÁSõ9tFn—&³¢'—ŸéIù'ÛZëx‰è5ûüB,$]&¤< ï Ç¢X—;îí]œë…#þØzÈä¿‹j,˜æ-WÚiÊ\TvÉNª«³ }Ócž„t;™¦§Gƒê Ì źB 31J¬¯M(©8OÒo£R³ð“Gå^Àh¯ßµºû~xÞÇÑ_$P:ïhO¹_ÇûI*4.E•Òß‘Ã{ì]J¨z|mLQ¹6ÅÈÂÒò/H%a#JðÇË=³RÔc$ØB“ƒ…¥•ßÅsüº¡*‹A›ä7T>±ÏÀ×sNžÏPI…~­™l¯ ö“ÀTÇô|–vv”bM”sñYzÊG\¼ï<‡÷ÅÈãn¼÷|G^¾KŠì²ÕI÷ôCQÁX êUžÄAwéeyLfGÂsm)Ê+5AÁM¯S[–ÎÙ$–1™)€×7 P€ €@QiÅï±VÔ%v©M P€È·°ÎêÉžî6g4ÙPkqeUcTw—Õ¿÷wÊÛÔ™õ£ª«±/¿gÝq€Qe›±Þh—õ‰ˆ– ¬,ü–Q‡:y$:%÷SRí!…ÜùB¸¨ª;¥û^TÕk7ãq1ñ!Õ›J8/+é>×Þ°ÐÓæ~Úò\iç§-ŽþWYY Ô=æL Îç~ž‚WíßEøŽŸaøéÝ0bZ®EÜG9ÿ_üý®ÿÓñijkšjßí ÊÔßcgÍtóŽŽwÂÖ×€Üm»âù?mêuÐÏýuøY…×Ç*¼NÞVJ¾â)çïk›jÞÏÔ¶ûºÞXç>4võ±B©Óq^ŽÇùÐÖy=>œþŠNŸ¿ ÏåÕé,Åp$FHƒ6èµVõkxWüè×o¯‘~Îⵋu£•X‰v®”Ž|¯ùÇpÓJì7ÞôtÚ­õûBÔmݺ=>I÷4ûùzΡó©ÏOÔ'ѶƒAÏq}zÈŽÕǹ?jõaŽÄg©R˜eŸ¥Rì×¥¾qFßàÕûzÔ³è×£þ¼x Ÿ§+ñYòŒ((x¢¥þæO°/ë7ýo‚`Gøh%ð}NxßÚ¿wÃþ¬…½é÷-}óÏøà;¦÷n”|.°¥ë“gAï*­xï}_è}õwÀÙ¯uÉü7£Æð`F à5É (`)€‹&E£ïí“zD7 P€ ¶ È‚NøØ—.zoû.þA P t–çĈÎ\i§ùs@_«3¥fžkæEÒÁçlâ)sFvçç„eÒ7 `šî=Сº;:0Bx–†Pž7Êèë©qõkpÃÄGBx¢ÓãmG©WU0ðrëÁ»®Ìâ÷5>gõ3`ð-m2±ÎƒëçÚ^~¯0?ã ¶ W<ƒÏ‡ÃM*€åBv÷ýM”& aLDý&Ê (`/€è®ѵ¸ƒw®}b¦ (@ d¥ÀÊ€’§dÈ´¬<;l(@ P€ÉHpFr*É\)@ P ™¡’J, ‚YL 6· 0vm}ÑÚöÙ1ćì|öáIa•(@ d€ —Ó±\:dz^W5™U¥(@ "€5ž»Ãùåœj{î¢(@ P€ (@¬Ø¥¸jD—tõtæ&}Ž›°¾¸ž"[ ĺØx“°i (`! Z›Üèy˜”âi‹t ¥(@ d‹@ Ö¨¼ µ¡î v (‹ÀJ$zTJ¹´eýn+ÄcÕáX2a P€ (@ P€ @¶(Gžlz;¶ôÔsÙÒn¶#²€é0øÈ9ð(@ P šÀ‰ÕÁ c?üŒ'¼Ý„+wRÂãOѼxŒ |% ¥Z/…\çtt½¹fÅ›|U9V† (@ P€ (Fmë=·¡ ÃLªe«ŽÃìaO˜Ä2&sعçŽ5§(Ö¼u-®ç‘gŒµ¤(@ P€ (@ P€ (0¤@§tË0ÊÕ¨ã™uŒÜè=ß2d® Èt®ùœégõ§(@ P€ (@ P€ (@ P€ @*ª«t<_eZ¤’bù{-ê0g\æ päsæž;Öœ (@ P€ (@¢œª =IDAT P€ (@ P KB%声Ô Oz—µ5,|ÆOÍ ½´ªõÙ߸NJÞcËÀŒàÈçŒ>}¬<(@ P€ (@ P€ (@ P€ @Ö `d±Pòn%Ä‘R9+ŠJ+óKÑ)¾‹Pâ:Óú`„tÛ¸îáËMã—Ùì|ÎìóÇÚS€ (@ P€ (@ P€ (@ d™@á «®RokÖh¥Äƒ¡’Н¤½™eeùBÈ? !Óº(¡nZùÀM¦ñŒËl@fWŸµ§(@ P€ (@ P€ (@ P€ @ö—Ÿ-¤üm¿åáñY#÷:lóÁS¯¿ŽAÑ)ÞÊÊ¡®Ñõ(õT‹’7uȯv¾þÒ‹4 Í`v>gðÉcÕ)@ P€ (@ P€ (@ P€ ²G hÖ¼C”Mh‘îl¸Iyì·àÔáûNbË›/´ HΞ1§_gôécå)@ P€ (@ P€ (@ P€ ²A`âì‹&xžûÚ2Ô”Ö“¥TóF|vš3ò £^Øüús]ÉlaqåÉÇ]†2¦[–óöNܯ­{ïå°e:†g°ÖøæF P€ (@ P€ (@ P€ (@ ¤Mk)‡:G?„ò·«ƒjJ\‡õ¡ëZl´K=º¨¤b FaW ¡¾=rð£Jª/µ5,xxð£Ü›­ì|ÎÖ3ËvQ€ (@ P€ (@ P€ (@ d„@¨¸âVt _Ge7cè?%ïµ1üÔ{-êˆ%¯)3.-X›¿é4G9s”P%È#¦Y”Ñy]KcÝ÷c©Ód¶;Ÿ3ûü±ö (@ P€ (@ P€ (@ P€,*)¿HyKâš · ¯'¥PÏ ¥Þ”R¾…Žé5*Øöºô1tò‡;ž¡è³昀“cíes)@ P€ (@ P€ (@ P€ (à+¶%µÿv Ó¥K|U1ÓÊ(ñl°Û;†Ϧ`ÙÇ‘ÏÙ{nÙ2 P€ (@ P€ (@ P€ (@ •TVaìPí‘™Put6Þ^P°áâÕõõ=ëIgBYÇä °ó9y¶Ì™ (@ P€ (@ P€ (@ P€Ö㊫vÈðï±óLëÄ©J D;V”¾¸¥iÁ}©*’åø_€Ïþ?G¬!(@ P€ (@ P€ (@ P€ @ L(wª§¼Ÿ¡é‡ø¨ùJIq»Óí~¯åþEû¨^¬ŠØù샓À*P€ (@ P€ (@ P€ (@ P ‚€,,®(uñ¥ÄQbR±[¡û!¿ßÜXûÏTÈ22O€Ï™wÎXc P€ (@ P€ (@ P€ (@(š5ï婯£éeXz×l@‡âÂsÛ²lÑ[)*“Åd¨;Ÿ3ôıÚ (@ P€ (@ P€ (@ P€¹+0±tî®8UHy*:¢‡ÄèÄiÈ”P:JýiLxä#+¸©3qy3§l`çs6Ÿ]¶ (@ P€ (@ P€ (@ P '&ž^¾—rÄTOŠ„’“•T»I!wCã ñ3 ?#ñÄÞÂøÙ„ŸRŠUJ©÷”tÞEgóËnP>ݾ¸vµâF P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (@ P€ (ŸÀÿ“.›6™°èÙIEND®B`‚pystac-1.9.0/docs/api.rst000066400000000000000000000223331451576074700153020ustar00rootroot00000000000000API Reference ============= .. toctree:: :hidden: :maxdepth: 2 :glob: api/pystac api/* This API reference is auto-generated from the Python docstrings. The table of contents on the left is organized by module. The sections below are organized based on concepts and sections within the :stac-spec:`STAC Spec <>` and PySTAC itself. Base Structures & Classes ------------------------- These are the core Python classes representing entities within the STAC Spec. These classes provide convenient methods for serializing and deserializing from JSON, extracting properties, and creating relationships between entities. * :class:`pystac.Link`: Represents a :stac-spec:`Link Object `. * :class:`pystac.MediaType`: Provides common values used in the Link and Asset ``"type"`` fields. * :class:`pystac.RelType`: Provides common values used in the Link ``"rel"`` field. * :class:`pystac.STACObject`: Base class implementing functionality common to :class:`Catalog `, :class:`Collection ` and :class:`Item `. Items ----- Representations of :stac-spec:`Items ` and related structures like :stac-spec:`Asset Objects`. * :class:`pystac.Asset`: Represents an :stac-spec:`Asset Object ` * :class:`pystac.Item`: Represents an :stac-spec:`Item ` * :class:`pystac.CommonMetadata`: A container for fields defined in the :stac-spec:`Common Metadata ` section of the spec. These fields are commonly found in STAC Item properties, but may be found elsewhere. Collections ----------- These are representations of :stac-spec:`Collections ` and related structures. * :class:`pystac.Collection`: Represents a :stac-spec:`Collection `. * :class:`pystac.Extent`: Represents an :stac-spec:`Extent Object `, which is composed of :class:`pystac.SpatialExtent` and :class:`pystac.TemporalExtent` instances. * :class:`pystac.Provider`: Represents a :stac-spec:`Provider Object `. The :class:`pystac.ProviderRole` enum provides common values used in the ``"roles"`` field. * :class:`pystac.Summaries`: Class for working with various types of :stac-spec:`CollectionSummaries ` * :class:`pystac.ItemCollection`: Represents a GeoJSON FeatureCollection in which all Features are STAC Items. Catalogs -------- Representations of :stac-spec:`Catalogs ` and related structures. * :class:`pystac.Catalog`: Represents a :stac-spec:`Catalog `. * :class:`pystac.CatalogType`: Enum representing the common types of Catalogs described in the :stac-spec:`STAC Best Practices ` I/O --- These classes are used to read and write files from disk or over the network, as well as to serialize and deserialize STAC object to and from JSON. * :class:`pystac.StacIO`: Base class that can be inherited to provide custom I/O * :class:`pystac.stac_io.DefaultStacIO`: The default :class:`pystac.StacIO` implementation used throughout the library. Extensions ---------- PySTAC provides support for the following STAC Extensions: * :mod:`Datacube ` * :mod:`Electro-Optical ` * :mod:`File Info ` * :mod:`Item Assets ` * :mod:`MGRS ` * :mod:`Point Cloud ` * :mod:`Projection ` * :mod:`Raster ` * :mod:`SAR ` * :mod:`Satellite ` * :mod:`Scientific Citation ` * :mod:`Table ` * :mod:`Timestamps ` * :mod:`Versioning Indicators ` * :mod:`View Geometry ` * :mod:`Xarray Assets ` The following classes are used internally to implement these extensions and may be used to create custom implementations of STAC Extensions not supported by the library (see :tutorial:`Adding New and Custom Extensions ` for details): * :class:`pystac.extensions.base.SummariesExtension`: Base class for extending the properties in :attr:`pystac.Collection.summaries` to include properties defined by a STAC Extension. * :class:`pystac.extensions.base.PropertiesExtension`: Abstract base class for extending the properties of an :class:`~pystac.Item` to include properties defined by a STAC Extension. * :class:`pystac.extensions.base.ExtensionManagementMixin`: Abstract base class with methods for adding and removing extensions from STAC Objects. * :class:`pystac.extensions.hooks.ExtensionHooks`: Used to implement hooks when extending a STAC Object. Primarily used to implement migrations from one extension version to another. * :class:`pystac.extensions.hooks.RegisteredExtensionHooks`: Used to register hooks defined in :class:`~pystac.extensions.hooks.ExtensionHooks` instances to ensure they are used in object deserialization. Catalog Layout -------------- These classes are used to set the HREFs of a STAC according to some layout. The templating functionality is also used when generating subcatalogs based on a template. * :class:`pystac.layout.LayoutTemplate`: Represents a template that can be used for deriving paths or other information based on properties of STAC objects supplied as a template string. * :class:`pystac.layout.BestPracticesLayoutStrategy`: Layout strategy that represents the catalog layout described in the :stac-spec:`STAC Best Practices documentation `. * :class:`pystac.layout.TemplateLayoutStrategy`: Layout strategy that can take strings to be supplied to a :class:`~pystac.layout.LayoutTemplate` to derive paths. * :class:`pystac.layout.CustomLayoutStrategy`: Layout strategy that allows users to supply functions to dictate stac object paths. Errors ------ The following exceptions may be raised internally by the library. * :class:`pystac.STACError`: Generic STAC-related error * :class:`pystac.STACTypeError`: Raised when a representation of a STAC entity is encountered that is not correct for the context * :class:`pystac.DuplicateObjectKeyError`: Raised when deserializing a JSON object containing a duplicate key. * :class:`pystac.ExtensionAlreadyExistsError`: Raised when deserializing a JSON object containing a duplicate key. * :class:`pystac.ExtensionTypeError`: Raised when an extension is used against an object to which that the extension does not apply to. * :class:`pystac.ExtensionNotImplemented`: Raised on an attempt to extend a STAC object that does not implement the given extension. * :class:`pystac.RequiredPropertyMissing`: Raised when a required value is expected to be present but is missing or ``None``. * :class:`pystac.STACValidationError`: Raised by validation calls if the STAC JSON is invalid. * :class:`pystac.TemplateError`: Raised when an error occurs while converting a template string into data for :class:`~pystac.layout.LayoutTemplate`. Serialization ------------- The ``pystac.serialization`` sub-package contains tools used internally by PySTAC to identify, serialize, and migrate STAC objects: * :mod:`pystac.serialization.identify`: Tools for identifying STAC objects * :mod:`pystac.serialization.migrate`: Tools for migrating STAC objects from a previous STAC Spec version. Validation ---------- .. note:: The tools described here require that you install PySTAC with the ``validation`` extra (see the documentation on :ref:`installing dependencies ` for details). PySTAC includes a ``pystac.validation`` package for validating STAC objects, including from PySTAC objects and directly from JSON. * :class:`pystac.validation.stac_validator.STACValidator`: Abstract base class defining methods for validating STAC JSON. Implementations define methods for validating core objects and extension. * :class:`pystac.validation.stac_validator.JsonSchemaSTACValidator`: The default :class:`~pystac.validation.stac_validator.STACValidator` implementation used by PySTAC. Uses JSON schemas read from URIs provided by a :class:`~pystac.validation.schema_uri_map.SchemaUriMap`, to validate STAC objects. * :class:`pystac.validation.schema_uri_map.SchemaUriMap`: Defines methods for mapping STAC versions, object types and extension ids to schema URIs. A default implementation is included that uses known locations; however users can provide their own schema URI maps in a :class:`~pystac.validation.stac_validator.JsonSchemaSTACValidator` to modify the URIs used. * :class:`pystac.validation.schema_uri_map.DefaultSchemaUriMap`: The default :class:`~pystac.validation.schema_uri_map.SchemaUriMap` used by PySTAC. Internal Classes ----------------------- These classes are used internally by PySTAC for caching. * :class:`pystac.cache.ResolvedObjectCache` pystac-1.9.0/docs/api/000077500000000000000000000000001451576074700145455ustar00rootroot00000000000000pystac-1.9.0/docs/api/asset.rst000066400000000000000000000001501451576074700164120ustar00rootroot00000000000000pystac.asset ============ .. automodule:: pystac.asset :members: :undoc-members: :noindex: pystac-1.9.0/docs/api/cache.rst000066400000000000000000000001321451576074700163360ustar00rootroot00000000000000pystac.cache ============ .. automodule:: pystac.cache :members: :undoc-members: pystac-1.9.0/docs/api/catalog.rst000066400000000000000000000001561451576074700167130ustar00rootroot00000000000000pystac.catalog ============== .. automodule:: pystac.catalog :members: :undoc-members: :noindex: pystac-1.9.0/docs/api/collection.rst000066400000000000000000000001671451576074700174360ustar00rootroot00000000000000pystac.collection ================= .. automodule:: pystac.collection :members: :undoc-members: :noindex: pystac-1.9.0/docs/api/common_metadata.rst000066400000000000000000000002061451576074700204250ustar00rootroot00000000000000pystac.common_metadata ====================== .. automodule:: pystac.common_metadata :members: :undoc-members: :noindex: pystac-1.9.0/docs/api/errors.rst000066400000000000000000000001531451576074700166120ustar00rootroot00000000000000pystac.errors ============= .. automodule:: pystac.errors :members: :undoc-members: :noindex: pystac-1.9.0/docs/api/extensions.rst000066400000000000000000000013201451576074700174720ustar00rootroot00000000000000pystac.extensions ================= .. toctree:: :hidden: :maxdepth: 2 :glob: extensions/* .. currentmodule:: pystac.extensions .. autosummary:: datacube.DatacubeExtension classification.ClassificationExtension eo.EOExtension file.FileExtension grid.GridExtension item_assets.ItemAssetsExtension mgrs.MgrsExtension pointcloud.PointcloudExtension projection.ProjectionExtension raster.RasterExtension sar.SarExtension sat.SatExtension scientific.ScientificExtension storage.StorageExtension table.TableExtension timestamps.TimestampsExtension version.VersionExtension view.ViewExtension xarray_assets.XarrayAssetsExtension pystac-1.9.0/docs/api/extensions/000077500000000000000000000000001451576074700167445ustar00rootroot00000000000000pystac-1.9.0/docs/api/extensions/base.rst000066400000000000000000000001641451576074700204110ustar00rootroot00000000000000pytac.extensions.base ===================== .. automodule:: pystac.extensions.base :members: :undoc-members: pystac-1.9.0/docs/api/extensions/classification.rst000066400000000000000000000002221451576074700224650ustar00rootroot00000000000000pytac.extensions.classification =============================== .. automodule:: pystac.extensions.classification :members: :undoc-members: pystac-1.9.0/docs/api/extensions/datacube.rst000066400000000000000000000002021451576074700212400ustar00rootroot00000000000000pystac.extensions.datacube ========================== .. automodule:: pystac.extensions.datacube :members: :undoc-members: pystac-1.9.0/docs/api/extensions/eo.rst000066400000000000000000000001601451576074700200760ustar00rootroot00000000000000pystac.extensions.eo ==================== .. automodule:: pystac.extensions.eo :members: :undoc-members: pystac-1.9.0/docs/api/extensions/file.rst000066400000000000000000000001661451576074700204200ustar00rootroot00000000000000pystac.extensions.file ====================== .. automodule:: pystac.extensions.file :members: :undoc-members: pystac-1.9.0/docs/api/extensions/grid.rst000066400000000000000000000001641451576074700204240ustar00rootroot00000000000000pytac.extensions.grid ===================== .. automodule:: pystac.extensions.grid :members: :undoc-members: pystac-1.9.0/docs/api/extensions/hooks.rst000066400000000000000000000001711451576074700206200ustar00rootroot00000000000000pystac.extensions.hooks ======================= .. automodule:: pystac.extensions.hooks :members: :undoc-members: pystac-1.9.0/docs/api/extensions/item_assets.rst000066400000000000000000000002151451576074700220140ustar00rootroot00000000000000pystac.extensions.item\_assets ============================== .. automodule:: pystac.extensions.item_assets :members: :undoc-members: pystac-1.9.0/docs/api/extensions/mgrs.rst000066400000000000000000000001741451576074700204500ustar00rootroot00000000000000pystac.extensions.mgrs ============================ .. automodule:: pystac.extensions.mgrs :members: :undoc-members: pystac-1.9.0/docs/api/extensions/pointcloud.rst000066400000000000000000000002101451576074700216470ustar00rootroot00000000000000pystac.extensions.pointcloud ============================ .. automodule:: pystac.extensions.pointcloud :members: :undoc-members: pystac-1.9.0/docs/api/extensions/projection.rst000066400000000000000000000002101451576074700216430ustar00rootroot00000000000000pystac.extensions.projection ============================ .. automodule:: pystac.extensions.projection :members: :undoc-members: pystac-1.9.0/docs/api/extensions/raster.rst000066400000000000000000000001741451576074700210000ustar00rootroot00000000000000pystac.extensions.raster ======================== .. automodule:: pystac.extensions.raster :members: :undoc-members: pystac-1.9.0/docs/api/extensions/sar.rst000066400000000000000000000001631451576074700202630ustar00rootroot00000000000000pystac.extensions.sar ===================== .. automodule:: pystac.extensions.sar :members: :undoc-members: pystac-1.9.0/docs/api/extensions/sat.rst000066400000000000000000000001631451576074700202650ustar00rootroot00000000000000pystac.extensions.sat ===================== .. automodule:: pystac.extensions.sat :members: :undoc-members: pystac-1.9.0/docs/api/extensions/scientific.rst000066400000000000000000000002101451576074700216070ustar00rootroot00000000000000pystac.extensions.scientific ============================ .. automodule:: pystac.extensions.scientific :members: :undoc-members: pystac-1.9.0/docs/api/extensions/storage.rst000066400000000000000000000002021451576074700211340ustar00rootroot00000000000000pystac.extensions.storage ============================ .. automodule:: pystac.extensions.storage :members: :undoc-members: pystac-1.9.0/docs/api/extensions/table.rst000066400000000000000000000001711451576074700205640ustar00rootroot00000000000000pystac.extensions.table ======================= .. automodule:: pystac.extensions.table :members: :undoc-members: pystac-1.9.0/docs/api/extensions/timestamps.rst000066400000000000000000000002101451576074700216550ustar00rootroot00000000000000pystac.extensions.timestamps ============================ .. automodule:: pystac.extensions.timestamps :members: :undoc-members: pystac-1.9.0/docs/api/extensions/version.rst000066400000000000000000000001771451576074700211700ustar00rootroot00000000000000pystac.extensions.version ========================= .. automodule:: pystac.extensions.version :members: :undoc-members: pystac-1.9.0/docs/api/extensions/view.rst000066400000000000000000000001661451576074700204530ustar00rootroot00000000000000pystac.extensions.view ====================== .. automodule:: pystac.extensions.view :members: :undoc-members: pystac-1.9.0/docs/api/extensions/xarray_assets.rst000066400000000000000000000002211451576074700223610ustar00rootroot00000000000000pystac.extensions.xarray_assets =============================== .. automodule:: pystac.extensions.xarray_assets :members: :undoc-members: pystac-1.9.0/docs/api/item.rst000066400000000000000000000001451451576074700162350ustar00rootroot00000000000000pystac.item =========== .. automodule:: pystac.item :members: :undoc-members: :noindex: pystac-1.9.0/docs/api/item_collection.rst000066400000000000000000000002061451576074700204460ustar00rootroot00000000000000pystac.item_collection ====================== .. automodule:: pystac.item_collection :members: :undoc-members: :noindex: pystac-1.9.0/docs/api/layout.rst000066400000000000000000000001351451576074700166130ustar00rootroot00000000000000pystac.layout ============= .. automodule:: pystac.layout :members: :undoc-members: pystac-1.9.0/docs/api/link.rst000066400000000000000000000001451451576074700162340ustar00rootroot00000000000000pystac.link =========== .. automodule:: pystac.link :members: :undoc-members: :noindex: pystac-1.9.0/docs/api/media_type.rst000066400000000000000000000001671451576074700174230ustar00rootroot00000000000000pystac.media_type ================= .. automodule:: pystac.media_type :members: :undoc-members: :noindex: pystac-1.9.0/docs/api/provider.rst000066400000000000000000000001611451576074700171270ustar00rootroot00000000000000pystac.provider =============== .. automodule:: pystac.provider :members: :undoc-members: :noindex: pystac-1.9.0/docs/api/pystac.rst000066400000000000000000000053631451576074700166110ustar00rootroot00000000000000pystac ------ .. automodule:: pystac :members: read_file, write_file, read_dict, set_stac_version, get_stac_version .. autosummary:: STACObject Catalog Collection Extent SpatialExtent TemporalExtent Provider Summaries Item Asset CommonMetadata ItemCollection Link StacIO read_file write_file read_dict set_stac_version get_stac_version STACObject ---------- .. autoclass:: pystac.STACObject :members: :inherited-members: :undoc-members: .. autoclass:: pystac.STACObjectType :members: :undoc-members: Catalog ------- .. autoclass:: pystac.Catalog :members: :undoc-members: CatalogType ----------- .. autoclass:: pystac.CatalogType :members: :inherited-members: :undoc-members: Collection ---------- .. autoclass:: pystac.Collection :members: :undoc-members: Extent ------ .. autoclass:: pystac.Extent :members: :undoc-members: SpatialExtent ------------- .. autoclass:: pystac.SpatialExtent :members: :undoc-members: TemporalExtent -------------- .. autoclass:: pystac.TemporalExtent :members: :undoc-members: ProviderRole ------------ .. autoclass:: pystac.ProviderRole :members: :undoc-members: Provider -------- .. autoclass:: pystac.Provider :members: :undoc-members: Summaries --------- .. autoclass:: pystac.Summaries :members: :undoc-members: Item ---- .. autoclass:: pystac.Item :members: :undoc-members: Asset ----- .. autoclass:: pystac.Asset :members: :undoc-members: CommonMetadata -------------- .. autoclass:: pystac.CommonMetadata :members: :undoc-members: ItemCollection -------------- .. autoclass:: pystac.ItemCollection :members: Link ---- .. autoclass:: pystac.Link :members: :undoc-members: MediaType --------- .. autoclass:: pystac.MediaType :members: :undoc-members: RelType ------- .. autoclass:: pystac.RelType :members: :undoc-members: StacIO ------ .. autoclass:: pystac.StacIO :members: :undoc-members: Errors ------ STACError ~~~~~~~~~ .. autoclass:: pystac.STACError STACTypeError ~~~~~~~~~~~~~ .. autoclass:: pystac.STACTypeError DuplicateObjectKeyError ~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: pystac.DuplicateObjectKeyError ExtensionAlreadyExistsError ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: pystac.ExtensionAlreadyExistsError ExtensionTypeError ~~~~~~~~~~~~~~~~~~ .. autoclass:: pystac.ExtensionTypeError ExtensionNotImplemented ~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: pystac.ExtensionNotImplemented RequiredPropertyMissing ~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: pystac.RequiredPropertyMissing STACValidationError ~~~~~~~~~~~~~~~~~~~ .. autoclass:: pystac.STACValidationError pystac-1.9.0/docs/api/rel_type.rst000066400000000000000000000001611451576074700171200ustar00rootroot00000000000000pystac.rel_type =============== .. automodule:: pystac.rel_type :members: :undoc-members: :noindex: pystac-1.9.0/docs/api/serialization.rst000066400000000000000000000002521451576074700201530ustar00rootroot00000000000000pystac.serialization ==================== .. toctree:: :hidden: :maxdepth: 2 :glob: serialization/* .. automodule:: pystac.serialization :members: pystac-1.9.0/docs/api/serialization/000077500000000000000000000000001451576074700174225ustar00rootroot00000000000000pystac-1.9.0/docs/api/serialization/common_properties.rst000066400000000000000000000002501451576074700237150ustar00rootroot00000000000000pystac.serialization.common\_properties ======================================= .. automodule:: pystac.serialization.common_properties :members: :undoc-members: pystac-1.9.0/docs/api/serialization/identify.rst000066400000000000000000000002301451576074700217620ustar00rootroot00000000000000pystac.serialization.identify ============================= .. automodule:: pystac.serialization.identify :members: :undoc-members: :noindex: pystac-1.9.0/docs/api/serialization/migrate.rst000066400000000000000000000002101451576074700215750ustar00rootroot00000000000000pystac.serialization.migrate ============================ .. automodule:: pystac.serialization.migrate :members: :undoc-members: pystac-1.9.0/docs/api/stac_io.rst000066400000000000000000000001401451576074700167130ustar00rootroot00000000000000pystac.stac_io ============== .. automodule:: pystac.stac_io :members: :undoc-members: pystac-1.9.0/docs/api/stac_object.rst000066400000000000000000000001721451576074700175570ustar00rootroot00000000000000pystac.stac_object ================== .. automodule:: pystac.stac_object :members: :undoc-members: :noindex: pystac-1.9.0/docs/api/summaries.rst000066400000000000000000000001641451576074700173050ustar00rootroot00000000000000pystac.summaries ================ .. automodule:: pystac.summaries :members: :undoc-members: :noindex: pystac-1.9.0/docs/api/utils.rst000066400000000000000000000001321451576074700164330ustar00rootroot00000000000000pystac.utils ============ .. automodule:: pystac.utils :members: :undoc-members: pystac-1.9.0/docs/api/validation.rst000066400000000000000000000002361451576074700174320ustar00rootroot00000000000000pystac.validation ================= .. toctree:: :hidden: :maxdepth: 2 :glob: validation/* .. automodule:: pystac.validation :members: pystac-1.9.0/docs/api/validation/000077500000000000000000000000001451576074700166775ustar00rootroot00000000000000pystac-1.9.0/docs/api/validation/schema_uri_map.rst000066400000000000000000000002301451576074700224000ustar00rootroot00000000000000pystac.validation.schema\_uri\_map ================================== .. automodule:: pystac.validation.schema_uri_map :members: :undoc-members: pystac-1.9.0/docs/api/validation/stac_validator.rst000066400000000000000000000002261451576074700224300ustar00rootroot00000000000000pystac.validation.stac\_validator ================================= .. automodule:: pystac.validation.stac_validator :members: :undoc-members: pystac-1.9.0/docs/api/version.rst000066400000000000000000000001561451576074700167660ustar00rootroot00000000000000pystac.version ============== .. automodule:: pystac.version :members: :undoc-members: :noindex: pystac-1.9.0/docs/concepts.rst000066400000000000000000000771001451576074700163510ustar00rootroot00000000000000Concepts ######## This page will give an overview of some important concepts to understand when working with PySTAC. If you want to check code examples, see the :ref:`tutorials`. .. _stac_version_support: STAC Spec Version Support ========================= The latest version of PySTAC supports STAC Spec |stac_version| and will automatically update any catalogs to this version. To work with older versions of the STAC Spec, please use an older version of PySTAC: ================= ============== STAC Spec Version PySTAC Version ================= ============== >=1.0 Latest 0.9 0.4.* 0.8 0.3.* <0.8 *Not supported* ================= ============== Reading STACs ============= PySTAC can read STAC data from JSON. Generally users read in the root catalog, and then use the python objects to crawl through the data. Once you read in the root of the STAC, you can work with the STAC in memory. .. code-block:: python from pystac import Catalog catalog = Catalog.from_file('/some/example/catalog.json') for root, catalogs, items in catalog.walk(): # Do interesting things with the STAC data. To see how to hook into PySTAC for reading from alternate URIs such as cloud object storage, see :ref:`using stac_io`. Writing STACs ============= While working with STACs in-memory don't require setting file paths, in order to save a STAC, you'll need to give each STAC object a ``self`` link that describes the location of where it should be saved to. Luckily, PySTAC makes it easy to create a STAC catalog with a :stac-spec:`canonical layout ` and with the links that follow the :stac-spec:`best practices `. You simply call ``normalize_hrefs`` with the root directory of where the STAC will be saved, and then call ``save`` with the type of catalog (described in the :ref:`catalog types` section) that matches your use case. .. code-block:: python from pystac import (Catalog, CatalogType) catalog = Catalog.from_file('/some/example/catalog.json') catalog.normalize_hrefs('/some/copy/') catalog.save(catalog_type=CatalogType.SELF_CONTAINED) copycat = Catalog.from_file('/some/copy/catalog.json') Normalizing HREFs ----------------- The ``normalize_hrefs`` call sets HREFs for all the links in the STAC according to the Catalog, Collection and Items, all based off of the root URI that is passed in: .. code-block:: python catalog.normalize_hrefs('/some/location') catalog.save(catalog_type=CatalogType.SELF_CONTAINED) This will lay out the HREFs of the STAC according to the :stac-spec:`best practices document `. Layouts ~~~~~~~ PySTAC provides a few different strategies for laying out the HREFs of a STAC. To use them you can pass in a strategy to the normalize_hrefs call. Using templates ''''''''''''''' You can utilize template strings to determine the file paths of HREFs set on Catalogs, Collection or Items. These templates use python format strings, which can name the property or attribute of the item you want to use for replacing the template variable. For example: .. code-block:: python from pystac.layout import TemplateLayoutStrategy strategy = TemplateLayoutStrategy(item_template="${collection}/${year}/${month}") catalog.normalize_hrefs('/some/location', strategy=strategy) catalog.save(catalog_type=CatalogType.SELF_CONTAINED) The above code will save items in subfolders based on the collection ID, year and month of it's datetime (or start_datetime if a date range is defined and no datetime is defined). Note that the forward slash (``/``) should be used as path separator in the template string regardless of the system path separator (thus both in POSIX-compliant and Windows environments). You can use dot notation to specify attributes of objects or keys in dictionaries for template variables. PySTAC will look at the object, it's ``properties`` and its ``extra_fields`` for property names or dictionary keys. Some special cases, like ``year``, ``month``, ``day`` and ``date`` exist for datetime on Items, as well as ``collection`` for Item's Collection's ID. See the documentation on :class:`~pystac.layout.LayoutTemplate` for more documentation on how layout templates work. Using custom functions '''''''''''''''''''''' If you want to build your own strategy, you can subclass ``HrefLayoutStrategy`` or use :class:`~pystac.layout.CustomLayoutStrategy` to provide functions that work with Catalogs, Collections or Items. Similar to the templating strategy, you can provide a fallback strategy (which defaults to :class:`~pystac.layout.BestPracticesLayoutStrategy`) for any stac object type that you don't supply a function for. .. _catalog types: Catalog Types ------------- The STAC :stac-spec:`best practices document ` lays out different catalog types, and how their links should be formatted. A brief description is below, but check out the document for the official take on these types: The catalog types will also dictate the asset HREF formats. Asset HREFs in any catalog type can be relative or absolute may be absolute depending on their location; see the section on :ref:`rel vs abs asset` below. Self-Contained Catalogs ~~~~~~~~~~~~~~~~~~~~~~~ A self-contained catalog (indicated by ``catalog_type=CatalogType.SELF_CONTAINED``) applies to STACs that do not have a long term location, and can be moved around. These STACs are useful for copying data to and from locations, without having to change any link metadata. A self-contained catalog has two important properties: - It contains only relative links - It contains **no** self links. For a catalog that is the most easy to copy around, it's recommended that item assets use relative links, and reside in the same directory as the item's STAC metadata file. Relative Published Catalogs ~~~~~~~~~~~~~~~~~~~~~~~~~~~ A relative published catalog (indicated by ``catalog_type=CatalogType.RELATIVE_PUBLISHED``) is one that is tied at it's root to a specific location, but otherwise contains relative links. This is designed so that a self-contained catalog can be 'published' online by just adding one field (the self link) to its root catalog. A relative published catalog has the following properties: - It contains **only one** self link: the root of the catalog contains a (necessarily absolute) link to it's published location. - All other objects in the STAC contain relative links, and no self links. Absolute Published Catalogs ~~~~~~~~~~~~~~~~~~~~~~~~~~~ An absolute published catalog (indicated by ``catalog_type=CatalogType.ABSOLUTE_PUBLISHED``) uses absolute links for everything. It is preferable where possible, since it allows for the easiest provenance tracking out of all the catalog types. An absolute published catalog has the following properties: - Each STAC object contains only absolute links. - Each STAC object has a self link. It is not recommended to have relative asset HREFs in an absolute published catalog. Relative vs Absolute HREFs -------------------------- HREFs inside a STAC for either links or assets can be relative or absolute. Relative vs Absolute Link HREFs ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Absolute links point to their file locations in a fully described way. Relative links are relative to the linking object's file location. For example, if a catalog at ``/some/location/catalog.json`` has a link to an item that has an HREF set to ``item-id/item-id.json``, then that link should resolve to the absolute path ``/some/location/item-id/item-id.json``. Links are set as absolute or relative HREFs at save time, as determine by the root catalog's catalog_type :attr:`~pystac.Catalog.catalog_type`. This means that, even if the stored HREF of the link is absolute, if the root ``catalog_type=CatalogType.RELATIVE_PUBLISHED`` or ``catalog_type=CatalogType.SELF_CONTAINED`` and subsequent serializing of the any links in the catalog will produce a relative link, based on the self link of the parent object. You can make all the links of a catalog relative or absolute by setting the :func:`Catalog.catalog_type` field then resaving the entire catalog. .. _rel vs abs asset: Relative vs Absolute Asset HREFs ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Asset HREFs can also be relative or absolute. If an asset HREF is relative, then it is relative to the Item's metadata file. For example, if the item at ``/some/location/item-id/item-id.json`` had an asset with an HREF of ``./image.tif``, then the fully resolved path for that image would be ``/some/location/item-id/image.tif`` You can make all the asset HREFs of a catalog relative or absolute using the :func:`Catalog.make_all_asset_hrefs_relative ` and :func:`Catalog.make_all_asset_hrefs_absolute ` methods. Note that these will not move any files around, and if the file location does not share a common parent with the asset's item's self HREF, then the asset HREF will remain absolute as no relative path is possible. Including a ``self`` link ------------------------- Every stac object has a :func:`~pystac.STACObject.save_object` method, that takes as an argument whether or not to include the object's self link. As noted in the section on :ref:`catalog types`, a self link is necessarily absolute; if an object only contains relative links, then it cannot contain the self link. PySTAC uses self links as a way of tracking the object's file location, either what it was read from or it's pending save location, so each object can have a self link even if you don't ever want that self link written (e.g. if you are working with self-contained catalogs). .. _using stac_io: I/O in PySTAC ============= The :class:`pystac.StacIO` class defines fundamental methods for I/O operations within PySTAC, including serialization and deserialization to and from JSON files and conversion to and from Python dictionaries. This is an abstract class and should not be instantiated directly. However, PySTAC provides a :class:`pystac.stac_io.DefaultStacIO` class with minimal implementations of these methods. This default implementation provides support for reading and writing files from the local filesystem as well as HTTP URIs (using ``urllib``). This class is created automatically by all of the object-specific I/O methods (e.g. :meth:`pystac.Catalog.from_file`), so most users will not need to instantiate this class themselves. If you are dealing with a STAC catalog with URIs that require authentication. It is possible provide auth headers (or any other customer headers) to the :class:`pystac.stac_io.DefaultStacIO`. .. code-block:: python from pystac import Catalog from pystac import StacIO stac_io = StacIO.default() stac_io.headers = {"Authorization": ""} catalog = Catalog.from_file("", stac_io=stac_io) You can double check that requests PySTAC is making by adjusting logging level so that you see all API calls. .. code-block:: python import logging logging.basicConfig() logger = logging.getLogger('pystac') logger.setLevel(logging.DEBUG) If you require more custom logic for I/O operations or would like to use a 3rd-party library for I/O operations (e.g. ``requests``), you can create a sub-class of :class:`pystac.StacIO` (or :class:`pystac.DefaultStacIO`) and customize the methods as you see fit. You can then pass instances of this custom sub-class into the ``stac_io`` argument of most object-specific I/O methods. You can also use :meth:`pystac.StacIO.set_default` in your client's ``__init__.py`` file to make this sub-class the default :class:`pystac.StacIO` implementation throughout the library. For example, this code will allow for reading from AWS's S3 cloud object storage using `boto3 `__: .. code-block:: python from urllib.parse import urlparse import boto3 from pystac import Link from pystac.stac_io import DefaultStacIO, StacIO class CustomStacIO(DefaultStacIO): def __init__(self): self.s3 = boto3.resource("s3") def read_text( self, source: Union[str, Link], *args: Any, **kwargs: Any ) -> str: parsed = urlparse(source) if parsed.scheme == "s3": bucket = parsed.netloc key = parsed.path[1:] obj = self.s3.Object(bucket, key) return obj.get()["Body"].read().decode("utf-8") else: return super().read_text(source, *args, **kwargs) def write_text( self, dest: Union[str, Link], txt: str, *args: Any, **kwargs: Any ) -> None: parsed = urlparse(dest) if parsed.scheme == "s3": bucket = parsed.netloc key = parsed.path[1:] self.s3.Object(bucket, key).put(Body=txt, ContentEncoding="utf-8") else: super().write_text(dest, txt, *args, **kwargs) StacIO.set_default(CustomStacIO) If you only need to customize read operations you can inherit from :class:`~pystac.stac_io.DefaultStacIO` and only overwrite the read method. For example, to take advantage of connection pooling using a `requests.Session `__: .. code-block:: python from urllib.parse import urlparse import requests from pystac.stac_io import DefaultStacIO, StacIO class ConnectionPoolingIO(DefaultStacIO): def __init__(self): self.session = requests.Session() def read_text( self, source: Union[str, Link], *args: Any, **kwargs: Any ) -> str: parsed = urlparse(uri) if parsed.scheme.startswith("http"): return self.session.get(uri).text else: return super().read_text(source, *args, **kwargs) StacIO.set_default(ConnectionPoolingIO) .. _validation_concepts: Validation ========== PySTAC includes validation functionality that allows users to validate PySTAC objects as well JSON-encoded STAC objects from STAC versions `0.8.0` and later. Enabling validation ------------------- To enable the validation feature you'll need to have installed PySTAC with the optional dependency via: .. code-block:: bash > pip install pystac[validation] This installs the ``jsonschema`` package which is used with the default validator. If you define your own validation class as described below, you are not required to have this extra dependency. Validating PySTAC objects ------------------------- You can validate any :class:`~pystac.Catalog`, :class:`~pystac.Collection` or :class:`~pystac.Item` by calling the :meth:`~pystac.STACObject.validate` method: .. code-block:: python item.validate() This validates against the latest set of JSON schemas (which are included with the PySTAC package) or older versions (which are hosted at https://schemas.stacspec.org). This validation includes any extensions that the object extends (these are always accessed remotely based on their URIs). If there are validation errors, a :class:`~pystac.validation.STACValidationError` is raised. You can also call :meth:`~pystac.Catalog.validate_all` on a Catalog or Collection to recursively walk through a catalog and validate all objects within it. .. code-block:: python catalog.validate_all() Validating STAC JSON -------------------- You can validate STAC JSON represented as a ``dict`` using the :meth:`pystac.validation.validate_dict` method: .. code-block:: python import json from pystac.validation import validate_dict with open('/path/to/item.json') as f: js = json.load(f) validate_dict(js) You can also recursively validate all of the catalogs, collections and items across STAC versions using the :meth:`pystac.validation.validate_all` method: .. code-block:: python import json from pystac.validation import validate_all with open('/path/to/catalog.json') as f: js = json.load(f) validate_all(js) Using your own validator ------------------------ By default PySTAC uses the :class:`~pystac.validation.JsonSchemaSTACValidator` implementation for validation. Users can define their own implementations of :class:`~pystac.validation.STACValidator` and register it with pystac using :meth:`pystac.validation.set_validator`. The :class:`~pystac.validation.JsonSchemaSTACValidator` takes a :class:`~pystac.validation.SchemaUriMap`, which by default uses the :class:`~pystac.validation.schema_uri_map.DefaultSchemaUriMap`. If desirable, users cn create their own implementation of :class:`~pystac.validation.SchemaUriMap` and register a new instance of :class:`~pystac.validation.JsonSchemaSTACValidator` using that schema map with :meth:`pystac.validation.set_validator`. Extensions ========== From the documentation on :stac-spec:`STAC Spec Extensions `: Extensions to the core STAC specification provide additional JSON fields that can be used to better describe the data. Most tend to be about describing a particular domain or type of data, but some imply functionality. This library makes an effort to support all extensions that are part of the `stac-extensions GitHub org `__, and we are committed to supporting all STAC Extensions at the "Candidate" maturity level or above (see the `Extension Maturity `__ documentation for details). Accessing Extension Functionality --------------------------------- Extension functionality is encapsulated in classes that are specific to the STAC Extension (e.g. Electro-Optical, Projection, etc.) and STAC Object (:class:`~pystac.Collection`, :class:`pystac.Item`, or :class:`pystac.Asset`). All classes that extend these objects inherit from :class:`pystac.extensions.base.PropertiesExtension`, and you can use the ``ext`` accessor on the object to access the extension fields. For instance, if you have an item that implements the :stac-ext:`Electro-Optical Extension `, you can access the fields associated with that extension using :meth:`Item.ext `: .. code-block:: python import pystac item = pystac.Item.from_file("tests/data-files/eo/eo-landsat-example.json") # As long as the Item implements the EO Extension you can access all the # EO properties directly bands = item.ext.eo.bands cloud_cover = item.ext.eo.cloud_cover ... .. note:: ``ext`` will raise an :exc:`~pystac.ExtensionNotImplemented` exception if the object does not implement that extension (e.g. if the extension URI is not in that object's :attr:`~pystac.STACObject.stac_extensions` list). See the `Adding an Extension`_ section below for details on adding an extension to an object. If you don't want to raise an error you can use :meth:`~pystac.Item.ext.has` to first check if the extension is implemented on your pystac object: .. code-block:: python if item.ext.has("eo"): bands = item.ext.eo.bands See the documentation for each extension implementation for details on the supported properties and other functionality. Extensions have access to the properties of the object. *This attribute is a reference to the properties of the* :class:`~pystac.Collection`, :class:`~pystac.Item` *or* :class:`~pystac.Asset` *being extended and can therefore mutate those properties.* For instance: .. code-block:: python item = pystac.Item.from_file("tests/data-files/eo/eo-landsat-example.json") print(item.properties["eo:cloud_cover"]) # 78 print(item.ext.eo.cloud_cover) # 78 item.ext.eo.cloud_cover = 45 print(item.properties["eo:cloud_cover"]) # 45 There is also a :attr:`~pystac.extensions.base.PropertiesExtension.additional_read_properties` attribute that, if present, gives read-only access to properties of any objects that own the extended object. For instance, an extended :class:`pystac.Asset` instance would have read access to the properties of the :class:`pystac.Item` that owns it (if there is one). If a property exists in both additional_read_properties and properties, the value in additional_read_properties will take precedence. An ``apply`` method is available on extended objects. This allows you to pass in property values pertaining to the extension. Properties that are required by the extension will be required arguments to the ``apply`` method. Optional properties will have a default value of ``None``: .. code-block:: python # Can also omit cloud_cover entirely... item.ext.eo.apply(0.5, bands, cloud_cover=None) Adding an Extension ------------------- You can add an extension to a STAC object that does not already implement that extension using the :meth:`~pystac.Item.ext.add` method. Any concrete extension implementations that extend existing STAC objects should have this method available. The :meth:`~pystac.Item.ext.add` method adds the correct schema URI to the :attr:`~pystac.Item.stac_extensions` list for the object being extended. .. code-block:: python # Load a basic item without any extensions item = pystac.Item.from_file("tests/data-files/item/sample-item.json") print(item.stac_extensions) # [] # Add the Electro-Optical extension item.ext.add("eo") print(item.stac_extensions) # ['https://stac-extensions.github.io/eo/v1.1.0/schema.json'] Extended Summaries ------------------ Extension classes like :class:`~pystac.extensions.projection.ProjectionExtension` may also provide a ``summaries`` static method that can be used to extend the Collection summaries. This method returns a class inheriting from :class:`pystac.extensions.base.SummariesExtension` that provides tools for summarizing the properties defined by that extension. These classes also hold a reference to the Collection's :class:`pystac.Summaries` instance in the ``summaries`` attribute. .. code-block:: python import pystac from pystac.extensions.projection import ProjectionExtension # Load a collection that does not implement the Projection extension collection = pystac.Collection.from_file( "tests/data-files/examples/1.0.0/collection.json" ) # Add Projection extension summaries to the collection proj = ProjectionExtension.summaries(collection, add_if_missing=True) print(collection.stac_extensions) # [ # ...., # 'https://stac-extensions.github.io/projection/v1.1.0/schema.json' # ] # Set the values for various extension fields proj.epsg = [4326] collection_as_dict = collection.to_dict() collection_as_dict["summaries"]["proj:epsg"] # [4326] Item Asset properties ===================== Properties that apply to Items can be found in two places: the Item's properties or in any of an Item's Assets. If the property is on an Asset, it applies only to that specific asset. For example, gsd defined for an Item represents the best Ground Sample Distance (resolution) for the data within the Item. However, some assets may be lower resolution and thus have a higher gsd. In that case, the `gsd` can be found on the Asset. See the STAC documentation on :stac-spec:`Additional Fields for Assets ` and the relevant :stac-spec:`Best Practices ` for more information. The implementation of this feature in PySTAC uses the method described here and is consistent across Item and ItemExtensions. The bare property names represent values for the Item only, but for each property where it is possible to set on both the Item or the Asset there is a ``get_`` and ``set_`` methods that optionally take an Asset. For the ``get_`` methods, if the property is found on the Asset, the Asset's value is used; otherwise the Item's value will be used. For the ``set_`` method, if an Asset is passed in the value will be applied to the Asset and not the Item. For example, if we have an Item with a ``gsd`` of 10 with three bands, and only asset "band3" having a ``gsd`` of 20, the ``get_gsd`` method will behave in the following way: .. code-block:: python assert item.common_metadata.gsd == 10 assert item.common_metadata.get_gsd() == 10 assert item.common_metadata.get_gsd(item.asset['band1']) == 10 assert item.common_metadata.get_gsd(item.asset['band3']) == 20 Similarly, if we set the asset at 'band2' to have a ``gsd`` of 30, it will only affect that asset: .. code-block:: python item.common_metadata.set_gsd(30, item.assets['band2'] assert item.common_metadata.gsd == 10 assert item.common_metadata.get_gsd(item.asset['band2']) == 30 Manipulating STACs ================== PySTAC is designed to allow for STACs to be manipulated in-memory. This includes :ref:`copy stacs`, walking over all objects in a STAC and mutating their properties, or using collection-style `map` methods for mapping over items. Walking over a STAC ------------------- You can walk through all sub-catalogs and items of a catalog with a method inspired by the Python Standard Library `os.walk() `_ method: :func:`Catalog.walk() `: .. code-block:: python for root, subcats, items in catalog.walk(): # Root represents a catalog currently being walked in the tree root.title = '{} has been walked!'.format(root.id) # subcats represents any catalogs or collections owned by root for cat in subcats: cat.title = 'About to be walked!' # items represent all items that are contained by root for item in items: item.title = '{} - owned by {}'.format(item.id, root.id) Mapping over Items ------------------ The :func:`Catalog.map_items ` method is useful for into smaller chunks (e.g. tiling out large image items). item, you can return multiple items, in case you are generating new objects, or splitting items manipulating items in a STAC. This will create a full copy of the STAC, so will leave the original catalog unmodified. In the method that manipulates and returns the modified .. code-block:: python def modify_item_title(item): item.title = 'Some new title' return item def duplicate_item(item): duplicated_item = item.clone() duplicated_item.id += "-duplicated" return [item, duplicated_item] c = catalog.map_items(modify_item_title) c = c.map_items(duplicate_item) new_catalog = c .. _copy stacs: Copying STACs in-memory ----------------------- The in-memory copying of STACs to create new ones is crucial to correct manipulations and mutations of STAC data. The :func:`STACObject.full_copy ` mechanism handles this in a way that ties the elements of the copies STAC together correctly. This includes situations where there might be cycles in the graph of connected objects of the STAC (which otherwise would be `a tree `_). Resolving STAC objects ====================== PySTAC tries to only "resolve" STAC Objects - that is, load the metadata contained by STAC files pointed to by links into Python objects in-memory - when necessary. It also ensures that two links that point to the same object resolve to the same in-memory object. Lazy resolution of STAC objects ------------------------------- Links are read only when they need to be. For instance, when you load a catalog using :func:`Catalog.from_file `, the catalog and all of its links are read into a :class:`~pystac.Catalog` instance. If you iterate through :attr:`Catalog.links `, you'll see the :attr:`~pystac.Link.target` of the :class:`~pystac.Link` will refer to a string - that is the HREF of the link. However, if you call :func:`Catalog.get_items `, for instance, you'll get back the actual :class:`~pystac.Item` instances that are referred to by each item link in the Catalog. That's because at the time you call ``get_items``, PySTAC is "resolving" the links for any link that represents an item in the catalog. The resolution mechanism is accomplished through :func:`Link.resolve_stac_object `. Though this method is used extensively internally to PySTAC, ideally this is completely transparent to users of PySTAC, and you won't have to worry about how and when links get resolved. However, one important aspect to understand is how object resolution caching happens. Resolution Caching ------------------ The root :class:`~pystac.Catalog` instance of a STAC (the Catalog which is linked to by every associated object's ``root`` link) contains a cache of resolved objects. This cache points to in-memory instances of :class:`~pystac.STACObject` s that have already been resolved through PySTAC crawling links associated with that root catalog. The cache works off of the stac object's ID, which is why **it is necessary for every STAC object in the catalog to have a unique identifier, which is unique across the entire STAC**. When a link is being resolved from a STACObject that has it's root set, that root is passed into the :func:`Link.resolve_stac_object ` call. That root's :class:`~pystac.resolved_object_cache.ResolvedObjectCache` will be used to ensure that if the link is pointing to an object that has already been resolved, then that link will point to the same, single instance in the cache. This ensures working with STAC objects in memory doesn't create a situation where multiple copies of the same STAC objects are created from different links, manipulated, and written over each other. Working with STAC JSON ====================== The ``pystac.serialization`` package has some functionality around working directly with STAC JSON objects, without utilizing PySTAC object types. This is used internally by PySTAC, but might also be useful to users working directly with JSON (e.g. on validation). Identifying STAC objects from JSON ---------------------------------- Users can identify STAC information, including the object type, version and extensions, from JSON. The main method for this is :func:`~pystac.serialization.identify_stac_object`, which returns an object that contains the object type, the range of versions this object is valid for (according to PySTAC's best guess), the common extensions implemented by this object, and any custom extensions (represented by URIs to JSON Schemas). .. code-block:: python from pystac.serialization import identify_stac_object json_dict = ... info = identify_stac_object(json_dict) # The object type info.object_type # The version range info.version_range # The common extensions info.common_extensions # The custom Extensions info.custom_extensions Merging common properties ------------------------- For pre-1.0.0 STAC, The :func:`~pystac.serialization.merge_common_properties` will take a JSON dict that represents an item, and if it is associated with a collection, merge in the collection's properties. You can pass in a dict that contains previously read collections that caches collections by the HREF of the collection link and/or the collection ID, which can help avoid multiple reads of collection links. Note that this feature was dropped in STAC 1.0.0-beta.1 Geo interface ============= :class:`~pystac.Item` implements ``__geo_interface__``, a de-facto standard for describing geospatial objects in Python: https://gist.github.com/sgillies/2217756. Many packages can automatically use objects that implement this protocol, e.g. `shapely `_: .. code-block:: python >>> from pystac import Item >>> from shapely.geometry import mapping, shape >>> item = Item.from_file("data-files/item/sample-item.json") >>> print(shape(item)) POLYGON ((-122.308150179 37.488035566, -122.597502109 37.538869539, -122.576687533 37.613537207, -122.2880486 37.562818007, -122.308150179 37.488035566)) pystac-1.9.0/docs/conf.py000066400000000000000000000162121451576074700152750ustar00rootroot00000000000000# # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import subprocess import sys from typing import Any sys.path.insert(0, os.path.abspath(".")) sys.path.insert(0, os.path.abspath("../")) from pystac.version import STACVersion, __version__ # noqa:E402 git_branch = ( subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]) .decode("utf-8") .strip() ) # -- Project information ----------------------------------------------------- project = "pystac" copyright = "2019, Azavea" author = "stac-utils" # The short X.Y version version = __version__ # The full version, including alpha/beta/rc tags release = __version__ # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx_design", "sphinx.ext.autodoc", "sphinx.ext.autosummary", "sphinx.ext.viewcode", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", "sphinx.ext.githubpages", "sphinx.ext.extlinks", "nbsphinx", ] extlinks = { "tutorial": ( "https://github.com/stac-utils/pystac/" "tree/{}/docs/tutorials/%s".format(git_branch), "%s tutorial", ), "stac-spec": ( "https://github.com/radiantearth/stac-spec/tree/" "v{}/%s".format(STACVersion.DEFAULT_STAC_VERSION), "%s path", ), "stac-ext": ("https://github.com/stac-extensions/%s", "%s extension"), } # Add any paths that contain templates here, relative to this directory. # templates_path = ["_templates"] # Static CSS files # html_css_files = ["custom.css"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The master toctree document. master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**.ipynb_checkpoints"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "pydata_sphinx_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = { "icon_links": [ { "name": "GitHub", "url": "https://github.com/stac-utils/pystac", "icon": "fab fa-github-square", }, { "name": "Gitter", "url": "https://gitter.im/SpatioTemporal-Asset-Catalog/" "python?utm_source=share-link&utm_medium=link&utm_campaign=share-link", "icon": "fab fa-gitter", }, ], "external_links": [ {"name": "STAC Spec", "url": "https://github.com/radiantearth/stac-spec"} ], "header_links_before_dropdown": 7, # "navbar_end": ["navbar-icon-links.html", "search-field.html"] } html_logo = "_static/STAC-03.png" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # html_sidebars: dict[str, list[str]] = {"index": []} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = "pystacdoc" # -- Options for LaTeX output ------------------------------------------------ latex_elements: dict[str, Any] = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, "pystac.tex", "pystac Documentation", "stac-utils", "manual"), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, "pystac", "pystac Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "pystac", "pystac Documentation", author, "pystac", "Python library for SpatioTemporal Asset Catalogs (STAC).", "Miscellaneous", ), ] # -- Options for Epub output ------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ["search.html"] # -- Extension configuration ------------------------------------------------- intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "dateutil": ("https://dateutil.readthedocs.io/en/stable", None), "urllib3": ("https://urllib3.readthedocs.io/en/stable", None), } # -- Substutition variables rst_epilog = f".. |stac_version| replace:: {STACVersion.DEFAULT_STAC_VERSION}" pystac-1.9.0/docs/contributing.rst000066400000000000000000000151151451576074700172400ustar00rootroot00000000000000Contributing ============ A list of issues and ongoing work is available on the PySTAC `issues page `_. If you want to contribute code, the best way is to coordinate with the core developers via an issue or pull request conversation. Development installation ^^^^^^^^^^^^^^^^^^^^^^^^ Fork PySTAC into your GitHub account. Then, clone the repo and install it locally with pip as follows: .. code-block:: bash git clone git@github.com:your_user_name/pystac.git cd pystac pip install -e '.[test]' Testing ^^^^^^^ tl;dr: Run ``./scripts/test`` to run all tests as they run on CI. PySTAC runs tests using `pytest `_. You can find unit tests in the ``tests/`` directory. To run the tests and generate the coverage report: .. code-block:: bash $ pytest -v -s --block-network --cov pystac --cov-report term-missing To view the coverage report, you can run `coverage report` (to view the report in the terminal) or `coverage html` (to generate an HTML report that can be opened in a browser). The PySTAC tests use `vcrpy `_ to mock API calls with "pre-recorded" API responses. This often comes up when testing validation. When adding new tests that require pulling remote files use the ``@pytest.mark.vcr`` decorator. Record the new responses and commit them to the repository. .. code-block:: bash $ pytest -v -s --record-mode new_episodes $ git add $ git commit -a -m 'new test episodes' Code quality checks ^^^^^^^^^^^^^^^^^^^ tl;dr: Run ``pre-commit install --overwrite`` to perform checks when committing, and ``./scripts/test`` to run all checks and tests. PySTAC uses - `ruff `_ for Python code linting - `black `_ for Python code formatting - `codespell `_ to check code for common misspellings - `doc8 `__ for style checking on RST files in the docs - `mypy `_ for Python type annotation checks Run all of these with ``pre-commit run --all-files`` or a single one using ``pre-commit run --all-files ID``, where ``ID`` is one of the command names above. For example, to format all the Python code, run ``pre-commit run --all-files black``. You can also install a Git pre-commit hook which will run the relevant linters and formatters on any staged code when committing. This will be much faster than running on all files, which is usually [#]_ only required when changing the pre-commit version or configuration. Once installed you can bypass this check by adding the ``--no-verify`` flag to Git commit commands, as in ``git commit --no-verify``. .. [#] In rare cases changes to one file might invalidate an unchanged file, such as when modifying the return type of a function used in another file. Documentation ^^^^^^^^^^^^^ All new features or changes should include API documentation, in the form of docstrings. Additionally, if you are updating an extension version, check to see if that extension is used in the ``examples/`` STAC objects at the top level of the repository. If so, update the extension version, then re-run ``docs/quickstart.ipynb`` to include the new extension versions in the notebook cell output. Benchmarks ^^^^^^^^^^ PySTAC uses `asv `_ for benchmarking. Benchmarks are defined in the ``./benchmarks`` directory. Due to the inherent uncertainty in the environment of Github workflow runners, benchmarks are not executed in CI. If your changes may affect performance, use the provided script to run the benchmark suite locally. You'll need to install the benchmark dependencies first. This script will compare your current ``HEAD`` with the **main** branch and report any improvements or regressions. .. code-block:: bash pip install -e '.[bench]' scripts/bench The benchmark suite takes a while to run, and will report any significant changes to standard output. For example, here's a benchmark comparison between v1.0.0 and v1.6.1 (from `@gadomski's `_ computer):: before after ratio [eee06027] [579c071b] - 533±20μs 416±10μs 0.78 collection.CollectionBench.time_collection_from_file [gadomski/virtualenv-py3.10-orjson] - 329±8μs 235±10μs 0.72 collection.CollectionBench.time_collection_from_dict [gadomski/virtualenv-py3.10-orjson] - 332±10μs 231±4μs 0.70 collection.CollectionBench.time_collection_from_dict [gadomski/virtualenv-py3.10] - 174±4μs 106±2μs 0.61 item.ItemBench.time_item_from_dict [gadomski/virtualenv-py3.10] - 174±4μs 106±2μs 0.61 item.ItemBench.time_item_from_dict [gadomski/virtualenv-py3.10-orjson] before after ratio [eee06027] [579c071b] + 87.1±3μs 124±5μs 1.42 catalog.CatalogBench.time_catalog_from_dict [gadomski/virtualenv-py3.10] + 87.1±4μs 122±5μs 1.40 catalog.CatalogBench.time_catalog_from_dict [gadomski/virtualenv-py3.10-orjson] When developing new benchmarks, you can run a shortened version of the benchmark suite: .. code-block:: bash asv dev CHANGELOG ^^^^^^^^^ PySTAC maintains a `changelog `_ to track changes between releases. All PRs should make a changelog entry unless the change is trivial (e.g. fixing typos) or is entirely invisible to users who may be upgrading versions (e.g. an improvement to the CI system). For changelog entries, please link to the PR of that change. This needs to happen in a few steps: - Make a PR to PySTAC with your changes - Record the link to the PR - Push an additional commit to your branch with the changelog entry with the link to the PR. For more information on changelogs and how to write a good entry, see `keep a changelog `_. Style ^^^^^ In an effort to maintain a consistent codebase, PySTAC conforms to the following rules: .. code-block:: python # DO from datetime import datetime # DON't import datetime import datetime as dt The exception to this rule is when ``datetime`` is only imported for type checking and using the class directly interferes with another variable name. In this case, in the TYPE_CHECKING block you should do ``from datetime import datetime as Datetime``. pystac-1.9.0/docs/example-catalog/000077500000000000000000000000001451576074700170375ustar00rootroot00000000000000pystac-1.9.0/docs/example-catalog/catalog.json000066400000000000000000000006511451576074700213460ustar00rootroot00000000000000{ "type": "Catalog", "stac_version": "1.0.0", "stac_extensions": [], "id": "landsat-stac-collection-catalog", "title": "STAC for Landsat data", "description": "STAC for Landsat data", "links": [ { "href": "./catalog.json", "rel": "self" }, { "href": "./catalog.json", "rel": "root" }, { "href": "./landsat-8-l1/collection.json", "rel": "child" } ] }pystac-1.9.0/docs/example-catalog/landsat-8-l1/000077500000000000000000000000001451576074700211445ustar00rootroot00000000000000pystac-1.9.0/docs/example-catalog/landsat-8-l1/2018-05/000077500000000000000000000000001451576074700217605ustar00rootroot00000000000000pystac-1.9.0/docs/example-catalog/landsat-8-l1/2018-05/LC80150322018141LGN00.json000066400000000000000000000200571451576074700251620ustar00rootroot00000000000000{ "type": "Feature", "id": "LC80150322018141LGN00", "stac_version" : "1.0.0", "stac_extensions" : [ "https://stac-extensions.github.io/eo/v1.1.0/schema.json", "https://stac-extensions.github.io/view/v1.0.0/schema.json", "https://stac-extensions.github.io/projection/v1.1.0/schema.json" ], "bbox": [ -77.88298, 39.23073, -75.07535, 41.41022 ], "geometry": { "type": "Polygon", "coordinates": [ [ [ -77.28911976020206, 41.40912394323429 ], [ -75.07576783500748, 40.97162247589133 ], [ -75.66872631473827, 39.23210949585851 ], [ -77.87946700654118, 39.67679918442899 ], [ -77.28911976020206, 41.40912394323429 ] ] ] }, "collection": "landsat-8-l1", "properties": { "collection": "landsat-8-l1", "datetime": "2018-05-21T15:44:59Z", "view:sun_azimuth": 134.8082647, "view:sun_elevation": 64.00406717, "eo:cloud_cover": 4, "instruments": ["OLI_TIRS"], "view:off_nadir": 0, "platform": "landsat-8", "gsd": 30, "proj:epsg": 32618, "proj:transform": [258885.0, 30.0, 0.0, 4584315.0, 0.0, -30.0], "proj:geometry": { "type": "Polygon", "coordinates": [ [ [258885.0, 4346085.0], [258885.0, 4584315.0], [493515.0, 4584315.0], [493515.0, 4346085.0], [258885.0, 4346085.0] ] ] }, "proj:shape": [7821, 7941] }, "assets": { "index": { "type": "text/html", "title": "HTML index page", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/index.html", "roles": [] }, "thumbnail": { "title": "Thumbnail image", "type": "image/jpeg", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_thumb_large.jpg", "roles" : [ "thumbnail" ] }, "B1": { "type": "image/tiff", "title": "Band 1 (coastal)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B1.TIF", "roles": [], "eo:bands": [ { "name" : "B1", "full_width_half_max" : 0.02, "center_wavelength" : 0.44, "common_name" : "coastal" } ] }, "B2": { "type": "image/tiff", "title": "Band 2 (blue)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B2.TIF", "roles": [], "eo:bands": [ { "name" : "B2", "full_width_half_max" : 0.06, "center_wavelength" : 0.48, "common_name" : "blue" } ] }, "B3": { "type": "image/tiff", "title": "Band 3 (green)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B3.TIF", "roles": [], "eo:bands": [ { "name" : "B3", "full_width_half_max" : 0.06, "center_wavelength" : 0.56, "common_name" : "green" } ] }, "B4": { "type": "image/tiff", "title": "Band 4 (red)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B4.TIF", "roles": [], "eo:bands": [ { "name" : "B4", "full_width_half_max" : 0.04, "center_wavelength" : 0.65, "common_name" : "red" } ] }, "B5": { "type": "image/tiff", "title": "Band 5 (nir)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B5.TIF", "roles": [], "eo:bands": [ { "name" : "B5", "full_width_half_max" : 0.03, "center_wavelength" : 0.86, "common_name" : "nir" } ] }, "B6": { "type": "image/tiff", "title": "Band 6 (swir16)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B6.TIF", "roles": [], "eo:bands": [ { "name" : "B6", "full_width_half_max" : 0.08, "center_wavelength" : 1.6, "common_name" : "swir16" } ] }, "B7": { "type": "image/tiff", "title": "Band 7 (swir22)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B7.TIF", "roles": [], "eo:bands": [ { "name" : "B7", "full_width_half_max" : 0.22, "center_wavelength" : 2.2, "common_name" : "swir22" } ] }, "B8": { "type": "image/tiff", "title": "Band 8 (pan)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B8.TIF", "roles": [], "eo:bands": [ { "name" : "B8", "full_width_half_max" : 0.18, "center_wavelength" : 0.59, "common_name" : "pan" } ] }, "B9": { "type": "image/tiff", "title": "Band 9 (cirrus)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B9.TIF", "roles": [], "eo:bands": [ { "name" : "B9", "full_width_half_max" : 0.02, "center_wavelength" : 1.37, "common_name" : "cirrus" } ] }, "B10": { "type": "image/tiff", "title": "Band 10 (lwir)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B10.TIF", "roles": [], "eo:bands": [ { "name" : "B10", "full_width_half_max" : 0.8, "center_wavelength" : 10.9, "common_name" : "lwir11" } ] }, "B11": { "type": "image/tiff", "title": "Band 11 (lwir)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B11.TIF", "roles": [], "eo:bands": [ { "name" : "B11", "full_width_half_max" : 1, "center_wavelength" : 12, "common_name" : "lwir2" } ] }, "ANG": { "title": "Angle coefficients file", "type": "text/plain", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_ANG.txt", "roles": [] }, "MTL": { "title": "original metadata file", "type": "text/plain", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_MTL.txt", "roles": [] }, "BQA": { "title": "Band quality data", "type": "image/tiff", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_BQA.TIF", "roles": [] } }, "links": [ { "rel": "self", "href": "./LC80150322018141LGN00.json" }, { "rel": "parent", "href": "../collection.json" }, { "rel": "collection", "href": "../collection.json" }, { "rel": "root", "href": "../../catalog.json" } ] } pystac-1.9.0/docs/example-catalog/landsat-8-l1/2018-06/000077500000000000000000000000001451576074700217615ustar00rootroot00000000000000pystac-1.9.0/docs/example-catalog/landsat-8-l1/2018-06/LC80140332018166LGN00.json000066400000000000000000000200631451576074700251670ustar00rootroot00000000000000{ "type": "Feature", "id": "LC80140332018166LGN00", "stac_version" : "1.0.0", "stac_extensions" : [ "https://stac-extensions.github.io/eo/v1.1.0/schema.json", "https://stac-extensions.github.io/view/v1.0.0/schema.json", "https://stac-extensions.github.io/projection/v1.1.0/schema.json" ], "bbox": [ -76.66703, 37.82561, -73.94861, 39.95958 ], "geometry": { "type": "Polygon", "coordinates": [ [ [ -76.12180471942207, 39.95810181489563 ], [ -73.94910518227414, 39.55117185146004 ], [ -74.49564725552679, 37.826064511480496 ], [ -76.66550404911956, 38.240699151776084 ], [ -76.12180471942207, 39.95810181489563 ] ] ] }, "collection": "landsat-8-l1", "properties": { "collection": "landsat-8-l1", "datetime": "2018-06-15T15:39:09Z", "view:sun_azimuth": 125.59055137, "view:sun_elevation": 66.54485226, "eo:cloud_cover": 22, "instruments": ["OLI_TIRS"], "view:off_nadir": 0, "platform": "landsat-8", "gsd": 30, "proj:epsg": 32618, "proj:transform": [357585.0, 30.0, 0.0, 4423815.0, 0.0, -30.0], "proj:geometry": { "type": "Polygon", "coordinates": [ [ [357585.0, 4187685.0], [357585.0, 4423815.0], [589815.0, 4423815.0], [589815.0, 4187685.0], [357585.0, 4187685.0] ] ] }, "proj:shape": [7741, 7871] }, "assets": { "index": { "type": "text/html", "title": "HTML index page", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/index.html", "roles": [] }, "thumbnail": { "title": "Thumbnail image", "type": "image/jpeg", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_thumb_large.jpg", "roles" : [ "thumbnail" ] }, "B1": { "type": "image/tiff", "title": "Band 1 (coastal)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B1.TIF", "roles": [], "eo:bands": [ { "name" : "B1", "full_width_half_max" : 0.02, "center_wavelength" : 0.44, "common_name" : "coastal" } ] }, "B2": { "type": "image/tiff", "title": "Band 2 (blue)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B2.TIF", "roles": [], "eo:bands": [ { "name" : "B2", "full_width_half_max" : 0.06, "center_wavelength" : 0.48, "common_name" : "blue" } ] }, "B3": { "type": "image/tiff", "title": "Band 3 (green)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B3.TIF", "roles": [], "eo:bands": [ { "name" : "B3", "full_width_half_max" : 0.06, "center_wavelength" : 0.56, "common_name" : "green" } ] }, "B4": { "type": "image/tiff", "title": "Band 4 (red)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B4.TIF", "roles": [], "eo:bands": [ { "name" : "B4", "full_width_half_max" : 0.04, "center_wavelength" : 0.65, "common_name" : "red" } ] }, "B5": { "type": "image/tiff", "title": "Band 5 (nir)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B5.TIF", "roles": [], "eo:bands": [ { "name" : "B5", "full_width_half_max" : 0.03, "center_wavelength" : 0.86, "common_name" : "nir" } ] }, "B6": { "type": "image/tiff", "title": "Band 6 (swir16)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B6.TIF", "roles": [], "eo:bands": [ { "name" : "B6", "full_width_half_max" : 0.08, "center_wavelength" : 1.6, "common_name" : "swir16" } ] }, "B7": { "type": "image/tiff", "title": "Band 7 (swir22)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B7.TIF", "roles": [], "eo:bands": [ { "name" : "B7", "full_width_half_max" : 0.22, "center_wavelength" : 2.2, "common_name" : "swir22" } ] }, "B8": { "type": "image/tiff", "title": "Band 8 (pan)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B8.TIF", "roles": [], "eo:bands": [ { "name" : "B8", "full_width_half_max" : 0.18, "center_wavelength" : 0.59, "common_name" : "pan" } ] }, "B9": { "type": "image/tiff", "title": "Band 9 (cirrus)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B9.TIF", "roles": [], "eo:bands": [ { "name" : "B9", "full_width_half_max" : 0.02, "center_wavelength" : 1.37, "common_name" : "cirrus" } ] }, "B10": { "type": "image/tiff", "title": "Band 10 (lwir)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B10.TIF", "roles": [], "eo:bands": [ { "name" : "B10", "full_width_half_max" : 0.8, "center_wavelength" : 10.9, "common_name" : "lwir11" } ] }, "B11": { "type": "image/tiff", "title": "Band 11 (lwir)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B11.TIF", "roles": [], "eo:bands": [ { "name" : "B11", "full_width_half_max" : 1, "center_wavelength" : 12, "common_name" : "lwir2" } ] }, "ANG": { "title": "Angle coefficients file", "type": "text/plain", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_ANG.txt", "roles": [] }, "MTL": { "title": "original metadata file", "type": "text/plain", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_MTL.txt", "roles": [] }, "BQA": { "title": "Band quality data", "type": "image/tiff", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_BQA.TIF", "roles": [] } }, "links": [ { "rel": "self", "href": "./LC80140332018166LGN00.json" }, { "rel": "parent", "href": "../collection.json" }, { "rel": "collection", "href": "../collection.json" }, { "rel": "root", "href": "../../catalog.json" } ] } pystac-1.9.0/docs/example-catalog/landsat-8-l1/2018-06/LC80300332018166LGN00.json000066400000000000000000000200611451576074700251630ustar00rootroot00000000000000{ "type": "Feature", "stac_version" : "1.0.0", "stac_extensions" : [ "https://stac-extensions.github.io/eo/v1.1.0/schema.json", "https://stac-extensions.github.io/view/v1.0.0/schema.json", "https://stac-extensions.github.io/projection/v1.1.0/schema.json" ], "id": "LC80300332018166LGN00", "bbox": [ -101.40793, 37.81084, -98.6721, 39.97469 ], "geometry": { "type": "Polygon", "coordinates": [ [ [ -100.84368079413701, 39.97210491033466 ], [ -98.67492641719046, 39.54833037653145 ], [ -99.23946071016417, 37.81370881408165 ], [ -101.40560438472555, 38.24476872678675 ], [ -100.84368079413701, 39.97210491033466 ] ] ] }, "collection": "landsat-8-l1", "properties": { "collection": "landsat-8-l1", "datetime": "2018-06-15T17:18:03Z", "view:sun_azimuth": 125.5799919, "view:sun_elevation": 66.54407242, "eo:cloud_cover": 0, "instruments": ["OLI_TIRS"], "view:off_nadir": 0, "platform": "landsat-8", "gsd": 30, "proj:epsg": 32614, "proj:transform": [294285.0, 30.0, 0.0, 4425015.0, 0.0, -30], "proj:geometry": { "type": "Polygon", "coordinates": [ [ [294285.0, 4187385.0], [294285.0, 4425015.0], [528015.0, 4425015.0], [528015.0, 4187385.0], [294285.0, 4187385.0] ] ] }, "proj:shape": [7791, 7921] }, "assets": { "index": { "type": "text/html", "title": "HTML index page", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/index.html", "roles" : [] }, "thumbnail": { "title": "Thumbnail image", "type": "image/jpeg", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_thumb_large.jpg", "roles" : [ "thumbnail" ] }, "B1": { "type": "image/tiff", "title": "Band 1 (coastal)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B1.TIF", "roles": [], "eo:bands": [ { "name" : "B1", "full_width_half_max" : 0.02, "center_wavelength" : 0.44, "common_name" : "coastal" } ] }, "B2": { "type": "image/tiff", "title": "Band 2 (blue)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B2.TIF", "roles": [], "eo:bands": [ { "name" : "B2", "full_width_half_max" : 0.06, "center_wavelength" : 0.48, "common_name" : "blue" } ] }, "B3": { "type": "image/tiff", "title": "Band 3 (green)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B3.TIF", "roles": [], "eo:bands": [ { "name" : "B3", "full_width_half_max" : 0.06, "center_wavelength" : 0.56, "common_name" : "green" } ] }, "B4": { "type": "image/tiff", "title": "Band 4 (red)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B4.TIF", "roles": [], "eo:bands": [ { "name" : "B4", "full_width_half_max" : 0.04, "center_wavelength" : 0.65, "common_name" : "red" } ] }, "B5": { "type": "image/tiff", "title": "Band 5 (nir)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B5.TIF", "roles": [], "eo:bands": [ { "name" : "B5", "full_width_half_max" : 0.03, "center_wavelength" : 0.86, "common_name" : "nir" } ] }, "B6": { "type": "image/tiff", "title": "Band 6 (swir16)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B6.TIF", "roles": [], "eo:bands": [ { "name" : "B6", "full_width_half_max" : 0.08, "center_wavelength" : 1.6, "common_name" : "swir16" } ] }, "B7": { "type": "image/tiff", "title": "Band 7 (swir22)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B7.TIF", "roles": [], "eo:bands": [ { "name" : "B7", "full_width_half_max" : 0.22, "center_wavelength" : 2.2, "common_name" : "swir22" } ] }, "B8": { "type": "image/tiff", "title": "Band 8 (pan)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B8.TIF", "roles": [], "eo:bands": [ { "name" : "B8", "full_width_half_max" : 0.18, "center_wavelength" : 0.59, "common_name" : "pan" } ] }, "B9": { "type": "image/tiff", "title": "Band 9 (cirrus)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B9.TIF", "roles": [], "eo:bands": [ { "name" : "B9", "full_width_half_max" : 0.02, "center_wavelength" : 1.37, "common_name" : "cirrus" } ] }, "B10": { "type": "image/tiff", "title": "Band 10 (lwir)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B10.TIF", "roles": [], "eo:bands": [ { "name" : "B10", "full_width_half_max" : 0.8, "center_wavelength" : 10.9, "common_name" : "lwir11" } ] }, "B11": { "type": "image/tiff", "title": "Band 11 (lwir)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_B11.TIF", "roles": [], "eo:bands": [ { "name" : "B11", "full_width_half_max" : 1, "center_wavelength" : 12, "common_name" : "lwir2" } ] }, "ANG": { "title": "Angle coefficients file", "type": "text/plain", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_ANG.txt", "roles": [] }, "MTL": { "title": "original metadata file", "type": "text/plain", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_MTL.txt", "roles": [] }, "BQA": { "title": "Band quality data", "type": "image/tiff", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/030/033/LC08_L1TP_030033_20180615_20180703_01_T1/LC08_L1TP_030033_20180615_20180703_01_T1_BQA.TIF", "roles": [] } }, "links": [ { "rel": "self", "href": "./LC80300332018166LGN00.json" }, { "rel": "parent", "href": "../collection.json" }, { "rel": "collection", "href": "../collection.json" }, { "rel": "root", "href": "../../catalog.json" } ] } pystac-1.9.0/docs/example-catalog/landsat-8-l1/2018-07/000077500000000000000000000000001451576074700217625ustar00rootroot00000000000000pystac-1.9.0/docs/example-catalog/landsat-8-l1/2018-07/LC80150332018189LGN00.json000066400000000000000000000200501451576074700251720ustar00rootroot00000000000000{ "type": "Feature", "id": "LC80150332018189LGN00", "stac_version" : "1.0.0", "stac_extensions" : [ "https://stac-extensions.github.io/eo/v1.1.0/schema.json", "https://stac-extensions.github.io/view/v1.0.0/schema.json", "https://stac-extensions.github.io/projection/v1.1.0/schema.json" ], "bbox": [ -78.25028, 37.79719, -75.48983, 39.98757 ], "geometry": { "type": "Polygon", "coordinates": [ [ [ -77.66532657556414, 39.987421383364385 ], [ -75.49021499188945, 39.54442448711656 ], [ -76.07747288135147, 37.799167045362736 ], [ -78.25025639728777, 38.24897728816149 ], [ -77.66532657556414, 39.987421383364385 ] ] ] }, "collection": "landsat-8-l1", "properties": { "collection": "landsat-8-l1", "datetime": "2018-07-08T15:45:34Z", "view:sun_azimuth": 125.31095515, "view:sun_elevation": 65.2014335, "eo:cloud_cover": 0, "instruments": ["OLI_TIRS"], "view:off_nadir": 0, "platform": "landsat-8", "gsd": 30, "proj:epsg": 32618, "proj:transform": [222285.0, 30.0, 0.0, 4426515.0, 0.0, -30.0], "proj:geometry": { "type": "Polygon", "coordinates": [ [ [222285.0, 4187985.0], [222285.0, 4426515.0], [456915.0, 4426515.0], [456915.0, 4187985.0], [222285.0,4187985.0]] ] }, "proj:shape": [7821, 7951] }, "assets": { "index": { "type": "text/html", "title": "HTML index page", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/index.html", "roles": [] }, "thumbnail": { "title": "Thumbnail image", "type": "image/jpeg", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_thumb_large.jpg", "roles" : [ "thumbnail" ] }, "B1": { "type": "image/tiff", "title": "Band 1 (coastal)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B1.TIF", "roles": [], "eo:bands": [ { "name" : "B1", "full_width_half_max" : 0.02, "center_wavelength" : 0.44, "common_name" : "coastal" } ] }, "B2": { "type": "image/tiff", "title": "Band 2 (blue)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B2.TIF", "roles": [], "eo:bands": [ { "name" : "B2", "full_width_half_max" : 0.06, "center_wavelength" : 0.48, "common_name" : "blue" } ] }, "B3": { "type": "image/tiff", "title": "Band 3 (green)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B3.TIF", "roles": [], "eo:bands": [ { "name" : "B3", "full_width_half_max" : 0.06, "center_wavelength" : 0.56, "common_name" : "green" } ] }, "B4": { "type": "image/tiff", "title": "Band 4 (red)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B4.TIF", "roles": [], "eo:bands": [ { "name" : "B4", "full_width_half_max" : 0.04, "center_wavelength" : 0.65, "common_name" : "red" } ] }, "B5": { "type": "image/tiff", "title": "Band 5 (nir)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B5.TIF", "roles": [], "eo:bands": [ { "name" : "B5", "full_width_half_max" : 0.03, "center_wavelength" : 0.86, "common_name" : "nir" } ] }, "B6": { "type": "image/tiff", "title": "Band 6 (swir16)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B6.TIF", "roles": [], "eo:bands": [ { "name" : "B6", "full_width_half_max" : 0.08, "center_wavelength" : 1.6, "common_name" : "swir16" } ] }, "B7": { "type": "image/tiff", "title": "Band 7 (swir22)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B7.TIF", "roles": [], "eo:bands": [ { "name" : "B7", "full_width_half_max" : 0.22, "center_wavelength" : 2.2, "common_name" : "swir22" } ] }, "B8": { "type": "image/tiff", "title": "Band 8 (pan)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B8.TIF", "roles": [], "eo:bands": [ { "name" : "B8", "full_width_half_max" : 0.18, "center_wavelength" : 0.59, "common_name" : "pan" } ] }, "B9": { "type": "image/tiff", "title": "Band 9 (cirrus)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B9.TIF", "roles": [], "eo:bands": [ { "name" : "B9", "full_width_half_max" : 0.02, "center_wavelength" : 1.37, "common_name" : "cirrus" } ] }, "B10": { "type": "image/tiff", "title": "Band 10 (lwir)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B10.TIF", "roles": [], "eo:bands": [ { "name" : "B10", "full_width_half_max" : 0.8, "center_wavelength" : 10.9, "common_name" : "lwir11" } ] }, "B11": { "type": "image/tiff", "title": "Band 11 (lwir)", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_B11.TIF", "roles": [], "eo:bands": [ { "name" : "B11", "full_width_half_max" : 1, "center_wavelength" : 12, "common_name" : "lwir2" } ] }, "ANG": { "title": "Angle coefficients file", "type": "text/plain", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_ANG.txt", "roles": [] }, "MTL": { "title": "original metadata file", "type": "text/plain", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_MTL.txt", "roles": [] }, "BQA": { "title": "Band quality data", "type": "image/tiff", "href": "https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/033/LC08_L1TP_015033_20180708_20180717_01_T1/LC08_L1TP_015033_20180708_20180717_01_T1_BQA.TIF", "roles": [] } }, "links": [ { "rel": "self", "href": "./LC80150332018189LGN00.json" }, { "rel": "parent", "href": "../collection.json" }, { "rel": "collection", "href": "../collection.json" }, { "rel": "root", "href": "../../catalog.json" } ] } pystac-1.9.0/docs/example-catalog/landsat-8-l1/collection.json000066400000000000000000000066041451576074700242000ustar00rootroot00000000000000{ "type": "Collection", "stac_version" : "1.0.0", "stac_extensions" : [ "eo", "view", "https://example.com/stac/landsat-extension/1.0/schema.json" ], "id" : "landsat-8-l1", "title" : "Landsat 8 L1", "description" : "Landsat 8 imagery radiometrically calibrated and orthorectified using ground points and Digital Elevation Model (DEM) data to correct relief displacement.", "keywords" : [ "landsat", "earth observation", "usgs" ], "license" : "proprietary", "providers" : [ { "name" : "Development Seed", "roles" : [ "processor" ], "url" : "https://github.com/sat-utils/sat-api" } ], "extent" : { "spatial" : { "bbox" : [ [ -180.0, -90.0, 180.0, 90.0 ] ] }, "temporal" : { "interval" : [ [ "2018-05-21T15:44:59Z", "2018-07-08T15:45:34Z" ] ] } }, "summaries": {}, "properties" : { "collection" : "landsat-8-l1", "instruments" : ["OLI_TIRS"], "view:sun_azimuth" : 149.01607154, "eo:bands" : [ { "name" : "B1", "full_width_half_max" : 0.02, "center_wavelength" : 0.44, "common_name" : "coastal" }, { "name" : "B2", "full_width_half_max" : 0.06, "center_wavelength" : 0.48, "common_name" : "blue" }, { "name" : "B3", "full_width_half_max" : 0.06, "center_wavelength" : 0.56, "common_name" : "green" }, { "name" : "B4", "full_width_half_max" : 0.04, "center_wavelength" : 0.65, "common_name" : "red" }, { "name" : "B5", "full_width_half_max" : 0.03, "center_wavelength" : 0.86, "common_name" : "nir" }, { "name" : "B6", "full_width_half_max" : 0.08, "center_wavelength" : 1.6, "common_name" : "swir16" }, { "name" : "B7", "full_width_half_max" : 0.22, "center_wavelength" : 2.2, "common_name" : "swir22" }, { "name" : "B8", "full_width_half_max" : 0.18, "center_wavelength" : 0.59, "common_name" : "pan" }, { "name" : "B9", "full_width_half_max" : 0.02, "center_wavelength" : 1.37, "common_name" : "cirrus" }, { "name" : "B10", "full_width_half_max" : 0.8, "center_wavelength" : 10.9, "common_name" : "lwir11" }, { "name" : "B11", "full_width_half_max" : 1, "center_wavelength" : 12, "common_name" : "lwir2" } ], "view:off_nadir" : 0, "view:azimuth" : 0, "platform" : "landsat-8", "gsd" : 15, "view:sun_elevation" : 59.214247 }, "links" : [ { "href" : "../catalog.json", "rel" : "root" }, { "href" : "../catalog.json", "rel" : "parent" }, { "href" : "./collection.json", "rel" : "self" }, { "href" : "./2018-06/LC80140332018166LGN00.json", "rel" : "item" }, { "href" : "./2018-05/LC80150322018141LGN00.json", "rel" : "item" }, { "href" : "./2018-07/LC80150332018189LGN00.json", "rel" : "item" }, { "href" : "./2018-06/LC80300332018166LGN00.json", "rel" : "item" } ] } pystac-1.9.0/docs/index.rst000066400000000000000000000045361451576074700156450ustar00rootroot00000000000000PySTAC Documentation #################### PySTAC is a library for working with `SpatioTemporal Asset Catalogs (STAC) `_ in `Python 3 `_. Some nice features of PySTAC are: * Reading and writing STAC version 1.0. Future versions will read older versions of STAC, but always write the latest supported version. See :ref:`stac_version_support` for details. * In-memory manipulations of STAC catalogs. * Extend the I/O of STAC metadata to provide support for other platforms (e.g. cloud providers). * Easy, efficient crawling of STAC catalogs. STAC objects are only read in when needed. * Easily write "absolute published", "relative published" and "self-contained" catalogs as :stac-spec:`described in the best practices documentation `. .. raw:: html .. grid:: 1 2 2 2 :gutter: 2 .. grid-item-card:: Get Started * :doc:`installation`: Instructions for installing the basic package as well as extras. * :doc:`quickstart`: Jupyter notebook tutorial on using PySTAC for reading & writing STAC catalogs. .. grid-item-card:: Go Deeper * :doc:`concepts`: Overview of how various concepts and structures from the STAC Specification are implemented within PySTAC. * :doc:`tutorials`: In-depth tutorials on using PySTAC for a number of different applications. * :doc:`api`: Detailed API documentation of PySTAC classes, methods, and functions. Related Projects ================ * `pystac-client `__: A Python client for working with STAC Catalogs and APIs. * `stactools `__: A command line tool and library for working with STAC. * `sat-stac `__: A Python 3 library for reading and working with existing Spatio-Temporal Asset Catalogs (STAC). *Much of PySTAC builds on the code and concepts of* ``sat-stac``. .. toctree:: :maxdepth: 2 :hidden: installation quickstart concepts api tutorials contributing pystac-1.9.0/docs/installation.rst000066400000000000000000000056231451576074700172350ustar00rootroot00000000000000Installation ############ Install from PyPi (recommended) =============================== .. code-block:: bash pip install pystac Install from conda-forge ======================== .. code-block:: bash conda install -c conda-forge pystac Install from source =================== .. code-block:: bash pip install git+https://github.com/stac-utils/pystac.git .. _installation_dependencies: Dependencies ============ PySTAC requires Python >= 3.9. This project follows the recommendations of `NEP-29 `__ in deprecating support for Python versions. This means that users can expect support for Python 3.9 to be removed from the ``main`` branch after Apr 14, 2023 and therefore from the next release after that date. As a foundational component of the Python STAC ecosystem used in a number of downstream libraries, PySTAC aims to minimize its dependencies. As a result, the only dependency for the basic PySTAC library is `python-dateutil `__. PySTAC also has the following extras, which can be optionally installed to provide additional functionality: * ``validation`` Installs the additional `jsonschema `__ dependency. When this dependency is installed, the :ref:`validation methods ` may be used to validate STAC objects against the appropriate JSON schemas. To install: .. code-block:: bash pip install pystac[validation] * ``orjson`` Installs the additional `orjson `__ dependency. When this dependency is installed, `orjson` will be used as the default JSON serialization/deserialization for all operations in PySTAC. To install: .. code-block:: bash pip install pystac[orjson] * ``urllib3`` Installs the additional `urllib3 `__ dependency. For now, this is only used in :py:class:`pystac.stac_io.RetryStacIO`, but it may be used more extensively in the future. To install: .. code-block:: bash pip install pystac[urllib3] * ``jinja2`` Installs the additional `jinja2 `__ dependency. When this dependency is installed, jupyter notebooks display pretty representations of PySTAC objects To install: .. code-block:: bash pip install pystac[jinja2] Versions ======== To install a version of PySTAC that works with a specific versions of the STAC specification, install the matching version of PySTAC from the following table. .. list-table:: :widths: 50 50 :header-rows: 1 * - PySTAC - STAC * - 1.x - 1.0.x * - 0.5.x - 1.0.0-beta.* * - 0.4.x - 0.9.x * - 0.3.x - 0.8.x For instance, to work with STAC v0.9.x: .. code-block:: bash pip install pystac==0.4.0 STAC spec versions below 0.8 are not supported by PySTAC. pystac-1.9.0/docs/make.bat000066400000000000000000000014301451576074700153770ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd pystac-1.9.0/docs/quickstart.ipynb000066400000000000000000002456431451576074700172470ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Quickstart\n", "\n", "This notebook is a quick introduction to using PySTAC for reading an existing STAC catalog. For more in-depth examples check out the other tutorials.\n", "\n", "## Dependencies\n", "\n", "- PySTAC" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Reading a Catalog\n", "\n", "[A STAC Catalog](https://github.com/radiantearth/stac-spec/tree/master/catalog-spec) is used to group other STAC objects like Items, Collections, or even other Catalogs.\n", "\n", "We will be using a small example catalog adapted from the [example Landsat Collection](https://github.com/geotrellis/geotrellis-server/tree/977bad7a64c409341479c281c8c72222008861fd/stac-example/catalog/landsat-stac-collection) in the [GeoTrellis](https://geotrellis.io) repository. All STAC Items and Collections can be found in the [docs/example-catalog](https://github.com/stac-utils/pystac/tree/main/docs/example-catalog) directory of this repo; all Assets are hosted in the Landsat S3 bucket.\n", "\n", "First, we import the PySTAC classes we will be working with." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import shutil\n", "import tempfile\n", "from pathlib import Path\n", "\n", "from pystac import Catalog, get_stac_version" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, we read the example catalog and print some basic metadata." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ID: landsat-stac-collection-catalog\n", "Title: STAC for Landsat data\n", "Description: STAC for Landsat data\n" ] } ], "source": [ "root_catalog = Catalog.from_file(\"./example-catalog/catalog.json\")\n", "print(f\"ID: {root_catalog.id}\")\n", "print(f\"Title: {root_catalog.title or 'N/A'}\")\n", "print(f\"Description: {root_catalog.description or 'N/A'}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Note that we do not print the \"stac_version\" here. PySTAC automatically updates any Catalogs to the most recent supported STAC version and will automatically write this to the JSON object during serialization.*\n", "\n", "Let's confirm the latest STAC Spec version supported by PySTAC." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1.0.0\n" ] } ], "source": [ "print(get_stac_version())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Crawling Child Catalogs/Collections\n", "\n", "[STAC Collections](https://github.com/radiantearth/stac-spec/tree/master/collection-spec) are used to group related Items and provide aggregate or summary metadata for those Items.\n", "\n", "STAC Catalogs may have many nested layers of Catalogs or Collections within the top-level collection. Our example catalog has one Collection within the main Catalog at [landsat-8-l1/collection.json](./example-catalog/landsat-8-l1/collection.json). We can list the Collections in a given Catalog using the [Catalog.get_collections](https://pystac.readthedocs.io/en/latest/api.html#pystac.Catalog.get_collections) method. This method returns an iterable of PySTAC [Collection](https://pystac.readthedocs.io/en/latest/api.html#collection) instances, which we will turn into a `list`." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Number of collections: 1\n", "Collections IDs:\n", "- landsat-8-l1\n" ] } ], "source": [ "collections = list(root_catalog.get_collections())\n", "\n", "print(f\"Number of collections: {len(collections)}\")\n", "print(\"Collections IDs:\")\n", "for collection in collections:\n", " print(f\"- {collection.id}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's grab that Collection as a PySTAC [Collection](https://pystac.readthedocs.io/en/latest/api.html#collection) instance using the [Catalog.get_child method](https://pystac.readthedocs.io/en/latest/api.html#pystac.Catalog.get_child) so we can look at it in more detail. This method gets a child Catalog or Collection by ID, so we'll use the Collection ID that we printed above. Since this method returns `None` if no child exists with the given ID, we'll check to make sure we actually got the `Collection`." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "collection = root_catalog.get_child(\"landsat-8-l1\")\n", "assert collection is not None" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Crawling Items\n", "\n", "[STAC Items](https://github.com/radiantearth/stac-spec/tree/master/item-spec) are the fundamental building blocks of a STAC Catalog. Each Item represents a single spatiotemporal resource (e.g. a satellite scene).\n", "\n", "Both Catalogs and Collections may have Items associated with them. Let's crawl our catalog, starting at the root, to see what Items we have. The [Catalog.get_items method](https://pystac.readthedocs.io/en/latest/api.html#pystac.Catalog.get_items) provides a convenient way of recursively listing all Items associated with a Catalog and all of its sub-Catalogs by including the `recursive=True` option." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Number of items: 4\n", "- LC80140332018166LGN00\n", "- LC80150322018141LGN00\n", "- LC80150332018189LGN00\n", "- LC80300332018166LGN00\n" ] } ], "source": [ "items = list(root_catalog.get_items(recursive=True))\n", "\n", "print(f\"Number of items: {len(items)}\")\n", "for item in items:\n", " print(f\"- {item.id}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "These IDs are not very descriptive; in the next section, we will take a look at how we can access the rich metadata associated with each Item." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Item Metadata\n", "\n", "Items can have *a lot* of metadata. This can be a bit overwhelming at first, but break the metadata fields down into a few categories:\n", "\n", "- Core Item Metadata\n", "- Common Metadata\n", "- STAC Extensions\n", "\n", "We will walk through each of these metadata categories in the following sections. \n", "\n", "First, let's grab one of the Items using the [Catalog.get_items method](https://pystac.readthedocs.io/en/latest/api.html#pystac.Catalog.get_items). We will use `recursive=True` to recursively crawl all child Catalogs and/or Collections to find the Item." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "item = next(root_catalog.get_items(\"LC80140332018166LGN00\", recursive=True))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Core Item Metadata\n", "\n", "The core Item metadata fields include spatiotemporal information and the ID of the collection to which the Item belongs. These fields are all at the top level of the Item JSON and we can access them through attributes on the [PySTAC Item](https://pystac.readthedocs.io/en/latest/api.html#item) instance." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'type': 'Polygon',\n", " 'coordinates': [[[-76.12180471942207, 39.95810181489563],\n", " [-73.94910518227414, 39.55117185146004],\n", " [-74.49564725552679, 37.826064511480496],\n", " [-76.66550404911956, 38.240699151776084],\n", " [-76.12180471942207, 39.95810181489563]]]}" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.geometry" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[-76.66703, 37.82561, -73.94861, 39.95958]" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.bbox" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "datetime.datetime(2018, 6, 15, 15, 39, 9, tzinfo=tzutc())" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.datetime" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'landsat-8-l1'" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.collection_id" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we want the actual `Collection` instance instead of just the ID, we can use the [Item.get_collection](https://pystac.readthedocs.io/en/latest/api.html#pystac.Item.get_collection) method." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "
\n", "
\n", "
    \n", " \n", " \n", " \n", "
  • \n", " type\n", " \"Collection\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " id\n", " \"landsat-8-l1\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " stac_version\n", " \"1.0.0\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " description\n", " \"Landsat 8 imagery radiometrically calibrated and orthorectified using ground points and Digital Elevation Model (DEM) data to correct relief displacement.\"\n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " \n", " links\n", " [] 7 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"root\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/catalog.json\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"application/json\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"STAC for Landsat data\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 1\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"item\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-06/LC80140332018166LGN00.json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 2\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"item\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-05/LC80150322018141LGN00.json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 3\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"item\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-07/LC80150332018189LGN00.json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 4\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"item\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-06/LC80300332018166LGN00.json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 5\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"self\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/collection.json\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"application/json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 6\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"parent\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/catalog.json\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"application/json\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"STAC for Landsat data\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", " \n", "
  • \n", " \n", " stac_extensions\n", " [] 1 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", " \"https://example.com/stac/landsat-extension/1.0/schema.json\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " properties\n", "
      \n", " \n", " \n", " \n", "
    • \n", " collection\n", " \"landsat-8-l1\"\n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " \n", " instruments\n", " [] 1 items\n", " \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 0\n", " \"OLI_TIRS\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " view:sun_azimuth\n", " 149.01607154\n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " \n", " eo:bands\n", " [] 11 items\n", " \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 0\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B1\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.02\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.44\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"coastal\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 1\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B2\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.06\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.48\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"blue\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 2\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B3\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.06\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.56\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"green\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 3\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B4\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.04\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.65\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"red\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 4\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B5\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.03\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.86\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"nir\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 5\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B6\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.08\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 1.6\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"swir16\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 6\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B7\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.22\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 2.2\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"swir22\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 7\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B8\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.18\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.59\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"pan\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 8\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B9\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.02\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 1.37\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"cirrus\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 9\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B10\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.8\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 10.9\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"lwir11\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 10\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B11\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 1\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 12\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"lwir2\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " view:off_nadir\n", " 0\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " view:azimuth\n", " 0\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " platform\n", " \"landsat-8\"\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " gsd\n", " 15\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " view:sun_elevation\n", " 59.214247\n", "
    • \n", " \n", " \n", " \n", "
    \n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " title\n", " \"Landsat 8 L1\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " extent\n", "
      \n", " \n", " \n", " \n", "
    • \n", " spatial\n", "
        \n", " \n", " \n", "
      • \n", " \n", " bbox\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", "
        • \n", " \n", " 0\n", " [] 4 items\n", " \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 0\n", " -180.0\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 1\n", " -90.0\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 2\n", " 180.0\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 3\n", " 90.0\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
        • \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " temporal\n", "
        \n", " \n", " \n", "
      • \n", " \n", " interval\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", "
        • \n", " \n", " 0\n", " [] 2 items\n", " \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 0\n", " \"2018-05-21T15:44:59Z\"\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 1\n", " \"2018-07-08T15:45:34Z\"\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
        • \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " license\n", " \"proprietary\"\n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " \n", " keywords\n", " [] 3 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", " \"landsat\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 1\n", " \"earth observation\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 2\n", " \"usgs\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", " \n", "
  • \n", " \n", " providers\n", " [] 1 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", "
        \n", " \n", " \n", " \n", "
      • \n", " name\n", " \"Development Seed\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", " \n", "
        • \n", " 0\n", " \"processor\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " url\n", " \"https://github.com/sat-utils/sat-api\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", "
\n", "
\n", "
" ], "text/plain": [ "" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.get_collection()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Common Metadata\n", "\n", "Certain fields that are commonly used in Items, but may also be found in other objects (e.g. Assets) are defined in the [Common Metadata](https://github.com/radiantearth/stac-spec/blob/master/item-spec/common-metadata.md) section of the spec. These include licensing and instrument information, descriptions of datetime ranges, and some other common fields. These properties can be found as attributes of the `Item.common_metadata` property, which is an instance of the [CommonMetadata class](https://pystac.readthedocs.io/en/latest/api.html#pystac.CommonMetadata)." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['OLI_TIRS']" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.common_metadata.instruments" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'landsat-8'" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.common_metadata.platform" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "30" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.common_metadata.gsd" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "### STAC Extensions\n", "\n", "[STAC Extensions](https://stac-extensions.github.io/) are a mechanism for providing additional metadata not covered by the core STAC Spec. We can see which STAC Extensions are implemented by this particular Item by examining the list of extension URIs in the `stac_extensions` field." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['https://stac-extensions.github.io/eo/v1.1.0/schema.json',\n", " 'https://stac-extensions.github.io/view/v1.0.0/schema.json',\n", " 'https://stac-extensions.github.io/projection/v1.1.0/schema.json']" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.stac_extensions" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This Item implements the [Electro-Optical](https://github.com/stac-extensions/eo), [View Geometry](https://github.com/stac-extensions/view), and [Projection](https://github.com/stac-extensions/projection) Extensions. \n", "\n", "We can also check if a specific extension is implemented using [ext.has](https://pystac.readthedocs.io/en/latest/api.html#pystac.item.ext.has) with the name of that extension." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.ext.has(\"eo\")" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "False" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.ext.has(\"raster\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can access fields associated with the extension as attributes on the extension instance. For instance, the [\"eo:cloud_cover\" field](https://github.com/stac-extensions/eo#item-properties-or-asset-fields) defined in the Electro-Optical Extension can be accessed using the [item.ext.eo.cloud_cover](https://pystac.readthedocs.io/en/latest/api.html#pystac.extensions.eo.EOExtension.cloud_cover) attribute." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "22" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.ext.eo.cloud_cover" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can also access the cloud cover field directly in the Item properties." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "22" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.properties[\"eo:cloud_cover\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can access the Item's assets through the `assets` attribute, which is a dictionary:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "index: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/index.html (text/html)\n", "thumbnail: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_thumb_large.jpg (image/jpeg)\n", "B1: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B1.TIF (image/tiff)\n", "B2: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B2.TIF (image/tiff)\n", "B3: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B3.TIF (image/tiff)\n", "B4: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B4.TIF (image/tiff)\n", "B5: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B5.TIF (image/tiff)\n", "B6: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B6.TIF (image/tiff)\n", "B7: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B7.TIF (image/tiff)\n", "B8: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B8.TIF (image/tiff)\n", "B9: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B9.TIF (image/tiff)\n", "B10: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B10.TIF (image/tiff)\n", "B11: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B11.TIF (image/tiff)\n", "ANG: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_ANG.txt (text/plain)\n", "MTL: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_MTL.txt (text/plain)\n", "BQA: https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_BQA.TIF (image/tiff)\n" ] } ], "source": [ "for asset_key in item.assets:\n", " asset = item.assets[asset_key]\n", " print(\"{}: {} ({})\".format(asset_key, asset.href, asset.media_type))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can use the `to_dict()` method to convert an Asset, or any PySTAC object, into a dictionary:" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'href': 'https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/014/033/LC08_L1TP_014033_20180615_20180703_01_T1/LC08_L1TP_014033_20180615_20180703_01_T1_B3.TIF',\n", " 'type': 'image/tiff',\n", " 'title': 'Band 3 (green)',\n", " 'eo:bands': [{'name': 'B3',\n", " 'full_width_half_max': 0.06,\n", " 'center_wavelength': 0.56,\n", " 'common_name': 'green'}],\n", " 'roles': []}" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "asset = item.assets[\"B3\"]\n", "asset.to_dict()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here we use the eo extension to get the band information for the asset:" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[]" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bands = asset.ext.eo.bands\n", "bands" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'name': 'B3',\n", " 'full_width_half_max': 0.06,\n", " 'center_wavelength': 0.56,\n", " 'common_name': 'green'}" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bands[0].to_dict()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Writing STAC Objects\n", "\n", "We can also use PySTAC to create and/or update STAC objects and write them to disk. This Quickstart Tutorial will introduce you to some very basic concepts in writing STAC objects; for a more thorough tutorial, please see the [\"How to create STAC Catalogs\"](./tutorials/how-to-create-stac-catalogs.ipynb) tutorial.\n", "\n", "Suppose there was a mistake in the cloud cover value that we looked at earlier and that we would like to add a value for the `instrument` field, which is currently null. We can update these values using the same attributes and properties as before, then save the entire catalog to our local drive." ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "new_catalog = root_catalog.clone()" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [], "source": [ "item_to_update = next(root_catalog.get_items(\"LC80140332018166LGN00\", recursive=True))\n", "\n", "# Update the cloud cover\n", "item_to_update.ext.eo.cloud_cover = 30\n", "\n", "# Add the instrument field\n", "item_to_update.common_metadata.instruments = [\"LANDSAT\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can examine the Item properties directly to verify that the changes have taken effect." ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "New Cloud Cover: 30\n", "New Instruments: ['LANDSAT']\n" ] } ], "source": [ "print(f\"New Cloud Cover: {item_to_update.properties['eo:cloud_cover']}\")\n", "print(f\"New Instruments: {item_to_update.properties['instruments']}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We will write this updated catalog to a temporary directory in our local drive using the [Catalog.normalize_and_save](https://pystac.readthedocs.io/en/latest/api.html#pystac.Catalog.normalize_and_save) method." ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [], "source": [ "# Create a temporary directory\n", "tmp_dir = tempfile.mkdtemp()" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Catalog saved to: /tmp/tmp9bmp70k9/catalog.json\n" ] } ], "source": [ "# Save the catalog and normalize all paths\n", "new_catalog.normalize_and_save(tmp_dir)\n", "print(f\"Catalog saved to: {new_catalog.get_self_href()}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can open up Item that we just updated to verify that the new values were written to disk." ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [], "source": [ "item_path = Path(tmp_dir) / \"landsat-8-l1\" / \"LC80140332018166LGN00\" / \"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, we clean up the temporary directory." ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [], "source": [ "shutil.rmtree(tmp_dir, ignore_errors=True)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.6" }, "vscode": { "interpreter": { "hash": "28618a729221ed2dc6301bcedf20e90b9d193b9b884dd15c675da71a09b73fa8" } } }, "nbformat": 4, "nbformat_minor": 4 } pystac-1.9.0/docs/tutorials.rst000066400000000000000000000046401451576074700165600ustar00rootroot00000000000000.. _tutorials: Tutorials ######### PySTAC Introduction ------------------- - :tutorial:`GitHub version ` - :ref:`Docs version ` This tutorial gives an introduction to PySTAC concepts through code examples. How to read data from STAC -------------------------- - :tutorial:`GitHub version ` - :ref:`Docs version ` This tutorial shows how to read data from PySTAC into xarray. PySTAC SpaceNet tutorial ------------------------ - :tutorial:`GitHub version ` - :ref:`Docs version ` This tutorial shows how to create and manipulate a STAC of `SpaceNet `_ data. How to create STAC Catalogs with PySTAC --------------------------------------- - :tutorial:`GitHub version ` - :ref:`Docs version ` This was a tutorial that was part of a 30 minute presentation at the `community STAC sprint `_ in Arlington, VA in November 2019. It runs through creating a STAC of images from the `SpaceNet 5 `_ dataset. Creating a Landsat 8 STAC ------------------------- - :tutorial:`GitHub version ` - :ref:`Docs version ` This tutorial was presented at [Cloud Native Geospatial Outreach Day](https://sites.google.com/radiant.earth/cng-agenda/) on September 8th, 2020. It shows how to create a STAC collection from a subset of Landsat 8 scenes over a location. Adding New and Custom Extensions -------------------------------- - :tutorial:`GitHub version ` - :ref:`Docs version ` This tutorial goes over how to contribute new extensions to PySTAC as well as how to implement your own custom extensions. .. toctree:: :hidden: :maxdepth: 2 :glob: tutorials/pystac-introduction.ipynb tutorials/how-to-read-data-from-stac.ipynb tutorials/pystac-spacenet-tutorial.ipynb tutorials/how-to-create-stac-catalogs.ipynb tutorials/creating-a-landsat-stac.ipynb tutorials/adding-new-and-custom-extensions.ipynb pystac-1.9.0/docs/tutorials/000077500000000000000000000000001451576074700160225ustar00rootroot00000000000000pystac-1.9.0/docs/tutorials/adding-new-and-custom-extensions.ipynb000066400000000000000000000424351451576074700253570ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Adding New and Custom Extensions\n", "\n", "This tutorial will cover using the `PropertiesExtension` and `ExtensionManagementMixin` classes in `pystac.extensions.base` to implement a new extension in PySTAC, and how to make that class accessible via the `pystac.Item.ext` interface.\n", "\n", "For this exercise, we will implement an imaginary Order Request Extension that allows us to track an internal order ID associated with a given satellite image, as well as the history of that imagery order. This use-case is specific enough that it would probably not be a good candidate for an actual STAC Extension, but it gives us an opportunity to highlight some of the key aspects and patterns used in implementing STAC Extensions in PySTAC." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First, we import the PySTAC modules and classes that we will be using throughout the tutorial." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from typing import Literal\n", "from datetime import datetime, timedelta\n", "from pprint import pprint\n", "from typing import Any, Dict, List, Optional, Union\n", "from uuid import uuid4\n", "\n", "import pystac\n", "from pystac.utils import (\n", " StringEnum,\n", " datetime_to_str,\n", " get_required,\n", " map_opt,\n", " str_to_datetime,\n", ")\n", "from pystac.extensions.base import PropertiesExtension, ExtensionManagementMixin" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Define the Extension" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Our extension will extend STAC Items by adding the following properties:\n", "\n", "- `order:id`: A unique string ID associated with the internal order for this image. This field will be required.\n", "- `order:history`: A chronological list of events associated with this order. Each of these \"events\" will have a timestamp and an event type, which will be one of the following: `submitted`, `started_processing`, `delivered`, `cancelled`. This field will be optional." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create Extension Classes\n", "\n", "Let's start by creating a class to represent the order history events." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "class OrderEventType(StringEnum):\n", " SUBMITTED = \"submitted\"\n", " STARTED_PROCESSING = \"started_processing\"\n", " DELIVERED = \"delivered\"\n", " CANCELLED = \"cancelled\"\n", "\n", "\n", "class OrderEvent:\n", " properties: Dict[str, Any]\n", "\n", " def __init__(self, properties: Dict[str, Any]) -> None:\n", " self.properties = properties\n", "\n", " @property\n", " def event_type(self) -> OrderEventType:\n", " return get_required(self.properties.get(\"type\"), self, \"event_type\")\n", "\n", " @event_type.setter\n", " def event_type(self, v: OrderEventType) -> None:\n", " self.properties[\"type\"] = str(v)\n", "\n", " @property\n", " def timestamp(self) -> datetime:\n", " return str_to_datetime(\n", " get_required(self.properties.get(\"timestamp\"), self, \"timestamp\")\n", " )\n", "\n", " @timestamp.setter\n", " def timestamp(self, v: datetime) -> None:\n", " self.properties[\"timestamp\"] = datetime_to_str(v)\n", "\n", " def __repr__(self) -> str:\n", " return \"\"\n", "\n", " def apply(\n", " self,\n", " event_type: OrderEventType,\n", " timestamp: datetime,\n", " ) -> None:\n", " self.event_type = event_type\n", " self.timestamp = timestamp\n", "\n", " @classmethod\n", " def create(\n", " cls,\n", " event_type: OrderEventType,\n", " timestamp: datetime,\n", " ) -> \"OrderEvent\":\n", " oe = cls({})\n", " oe.apply(event_type=event_type, timestamp=timestamp)\n", " return oe\n", "\n", " def to_dict(self) -> Dict[str, Any]:\n", " return self.properties" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A few important notes about how we constructed this:\n", "\n", "- We used PySTAC's [StringEnum class](https://pystac.readthedocs.io/en/latest/api/utils.html#pystac.utils.StringEnum), which inherits from the Python [Enum](https://docs.python.org/3/library/enum.html) class, to capture the allowed event type values. This class has built-in methods that will convert these instances to strings when serializing STAC Items to JSON.\n", "- We use property getters and setters to manipulate a `properties` dictionary in our `OrderEvent` class. We will see later how this pattern allows us to mutate Item property dictionaries in-place so that updates to the `OrderEvent` object are synced to the Item they extend.\n", "- The `timestamp` property is converted to a string before it is saved in the `properties` dictionary. This ensures that dictionary is always JSON-serializable but allows us to work with the values as a Python `datetime` instance when using the property getter.\n", "- We use `event_type` as our property name so that we do not shadow the built-in `type` function in the `apply` method. However, this values is stored under the desired `\"type\"` key in the underlying `properties` dictionary." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, we will create a new class inheriting from `PropertiesExtension` and `ExtensionManagementMixin`. Since this class only extends `pystac.Item` instance, we do not need to make it [generic](https://docs.python.org/3/library/typing.html#typing.Generic). If you were creating an extension that applied to multiple object types (e.g. `pystac.Item` and `pystac.Asset`) then you would need to inherit from `typing.Generic` as well and create concrete extension classed for each of these object types (see the [EOExtension](https://github.com/stac-utils/pystac/blob/3c5176f178a4345cb50d5dab83f1dab504ed2682/pystac/extensions/eo.py#L279), [ItemEOExtension](https://github.com/stac-utils/pystac/blob/3c5176f178a4345cb50d5dab83f1dab504ed2682/pystac/extensions/eo.py#L385), and [AssetEOExtension](https://github.com/stac-utils/pystac/blob/3c5176f178a4345cb50d5dab83f1dab504ed2682/pystac/extensions/eo.py#L429) classes for an example of this implementation)." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "SCHEMA_URI: str = \"https://example.com/image-order/v1.0.0/schema.json\"\n", "PREFIX: str = \"order:\"\n", "ID_PROP: str = PREFIX + \"id\"\n", "HISTORY_PROP: str = PREFIX + \"history\"\n", "\n", "\n", "class OrderExtension(\n", " PropertiesExtension, ExtensionManagementMixin[Union[pystac.Item, pystac.Collection]]\n", "):\n", " name: Literal[\"order\"] = \"order\"\n", "\n", " def __init__(self, item: pystac.Item):\n", " self.item = item\n", " self.properties = item.properties\n", "\n", " def apply(\n", " self, order_id: str = None, history: Optional[List[OrderEvent]] = None\n", " ) -> None:\n", " self.order_id = order_id\n", " self.history = history\n", "\n", " @property\n", " def order_id(self) -> str:\n", " return get_required(self._get_property(ID_PROP, str), self, ID_PROP)\n", "\n", " @order_id.setter\n", " def order_id(self, v: str) -> None:\n", " self._set_property(ID_PROP, v, pop_if_none=False)\n", "\n", " @property\n", " def history(self) -> Optional[List[OrderEvent]]:\n", " return map_opt(\n", " lambda history: [OrderEvent(d) for d in history],\n", " self._get_property(HISTORY_PROP, List[OrderEvent]),\n", " )\n", "\n", " @history.setter\n", " def history(self, v: Optional[List[OrderEvent]]) -> None:\n", " self._set_property(\n", " HISTORY_PROP,\n", " map_opt(lambda history: [event.to_dict() for event in history], v),\n", " pop_if_none=True,\n", " )\n", "\n", " @classmethod\n", " def get_schema_uri(cls) -> str:\n", " return SCHEMA_URI\n", "\n", " @classmethod\n", " def ext(cls, obj: pystac.Item, add_if_missing: bool = False) -> \"OrderExtension\":\n", " if isinstance(obj, pystac.Item):\n", " cls.ensure_has_extension(obj, add_if_missing)\n", " return OrderExtension(obj)\n", " else:\n", " raise pystac.ExtensionTypeError(\n", " f\"OrderExtension does not apply to type '{type(obj).__name__}'\"\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As with the `OrderEvent` class, we use property getters and setters for our extension fields (the `PropertiesExtension` class has a `properties` attribute where these are stored). Rather than setting these values directly in the dictionary, we use the `_get_property` and `_set_property` methods that are built into the `PropertiesExtension` class). We also add an `ext` method that will be used to extend `pystac.Item` instances, and a `get_schema_uri` method that is required for all `PropertiesExtension` classes." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Use the Extension\n", "\n", "Let's try using our new classes to extend an `Item` and access the extension properties. We'll start by loading the core Item example from the STAC spec examples [here](https://github.com/radiantearth/stac-spec/blob/master/examples/core-item.json) and printing the existing properties." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'title': 'Core Item',\n", " 'description': 'A sample STAC Item that includes examples of all common metadata',\n", " 'datetime': None,\n", " 'start_datetime': '2020-12-11T22:38:32.125Z',\n", " 'end_datetime': '2020-12-11T22:38:32.327Z',\n", " 'created': '2020-12-12T01:48:13.725Z',\n", " 'updated': '2020-12-12T01:48:13.725Z',\n", " 'platform': 'cool_sat1',\n", " 'instruments': ['cool_sensor_v1'],\n", " 'constellation': 'ion',\n", " 'mission': 'collection 5624',\n", " 'gsd': 0.512}" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item = pystac.read_file(\n", " \"https://raw.githubusercontent.com/radiantearth/stac-spec/master/examples/core-item.json\"\n", ")\n", "item.properties" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, let's verify that this Item does not implement our new Order Extension yet and that it does not already contain any of our Order Extension properties." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Implements Extension: False\n", "Order ID: None\n", "History:\n" ] } ], "source": [ "print(f\"Implements Extension: {OrderExtension.has_extension(item)}\")\n", "print(f\"Order ID: {item.properties.get(ID_PROP)}\")\n", "print(\"History:\")\n", "for event in item.properties.get(HISTORY_PROP, []):\n", " pprint(event)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As expected, this Item does not implement the extension (i.e. the schema URI is not in the Item's `stac_extensions` list). Let's add it, create an instance of `OrderExtension` that extends the `Item`, and add some values for our extension fields." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "order_ext = OrderExtension.ext(item, add_if_missing=True)\n", "\n", "# Create a unique string ID for the order ID\n", "order_ext.order_id = str(uuid4())\n", "\n", "# Create some fake order history and set it using the extension\n", "event_1 = OrderEvent.create(\n", " event_type=OrderEventType.SUBMITTED, timestamp=datetime.now() - timedelta(days=1)\n", ")\n", "event_2 = OrderEvent.create(\n", " event_type=OrderEventType.STARTED_PROCESSING,\n", " timestamp=datetime.now() - timedelta(hours=12),\n", ")\n", "event_3 = OrderEvent.create(\n", " event_type=OrderEventType.DELIVERED, timestamp=datetime.now() - timedelta(hours=1)\n", ")\n", "order_ext.history = [event_1, event_2, event_3]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's check to see if these values were written to our Item properties." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Implements Extension: True\n", "Order ID: 7a206229-78f0-46cb-afc2-acf45e14afab\n", "History:\n", "{'timestamp': '2023-10-11T11:21:50.989315Z', 'type': 'submitted'}\n", "{'timestamp': '2023-10-11T23:21:50.989372Z', 'type': 'started_processing'}\n", "{'timestamp': '2023-10-12T10:21:50.989403Z', 'type': 'delivered'}\n" ] } ], "source": [ "print(f\"Implements Extension: {OrderExtension.has_extension(item)}\")\n", "print(f\"Order ID: {item.properties.get(ID_PROP)}\")\n", "print(\"History:\")\n", "for event in item.properties.get(HISTORY_PROP, []):\n", " pprint(event)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## (Optional) Add access via `Item.ext`\n", "\n", "_This applies if you are planning on opening a Pull Request to add this implementation of the extension class to the pystac library_\n", "\n", "Now that you have a complete extension class, you can add access to it via the `pystac.Item.ext` interface by following these steps:\n", "\n", "1) Make sure that your Extension class has a `name` attribute with `Literal()` as the type.\n", "2) Import your Extension class in `pystac/extensions/ext.py`\n", "3) Add the `name` to `EXTENSION_NAMES`\n", "4) Add the mapping from name to class to `EXTENSION_NAME_MAPPING`\n", "5) Add a getter method to the Ext class for any object type that this extension works with.\n", "\n", "Here is an example of the diff:\n", "\n", "```diff\n", "diff --git a/pystac/extensions/ext.py b/pystac/extensions/ext.py\n", "index 93a30fe..2dbe5ca 100644\n", "--- a/pystac/extensions/ext.py\n", "+++ b/pystac/extensions/ext.py\n", "@@ -9,6 +9,7 @@ from pystac.extensions.file import FileExtension\n", " from pystac.extensions.grid import GridExtension\n", " from pystac.extensions.item_assets import AssetDefinition, ItemAssetsExtension\n", " from pystac.extensions.mgrs import MgrsExtension\n", "+from pystac.extensions.order import OrderExtension\n", " from pystac.extensions.pointcloud import PointcloudExtension\n", " from pystac.extensions.projection import ProjectionExtension\n", " from pystac.extensions.raster import RasterExtension\n", "@@ -32,6 +33,7 @@ EXTENSION_NAMES = Literal[\n", " \"grid\",\n", " \"item_assets\",\n", " \"mgrs\",\n", "+ \"order\",\n", " \"pc\",\n", " \"proj\",\n", " \"raster\",\n", "@@ -54,6 +56,7 @@ EXTENSION_NAME_MAPPING: Dict[EXTENSION_NAMES, Any] = {\n", " GridExtension.name: GridExtension,\n", " ItemAssetsExtension.name: ItemAssetsExtension,\n", " MgrsExtension.name: MgrsExtension,\n", "+ OrderExtension.name: OrderExtension,\n", " PointcloudExtension.name: PointcloudExtension,\n", " ProjectionExtension.name: ProjectionExtension,\n", " RasterExtension.name: RasterExtension,\n", "@@ -150,6 +153,10 @@ class ItemExt:\n", " def mgrs(self) -> MgrsExtension:\n", " return MgrsExtension.ext(self.stac_object)\n", " \n", "+ @property\n", "+ def order(self) -> OrderExtension:\n", "+ return OrderExtension.ext(self.stac_object)\n", "+\n", " @property\n", " def pc(self) -> PointcloudExtension[pystac.Item]:\n", " return PointcloudExtension.ext(self.stac_object)\n", "```\n", "\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.6" } }, "nbformat": 4, "nbformat_minor": 2 } pystac-1.9.0/docs/tutorials/creating-a-landsat-stac.ipynb000066400000000000000000003316251451576074700234650ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Creating a STAC of Landsat data\n", "\n", "In this tutorial we create a STAC of Landsat data provided by Microsoft's [Planetary Computer](https://planetarycomputer.microsoft.com/dataset/landsat-c2-l2). There's a lot of Landsat scenes, so we'll only take a subset of scenes that are from a specific year and over a specific location. We'll translate existing metadata about each scene to STAC information, utilizing the `eo`, `view`, `proj`, `raster` and `classification` extensions. Finally we'll write out the STAC catalog to our local machine, allowing us to use [stac-browser](https://github.com/radiantearth/stac-browser) to preview the images.\n", "\n", "### Requirements\n", "\n", "To run this tutorial you'll need to have installed PySTAC with the validation extra and the Planetary Computer package. To do this, use:\n", "\n", "```\n", "pip install 'pystac[validation]' planetary-computer\n", "```\n", "\n", "Also to run this notebook you'll need [jupyter](https://jupyter.org/) installed locally as well. If you're running in a docker container, make sure that port `5555` is exposed if you want to run the server at the end of the notebook." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from datetime import datetime\n", "from dateutil.parser import parse\n", "from functools import partial\n", "import json\n", "from os.path import dirname, join\n", "from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast\n", "from typing_extensions import TypedDict\n", "from urllib.parse import urlparse, urlunparse\n", "\n", "import planetary_computer as pc\n", "import pystac\n", "from pystac.extensions.classification import (\n", " ClassificationExtension,\n", " Classification,\n", " Bitfield,\n", ")\n", "from pystac.extensions.eo import EOExtension\n", "from pystac.extensions.eo import Band as EOBand\n", "from pystac.extensions.projection import ProjectionExtension\n", "from pystac.extensions.raster import RasterBand, RasterExtension\n", "from pystac.extensions.view import ViewExtension" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Identify target scenes\n", "\n", "The Planetary Computer provides a STAC API that we could use to search for data within an area and time of interest, but since this notebook is intended to be a tutorial on creating STAC in the first place, doing so would put the cart ahead of the horse. Instead, we supply a list of metadata files for Landsat-8 and Landsat-9 scenes covering the center of Philadelphia, Pennsylvania in autumn of 2022 that we will work with:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "location_name = \"Philly\"\n", "scene_mtls = [\n", " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_MTL.xml\",\n", " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC09_L2SP_014032_20221211_20221213_02_T2/LC09_L2SP_014032_20221211_20221213_02_T2_MTL.xml\",\n", " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221203_20221212_02_T2/LC08_L2SP_014032_20221203_20221212_02_T2_MTL.xml\",\n", " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC09_L2SP_014032_20221125_20230320_02_T2/LC09_L2SP_014032_20221125_20230320_02_T2_MTL.xml\",\n", " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221117_20221128_02_T1/LC08_L2SP_014032_20221117_20221128_02_T1_MTL.xml\",\n", " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC09_L2SP_014032_20221109_20221111_02_T1/LC09_L2SP_014032_20221109_20221111_02_T1_MTL.xml\",\n", " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221101_20221114_02_T1/LC08_L2SP_014032_20221101_20221114_02_T1_MTL.xml\",\n", " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC09_L2SP_014032_20221024_20221026_02_T2/LC09_L2SP_014032_20221024_20221026_02_T2_MTL.xml\",\n", " \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC09_L2SP_014032_20221008_20221010_02_T1/LC09_L2SP_014032_20221008_20221010_02_T1_MTL.xml\",\n", "]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Read metadata from the MTL file\n", "\n", "Landsat metadata is contained in an `MTL` file that comes in either `.txt` or `.xml` formats. We'll rely on the XML version since it is more consistently available. This will require that we provide some facility for parsing the XML into a more usable format:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "# Taken from https://stackoverflow.com/questions/2148119/how-to-convert-an-xml-string-to-a-dictionary\n", "from xml.etree import cElementTree as ElementTree\n", "\n", "\n", "class XmlListConfig(list):\n", " def __init__(self, aList):\n", " for element in aList:\n", " if element:\n", " if len(element) == 1 or element[0].tag != element[1].tag:\n", " self.append(XmlDictConfig(element))\n", " elif element[0].tag == element[1].tag:\n", " self.append(XmlListConfig(element))\n", " elif element.text:\n", " text = element.text.strip()\n", " if text:\n", " self.append(text)\n", "\n", "\n", "class XmlDictConfig(dict):\n", " def __init__(self, parent_element):\n", " if parent_element.items():\n", " self.update(dict(parent_element.items()))\n", " for element in parent_element:\n", " if element:\n", " if len(element) == 1 or element[0].tag != element[1].tag:\n", " aDict = XmlDictConfig(element)\n", " else:\n", " aDict = {element[0].tag: XmlListConfig(element)}\n", " if element.items():\n", " aDict.update(dict(element.items()))\n", " self.update({element.tag: aDict})\n", " elif element.items():\n", " self.update({element.tag: dict(element.items())})\n", " else:\n", " self.update({element.tag: element.text})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can then use these classes to get the MTL file for our scene. Notice we use `pystac.STAC_IO.read_text`; this is the method that PySTAC uses to read text as it crawls a STAC. It can read from the local filesystem or HTTP/HTTPS by default. Also, it can be extended to read from other sources such as cloud providers—[see the documentation here](https://pystac.readthedocs.io/en/latest/concepts.html#using-stac-io). For now we'll use it directly as an easy way to read a text file from an HTTPS source." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "stac_io = pystac.StacIO.default()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Since we're reading our files from the Planetary Computer's blob storage, we're also going to have to take the additional step of signing our requests using the `planetary-computer` package's `sign()` function. The raw URL is passed in, and the result has a shared access token applied. See the [planetary-computer Python package](https://github.com/microsoft/planetary-computer-sdk-for-python) for more details. We'll see the use of `pc.sign()` throughout the code below, and it will be necessary for asset HREFs to be passed through this function by the user of the catalog as well." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "def get_metadata(xml_url: str) -> Dict[str, Any]:\n", " result = XmlDictConfig(ElementTree.XML(stac_io.read_text(pc.sign(xml_url))))\n", " result[\n", " \"ORIGINAL_URL\"\n", " ] = xml_url # Include the original URL in the metadata for use later\n", " return result" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's read the MTL file for the first scene and see what it looks like." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " \"PRODUCT_CONTENTS\": {\n", " \"ORIGIN\": \"Image courtesy of the U.S. Geological Survey\",\n", " \"DIGITAL_OBJECT_IDENTIFIER\": \"https://doi.org/10.5066/P9OGBGM6\",\n", " \"LANDSAT_PRODUCT_ID\": \"LC08_L2SP_014032_20221219_20230113_02_T1\",\n", " \"PROCESSING_LEVEL\": \"L2SP\",\n", " \"COLLECTION_NUMBER\": \"02\",\n", " \"COLLECTION_CATEGORY\": \"T1\",\n", " \"OUTPUT_FORMAT\": \"GEOTIFF\",\n", " \"FILE_NAME_BAND_1\": \"LC08_L2SP_014032_20221219_20230113_02_T1_SR_B1.TIF\",\n", " \"FILE_NAME_BAND_2\": \"LC08_L2SP_014032_20221219_20230113_02_T1_SR_B2.TIF\",\n", " \"FILE_NAME_BAND_3\": \"LC08_L2SP_014032_20221219_20230113_02_T1_SR_B3.TIF\",\n", " \"FILE_NAME_BAND_4\": \"LC08_L2SP_014032_20221219_20230113_02_T1_SR_B4.TIF\",\n", " \"FILE_NAME_BAND_5\": \"LC08_L2SP_014032_20221219_20230113_02_T1_SR_B5.TIF\",\n", " \"FILE_NAME_BAND_6\": \"LC08_L2SP_014032_20221219_20230113_02_T1_SR_B6.TIF\",\n", " \"FILE_NAME_BAND_7\": \"LC08_L2SP_014032_20221219_20230113_02_T1_SR_B7.TIF\",\n", " \"FILE_NAME_BAND_ST_B10\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_B10.TIF\",\n", " \"FILE_NAME_THERMAL_RADIANCE\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_TRAD.TIF\",\n", " \"FILE_NAME_UPWELL_RADIANCE\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_URAD.TIF\",\n", " \"FILE_NAME_DOWNWELL_RADIANCE\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_DRAD.TIF\",\n", " \"FILE_NAME_ATMOSPHERIC_TRANSMITTANCE\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_ATRAN.TIF\",\n", " \"FILE_NAME_EMISSIVITY\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_EMIS.TIF\",\n", " \"FILE_NAME_EMISSIVITY_STDEV\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_EMSD.TIF\",\n", " \"FILE_NAME_CLOUD_DISTANCE\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_CDIST.TIF\",\n", " \"FILE_NAME_QUALITY_L2_AEROSOL\": \"LC08_L2SP_014032_20221219_20230113_02_T1_SR_QA_AEROSOL.TIF\",\n", " \"FILE_NAME_QUALITY_L2_SURFACE_TEMPERATURE\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ST_QA.TIF\",\n", " \"FILE_NAME_QUALITY_L1_PIXEL\": \"LC08_L2SP_014032_20221219_20230113_02_T1_QA_PIXEL.TIF\",\n", " \"FILE_NAME_QUALITY_L1_RADIOMETRIC_SATURATION\": \"LC08_L2SP_014032_20221219_20230113_02_T1_QA_RADSAT.TIF\",\n", " \"FILE_NAME_ANGLE_COEFFICIENT\": \"LC08_L2SP_014032_20221219_20230113_02_T1_ANG.txt\",\n", " \"FILE_NAME_METADATA_ODL\": \"LC08_L2SP_014032_20221219_20230113_02_T1_MTL.txt\",\n", " \"FILE_NAME_METADATA_XML\": \"LC08_L2SP_014032_20221219_20230113_02_T1_MTL.xml\",\n", " \"DATA_TYPE_BAND_1\": \"UINT16\",\n", " \"DATA_TYPE_BAND_2\": \"UINT16\",\n", " \"DATA_TYPE_BAND_3\": \"UINT16\",\n", " \"DATA_TYPE_BAND_4\": \"UINT16\",\n", " \"DATA_TYPE_BAND_5\": \"UINT16\",\n", " \"DATA_TYPE_BAND_6\": \"UINT16\",\n", " \"DATA_TYPE_BAND_7\": \"UINT16\",\n", " \"DATA_TYPE_BAND_ST_B10\": \"UINT16\",\n", " \"DATA_TYPE_THERMAL_RADIANCE\": \"INT16\",\n", " \"DATA_TYPE_UPWELL_RADIANCE\": \"INT16\",\n", " \"DATA_TYPE_DOWNWELL_RADIANCE\": \"INT16\",\n", " \"DATA_TYPE_ATMOSPHERIC_TRANSMITTANCE\": \"INT16\",\n", " \"DATA_TYPE_EMISSIVITY\": \"INT16\",\n", " \"DATA_TYPE_EMISSIVITY_STDEV\": \"INT16\",\n", " \"DATA_TYPE_CLOUD_DISTANCE\": \"INT16\",\n", " \"DATA_TYPE_QUALITY_L2_AEROSOL\": \"UINT8\",\n", " \"DATA_TYPE_QUALITY_L2_SURFACE_TEMPERATURE\": \"INT16\",\n", " \"DATA_TYPE_QUALITY_L1_PIXEL\": \"UINT16\",\n", " \"DATA_TYPE_QUALITY_L1_RADIOMETRIC_SATURATION\": \"UINT16\"\n", " },\n", " \"IMAGE_ATTRIBUTES\": {\n", " \"SPACECRAFT_ID\": \"LANDSAT_8\",\n", " \"SENSOR_ID\": \"OLI_TIRS\",\n", " \"WRS_TYPE\": \"2\",\n", " \"WRS_PATH\": \"14\",\n", " \"WRS_ROW\": \"32\",\n", " \"NADIR_OFFNADIR\": \"NADIR\",\n", " \"TARGET_WRS_PATH\": \"14\",\n", " \"TARGET_WRS_ROW\": \"32\",\n", " \"DATE_ACQUIRED\": \"2022-12-19\",\n", " \"SCENE_CENTER_TIME\": \"15:40:17.7299160Z\",\n", " \"STATION_ID\": \"LGN\",\n", " \"CLOUD_COVER\": \"43.42\",\n", " \"CLOUD_COVER_LAND\": \"48.41\",\n", " \"IMAGE_QUALITY_OLI\": \"9\",\n", " \"IMAGE_QUALITY_TIRS\": \"9\",\n", " \"SATURATION_BAND_1\": \"N\",\n", " \"SATURATION_BAND_2\": \"Y\",\n", " \"SATURATION_BAND_3\": \"N\",\n", " \"SATURATION_BAND_4\": \"Y\",\n", " \"SATURATION_BAND_5\": \"Y\",\n", " \"SATURATION_BAND_6\": \"Y\",\n", " \"SATURATION_BAND_7\": \"Y\",\n", " \"SATURATION_BAND_8\": \"N\",\n", " \"SATURATION_BAND_9\": \"N\",\n", " \"ROLL_ANGLE\": \"-0.001\",\n", " \"SUN_AZIMUTH\": \"160.86021018\",\n", " \"SUN_ELEVATION\": \"23.81656674\",\n", " \"EARTH_SUN_DISTANCE\": \"0.9839500\",\n", " \"TRUNCATION_OLI\": \"UPPER\",\n", " \"TIRS_SSM_MODEL\": \"FINAL\",\n", " \"TIRS_SSM_POSITION_STATUS\": \"ESTIMATED\"\n", " },\n", " \"PROJECTION_ATTRIBUTES\": {\n", " \"MAP_PROJECTION\": \"UTM\",\n", " \"DATUM\": \"WGS84\",\n", " \"ELLIPSOID\": \"WGS84\",\n", " \"UTM_ZONE\": \"18\",\n", " \"GRID_CELL_SIZE_REFLECTIVE\": \"30.00\",\n", " \"GRID_CELL_SIZE_THERMAL\": \"30.00\",\n", " \"REFLECTIVE_LINES\": \"7861\",\n", " \"REFLECTIVE_SAMPLES\": \"7731\",\n", " \"THERMAL_LINES\": \"7861\",\n", " \"THERMAL_SAMPLES\": \"7731\",\n", " \"ORIENTATION\": \"NORTH_UP\",\n", " \"CORNER_UL_LAT_PRODUCT\": \"41.38441\",\n", " \"CORNER_UL_LON_PRODUCT\": \"-76.26178\",\n", " \"CORNER_UR_LAT_PRODUCT\": \"41.38140\",\n", " \"CORNER_UR_LON_PRODUCT\": \"-73.48833\",\n", " \"CORNER_LL_LAT_PRODUCT\": \"39.26052\",\n", " \"CORNER_LL_LON_PRODUCT\": \"-76.22284\",\n", " \"CORNER_LR_LAT_PRODUCT\": \"39.25773\",\n", " \"CORNER_LR_LON_PRODUCT\": \"-73.53498\",\n", " \"CORNER_UL_PROJECTION_X_PRODUCT\": \"394500.000\",\n", " \"CORNER_UL_PROJECTION_Y_PRODUCT\": \"4582200.000\",\n", " \"CORNER_UR_PROJECTION_X_PRODUCT\": \"626400.000\",\n", " \"CORNER_UR_PROJECTION_Y_PRODUCT\": \"4582200.000\",\n", " \"CORNER_LL_PROJECTION_X_PRODUCT\": \"394500.000\",\n", " \"CORNER_LL_PROJECTION_Y_PRODUCT\": \"4346400.000\",\n", " \"CORNER_LR_PROJECTION_X_PRODUCT\": \"626400.000\",\n", " \"CORNER_LR_PROJECTION_Y_PRODUCT\": \"4346400.000\"\n", " },\n", " \"LEVEL2_PROCESSING_RECORD\": {\n", " \"ORIGIN\": \"Image courtesy of the U.S. Geological Survey\",\n", " \"DIGITAL_OBJECT_IDENTIFIER\": \"https://doi.org/10.5066/P9OGBGM6\",\n", " \"REQUEST_ID\": \"1626123_00008\",\n", " \"LANDSAT_PRODUCT_ID\": \"LC08_L2SP_014032_20221219_20230113_02_T1\",\n", " \"PROCESSING_LEVEL\": \"L2SP\",\n", " \"OUTPUT_FORMAT\": \"GEOTIFF\",\n", " \"DATE_PRODUCT_GENERATED\": \"2023-01-13T02:53:40Z\",\n", " \"PROCESSING_SOFTWARE_VERSION\": \"LPGS_16.1.0\",\n", " \"ALGORITHM_SOURCE_SURFACE_REFLECTANCE\": \"LaSRC_1.5.0\",\n", " \"DATA_SOURCE_OZONE\": \"MODIS\",\n", " \"DATA_SOURCE_PRESSURE\": \"Calculated\",\n", " \"DATA_SOURCE_WATER_VAPOR\": \"MODIS\",\n", " \"DATA_SOURCE_AIR_TEMPERATURE\": \"MODIS\",\n", " \"ALGORITHM_SOURCE_SURFACE_TEMPERATURE\": \"st_1.3.0\",\n", " \"DATA_SOURCE_REANALYSIS\": \"GEOS-5 FP-IT\"\n", " },\n", " \"LEVEL2_SURFACE_REFLECTANCE_PARAMETERS\": {\n", " \"REFLECTANCE_MAXIMUM_BAND_1\": \"1.602213\",\n", " \"REFLECTANCE_MINIMUM_BAND_1\": \"-0.199972\",\n", " \"REFLECTANCE_MAXIMUM_BAND_2\": \"1.602213\",\n", " \"REFLECTANCE_MINIMUM_BAND_2\": \"-0.199972\",\n", " \"REFLECTANCE_MAXIMUM_BAND_3\": \"1.602213\",\n", " \"REFLECTANCE_MINIMUM_BAND_3\": \"-0.199972\",\n", " \"REFLECTANCE_MAXIMUM_BAND_4\": \"1.602213\",\n", " \"REFLECTANCE_MINIMUM_BAND_4\": \"-0.199972\",\n", " \"REFLECTANCE_MAXIMUM_BAND_5\": \"1.602213\",\n", " \"REFLECTANCE_MINIMUM_BAND_5\": \"-0.199972\",\n", " \"REFLECTANCE_MAXIMUM_BAND_6\": \"1.602213\",\n", " \"REFLECTANCE_MINIMUM_BAND_6\": \"-0.199972\",\n", " \"REFLECTANCE_MAXIMUM_BAND_7\": \"1.602213\",\n", " \"REFLECTANCE_MINIMUM_BAND_7\": \"-0.199972\",\n", " \"QUANTIZE_CAL_MAX_BAND_1\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_1\": \"1\",\n", " \"QUANTIZE_CAL_MAX_BAND_2\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_2\": \"1\",\n", " \"QUANTIZE_CAL_MAX_BAND_3\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_3\": \"1\",\n", " \"QUANTIZE_CAL_MAX_BAND_4\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_4\": \"1\",\n", " \"QUANTIZE_CAL_MAX_BAND_5\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_5\": \"1\",\n", " \"QUANTIZE_CAL_MAX_BAND_6\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_6\": \"1\",\n", " \"QUANTIZE_CAL_MAX_BAND_7\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_7\": \"1\",\n", " \"REFLECTANCE_MULT_BAND_1\": \"2.75e-05\",\n", " \"REFLECTANCE_MULT_BAND_2\": \"2.75e-05\",\n", " \"REFLECTANCE_MULT_BAND_3\": \"2.75e-05\",\n", " \"REFLECTANCE_MULT_BAND_4\": \"2.75e-05\",\n", " \"REFLECTANCE_MULT_BAND_5\": \"2.75e-05\",\n", " \"REFLECTANCE_MULT_BAND_6\": \"2.75e-05\",\n", " \"REFLECTANCE_MULT_BAND_7\": \"2.75e-05\",\n", " \"REFLECTANCE_ADD_BAND_1\": \"-0.2\",\n", " \"REFLECTANCE_ADD_BAND_2\": \"-0.2\",\n", " \"REFLECTANCE_ADD_BAND_3\": \"-0.2\",\n", " \"REFLECTANCE_ADD_BAND_4\": \"-0.2\",\n", " \"REFLECTANCE_ADD_BAND_5\": \"-0.2\",\n", " \"REFLECTANCE_ADD_BAND_6\": \"-0.2\",\n", " \"REFLECTANCE_ADD_BAND_7\": \"-0.2\"\n", " },\n", " \"LEVEL2_SURFACE_TEMPERATURE_PARAMETERS\": {\n", " \"TEMPERATURE_MAXIMUM_BAND_ST_B10\": \"372.999941\",\n", " \"TEMPERATURE_MINIMUM_BAND_ST_B10\": \"149.003418\",\n", " \"QUANTIZE_CAL_MAXIMUM_BAND_ST_B10\": \"65535\",\n", " \"QUANTIZE_CAL_MINIMUM_BAND_ST_B10\": \"1\",\n", " \"TEMPERATURE_MULT_BAND_ST_B10\": \"0.00341802\",\n", " \"TEMPERATURE_ADD_BAND_ST_B10\": \"149.0\"\n", " },\n", " \"LEVEL1_PROCESSING_RECORD\": {\n", " \"ORIGIN\": \"Image courtesy of the U.S. Geological Survey\",\n", " \"DIGITAL_OBJECT_IDENTIFIER\": \"https://doi.org/10.5066/P975CC9B\",\n", " \"REQUEST_ID\": \"1626123_00008\",\n", " \"LANDSAT_SCENE_ID\": \"LC80140322022353LGN00\",\n", " \"LANDSAT_PRODUCT_ID\": \"LC08_L1TP_014032_20221219_20230113_02_T1\",\n", " \"PROCESSING_LEVEL\": \"L1TP\",\n", " \"COLLECTION_CATEGORY\": \"T1\",\n", " \"OUTPUT_FORMAT\": \"GEOTIFF\",\n", " \"DATE_PRODUCT_GENERATED\": \"2023-01-13T02:38:55Z\",\n", " \"PROCESSING_SOFTWARE_VERSION\": \"LPGS_16.1.0\",\n", " \"FILE_NAME_BAND_1\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B1.TIF\",\n", " \"FILE_NAME_BAND_2\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B2.TIF\",\n", " \"FILE_NAME_BAND_3\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B3.TIF\",\n", " \"FILE_NAME_BAND_4\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B4.TIF\",\n", " \"FILE_NAME_BAND_5\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B5.TIF\",\n", " \"FILE_NAME_BAND_6\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B6.TIF\",\n", " \"FILE_NAME_BAND_7\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B7.TIF\",\n", " \"FILE_NAME_BAND_8\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B8.TIF\",\n", " \"FILE_NAME_BAND_9\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B9.TIF\",\n", " \"FILE_NAME_BAND_10\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B10.TIF\",\n", " \"FILE_NAME_BAND_11\": \"LC08_L1TP_014032_20221219_20230113_02_T1_B11.TIF\",\n", " \"FILE_NAME_QUALITY_L1_PIXEL\": \"LC08_L1TP_014032_20221219_20230113_02_T1_QA_PIXEL.TIF\",\n", " \"FILE_NAME_QUALITY_L1_RADIOMETRIC_SATURATION\": \"LC08_L1TP_014032_20221219_20230113_02_T1_QA_RADSAT.TIF\",\n", " \"FILE_NAME_ANGLE_COEFFICIENT\": \"LC08_L1TP_014032_20221219_20230113_02_T1_ANG.txt\",\n", " \"FILE_NAME_ANGLE_SENSOR_AZIMUTH_BAND_4\": \"LC08_L1TP_014032_20221219_20230113_02_T1_VAA.TIF\",\n", " \"FILE_NAME_ANGLE_SENSOR_ZENITH_BAND_4\": \"LC08_L1TP_014032_20221219_20230113_02_T1_VZA.TIF\",\n", " \"FILE_NAME_ANGLE_SOLAR_AZIMUTH_BAND_4\": \"LC08_L1TP_014032_20221219_20230113_02_T1_SAA.TIF\",\n", " \"FILE_NAME_ANGLE_SOLAR_ZENITH_BAND_4\": \"LC08_L1TP_014032_20221219_20230113_02_T1_SZA.TIF\",\n", " \"FILE_NAME_METADATA_ODL\": \"LC08_L1TP_014032_20221219_20230113_02_T1_MTL.txt\",\n", " \"FILE_NAME_METADATA_XML\": \"LC08_L1TP_014032_20221219_20230113_02_T1_MTL.xml\",\n", " \"FILE_NAME_CPF\": \"LC08CPF_20221001_20221231_02.03\",\n", " \"FILE_NAME_BPF_OLI\": \"LO8BPF20221219152831_20221219170353.01\",\n", " \"FILE_NAME_BPF_TIRS\": \"LT8BPF20221215135451_20221222101440.01\",\n", " \"FILE_NAME_RLUT\": \"LC08RLUT_20150303_20431231_02_01.h5\",\n", " \"DATA_SOURCE_TIRS_STRAY_LIGHT_CORRECTION\": \"TIRS\",\n", " \"DATA_SOURCE_ELEVATION\": \"GLS2000\",\n", " \"GROUND_CONTROL_POINTS_VERSION\": \"5\",\n", " \"GROUND_CONTROL_POINTS_MODEL\": \"462\",\n", " \"GEOMETRIC_RMSE_MODEL\": \"8.179\",\n", " \"GEOMETRIC_RMSE_MODEL_Y\": \"7.213\",\n", " \"GEOMETRIC_RMSE_MODEL_X\": \"3.856\",\n", " \"GROUND_CONTROL_POINTS_VERIFY\": \"120\",\n", " \"GEOMETRIC_RMSE_VERIFY\": \"7.426\"\n", " },\n", " \"LEVEL1_MIN_MAX_RADIANCE\": {\n", " \"RADIANCE_MAXIMUM_BAND_1\": \"785.06079\",\n", " \"RADIANCE_MINIMUM_BAND_1\": \"-64.83057\",\n", " \"RADIANCE_MAXIMUM_BAND_2\": \"803.91187\",\n", " \"RADIANCE_MINIMUM_BAND_2\": \"-66.38730\",\n", " \"RADIANCE_MAXIMUM_BAND_3\": \"740.79791\",\n", " \"RADIANCE_MINIMUM_BAND_3\": \"-61.17533\",\n", " \"RADIANCE_MAXIMUM_BAND_4\": \"624.68250\",\n", " \"RADIANCE_MINIMUM_BAND_4\": \"-51.58648\",\n", " \"RADIANCE_MAXIMUM_BAND_5\": \"382.27454\",\n", " \"RADIANCE_MINIMUM_BAND_5\": \"-31.56836\",\n", " \"RADIANCE_MAXIMUM_BAND_6\": \"95.06820\",\n", " \"RADIANCE_MINIMUM_BAND_6\": \"-7.85076\",\n", " \"RADIANCE_MAXIMUM_BAND_7\": \"32.04307\",\n", " \"RADIANCE_MINIMUM_BAND_7\": \"-2.64613\",\n", " \"RADIANCE_MAXIMUM_BAND_8\": \"706.96869\",\n", " \"RADIANCE_MINIMUM_BAND_8\": \"-58.38170\",\n", " \"RADIANCE_MAXIMUM_BAND_9\": \"149.40157\",\n", " \"RADIANCE_MINIMUM_BAND_9\": \"-12.33763\",\n", " \"RADIANCE_MAXIMUM_BAND_10\": \"22.00180\",\n", " \"RADIANCE_MINIMUM_BAND_10\": \"0.10033\",\n", " \"RADIANCE_MAXIMUM_BAND_11\": \"22.00180\",\n", " \"RADIANCE_MINIMUM_BAND_11\": \"0.10033\"\n", " },\n", " \"LEVEL1_MIN_MAX_REFLECTANCE\": {\n", " \"REFLECTANCE_MAXIMUM_BAND_1\": \"1.210700\",\n", " \"REFLECTANCE_MINIMUM_BAND_1\": \"-0.099980\",\n", " \"REFLECTANCE_MAXIMUM_BAND_2\": \"1.210700\",\n", " \"REFLECTANCE_MINIMUM_BAND_2\": \"-0.099980\",\n", " \"REFLECTANCE_MAXIMUM_BAND_3\": \"1.210700\",\n", " \"REFLECTANCE_MINIMUM_BAND_3\": \"-0.099980\",\n", " \"REFLECTANCE_MAXIMUM_BAND_4\": \"1.210700\",\n", " \"REFLECTANCE_MINIMUM_BAND_4\": \"-0.099980\",\n", " \"REFLECTANCE_MAXIMUM_BAND_5\": \"1.210700\",\n", " \"REFLECTANCE_MINIMUM_BAND_5\": \"-0.099980\",\n", " \"REFLECTANCE_MAXIMUM_BAND_6\": \"1.210700\",\n", " \"REFLECTANCE_MINIMUM_BAND_6\": \"-0.099980\",\n", " \"REFLECTANCE_MAXIMUM_BAND_7\": \"1.210700\",\n", " \"REFLECTANCE_MINIMUM_BAND_7\": \"-0.099980\",\n", " \"REFLECTANCE_MAXIMUM_BAND_8\": \"1.210700\",\n", " \"REFLECTANCE_MINIMUM_BAND_8\": \"-0.099980\",\n", " \"REFLECTANCE_MAXIMUM_BAND_9\": \"1.210700\",\n", " \"REFLECTANCE_MINIMUM_BAND_9\": \"-0.099980\"\n", " },\n", " \"LEVEL1_MIN_MAX_PIXEL_VALUE\": {\n", " \"QUANTIZE_CAL_MAX_BAND_1\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_1\": \"1\",\n", " \"QUANTIZE_CAL_MAX_BAND_2\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_2\": \"1\",\n", " \"QUANTIZE_CAL_MAX_BAND_3\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_3\": \"1\",\n", " \"QUANTIZE_CAL_MAX_BAND_4\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_4\": \"1\",\n", " \"QUANTIZE_CAL_MAX_BAND_5\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_5\": \"1\",\n", " \"QUANTIZE_CAL_MAX_BAND_6\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_6\": \"1\",\n", " \"QUANTIZE_CAL_MAX_BAND_7\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_7\": \"1\",\n", " \"QUANTIZE_CAL_MAX_BAND_8\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_8\": \"1\",\n", " \"QUANTIZE_CAL_MAX_BAND_9\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_9\": \"1\",\n", " \"QUANTIZE_CAL_MAX_BAND_10\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_10\": \"1\",\n", " \"QUANTIZE_CAL_MAX_BAND_11\": \"65535\",\n", " \"QUANTIZE_CAL_MIN_BAND_11\": \"1\"\n", " },\n", " \"LEVEL1_RADIOMETRIC_RESCALING\": {\n", " \"RADIANCE_MULT_BAND_1\": \"1.2969E-02\",\n", " \"RADIANCE_MULT_BAND_2\": \"1.3280E-02\",\n", " \"RADIANCE_MULT_BAND_3\": \"1.2238E-02\",\n", " \"RADIANCE_MULT_BAND_4\": \"1.0319E-02\",\n", " \"RADIANCE_MULT_BAND_5\": \"6.3149E-03\",\n", " \"RADIANCE_MULT_BAND_6\": \"1.5705E-03\",\n", " \"RADIANCE_MULT_BAND_7\": \"5.2933E-04\",\n", " \"RADIANCE_MULT_BAND_8\": \"1.1679E-02\",\n", " \"RADIANCE_MULT_BAND_9\": \"2.4680E-03\",\n", " \"RADIANCE_MULT_BAND_10\": \"3.3420E-04\",\n", " \"RADIANCE_MULT_BAND_11\": \"3.3420E-04\",\n", " \"RADIANCE_ADD_BAND_1\": \"-64.84355\",\n", " \"RADIANCE_ADD_BAND_2\": \"-66.40058\",\n", " \"RADIANCE_ADD_BAND_3\": \"-61.18757\",\n", " \"RADIANCE_ADD_BAND_4\": \"-51.59680\",\n", " \"RADIANCE_ADD_BAND_5\": \"-31.57467\",\n", " \"RADIANCE_ADD_BAND_6\": \"-7.85233\",\n", " \"RADIANCE_ADD_BAND_7\": \"-2.64666\",\n", " \"RADIANCE_ADD_BAND_8\": \"-58.39338\",\n", " \"RADIANCE_ADD_BAND_9\": \"-12.34010\",\n", " \"RADIANCE_ADD_BAND_10\": \"0.10000\",\n", " \"RADIANCE_ADD_BAND_11\": \"0.10000\",\n", " \"REFLECTANCE_MULT_BAND_1\": \"2.0000E-05\",\n", " \"REFLECTANCE_MULT_BAND_2\": \"2.0000E-05\",\n", " \"REFLECTANCE_MULT_BAND_3\": \"2.0000E-05\",\n", " \"REFLECTANCE_MULT_BAND_4\": \"2.0000E-05\",\n", " \"REFLECTANCE_MULT_BAND_5\": \"2.0000E-05\",\n", " \"REFLECTANCE_MULT_BAND_6\": \"2.0000E-05\",\n", " \"REFLECTANCE_MULT_BAND_7\": \"2.0000E-05\",\n", " \"REFLECTANCE_MULT_BAND_8\": \"2.0000E-05\",\n", " \"REFLECTANCE_MULT_BAND_9\": \"2.0000E-05\",\n", " \"REFLECTANCE_ADD_BAND_1\": \"-0.100000\",\n", " \"REFLECTANCE_ADD_BAND_2\": \"-0.100000\",\n", " \"REFLECTANCE_ADD_BAND_3\": \"-0.100000\",\n", " \"REFLECTANCE_ADD_BAND_4\": \"-0.100000\",\n", " \"REFLECTANCE_ADD_BAND_5\": \"-0.100000\",\n", " \"REFLECTANCE_ADD_BAND_6\": \"-0.100000\",\n", " \"REFLECTANCE_ADD_BAND_7\": \"-0.100000\",\n", " \"REFLECTANCE_ADD_BAND_8\": \"-0.100000\",\n", " \"REFLECTANCE_ADD_BAND_9\": \"-0.100000\"\n", " },\n", " \"LEVEL1_THERMAL_CONSTANTS\": {\n", " \"K1_CONSTANT_BAND_10\": \"774.8853\",\n", " \"K2_CONSTANT_BAND_10\": \"1321.0789\",\n", " \"K1_CONSTANT_BAND_11\": \"480.8883\",\n", " \"K2_CONSTANT_BAND_11\": \"1201.1442\"\n", " },\n", " \"LEVEL1_PROJECTION_PARAMETERS\": {\n", " \"MAP_PROJECTION\": \"UTM\",\n", " \"DATUM\": \"WGS84\",\n", " \"ELLIPSOID\": \"WGS84\",\n", " \"UTM_ZONE\": \"18\",\n", " \"GRID_CELL_SIZE_PANCHROMATIC\": \"15.00\",\n", " \"GRID_CELL_SIZE_REFLECTIVE\": \"30.00\",\n", " \"GRID_CELL_SIZE_THERMAL\": \"30.00\",\n", " \"ORIENTATION\": \"NORTH_UP\",\n", " \"RESAMPLING_OPTION\": \"CUBIC_CONVOLUTION\"\n", " },\n", " \"ORIGINAL_URL\": \"https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_MTL.xml\"\n", "}\n" ] } ], "source": [ "metadata = get_metadata(scene_mtls[0])\n", "print(json.dumps(metadata, indent=4))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There are a number of files referred to by this metadata file which are in the same tree in the cloud. We must provide an easy means for creating a URL for these sidecar files. We can then use `partial` to create a helper function to turn a file name into a URL." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "def download_sidecar(metadata: Dict[str, Any], filename: str) -> str:\n", " parsed = urlparse(metadata[\"ORIGINAL_URL\"])\n", " return urlunparse(parsed._replace(path=join(dirname(parsed.path), filename)))" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "download_url = partial(download_sidecar, metadata)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create a STAC Item from a scene\n", "\n", "Now that we have metadata for the scene let's use it to create a [STAC Item](https://github.com/radiantearth/stac-spec/blob/master/item-spec/item-spec.md).\n", "\n", "We can use the `help` method to see the signature of the `__init__` method on `pystac.Item`. You can also call `help` directly on `pystac.Item` for broader documentation, or check the [API docs for Item here](https://pystac.readthedocs.io/en/latest/api.html#item)." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Help on function __init__ in module pystac.item:\n", "\n", "__init__(self, id: 'str', geometry: 'Optional[Dict[str, Any]]', bbox: 'Optional[List[float]]', datetime: 'Optional[Datetime]', properties: 'Dict[str, Any]', start_datetime: 'Optional[Datetime]' = None, end_datetime: 'Optional[Datetime]' = None, stac_extensions: 'Optional[List[str]]' = None, href: 'Optional[str]' = None, collection: 'Optional[Union[str, Collection]]' = None, extra_fields: 'Optional[Dict[str, Any]]' = None, assets: 'Optional[Dict[str, Asset]]' = None)\n", " Initialize self. See help(type(self)) for accurate signature.\n", "\n" ] } ], "source": [ "help(pystac.Item.__init__)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see we'll need at least an `id`, `geometry`, `bbox`, and `datetime`. Properties is required, but can be an empty dictionary that we fill out on the Item once it's created.\n", "\n", "> Caution! The `Optional` type hint is used when None can be provided in place of a meaningful argument; it does not indicate that the argument does not need to be supplied—that is only true if a default value is indicated in the type signature." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Item `id`\n", "\n", "For the Item's `id`, we'll use the scene ID. We'll chop off the last 5 characters as they are repeated for each ID and so aren't necessary: " ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "def get_item_id(metadata: Dict[str, Any]) -> str:\n", " return cast(str, metadata[\"LEVEL1_PROCESSING_RECORD\"][\"LANDSAT_SCENE_ID\"][:-5])" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'LC80140322022353'" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item_id = get_item_id(metadata)\n", "item_id" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Item `datetime`\n", "\n", "Here we parse the datetime of the Item from two metadata fields that describe the date and time the scene was captured:" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "def get_datetime(metadata: Dict[str, Any]) -> datetime:\n", " return parse(\n", " \"%sT%s\"\n", " % (\n", " metadata[\"IMAGE_ATTRIBUTES\"][\"DATE_ACQUIRED\"],\n", " metadata[\"IMAGE_ATTRIBUTES\"][\"SCENE_CENTER_TIME\"],\n", " )\n", " )" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "datetime.datetime(2022, 12, 19, 15, 40, 17, 729916, tzinfo=tzutc())" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item_datetime = get_datetime(metadata)\n", "item_datetime" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Item `bbox`\n", "\n", "Here we read in the bounding box information from the scene and transform it into the format of the Item's `bbox` property:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "def get_bbox(metadata: Dict[str, Any]) -> List[float]:\n", " metadata = metadata[\"PROJECTION_ATTRIBUTES\"]\n", " coords = [\n", " [\n", " [\n", " float(metadata[\"CORNER_UL_LON_PRODUCT\"]),\n", " float(metadata[\"CORNER_UL_LAT_PRODUCT\"]),\n", " ],\n", " [\n", " float(metadata[\"CORNER_UR_LON_PRODUCT\"]),\n", " float(metadata[\"CORNER_UR_LAT_PRODUCT\"]),\n", " ],\n", " [\n", " float(metadata[\"CORNER_LR_LON_PRODUCT\"]),\n", " float(metadata[\"CORNER_LR_LAT_PRODUCT\"]),\n", " ],\n", " [\n", " float(metadata[\"CORNER_LL_LON_PRODUCT\"]),\n", " float(metadata[\"CORNER_LL_LAT_PRODUCT\"]),\n", " ],\n", " [\n", " float(metadata[\"CORNER_UL_LON_PRODUCT\"]),\n", " float(metadata[\"CORNER_UL_LAT_PRODUCT\"]),\n", " ],\n", " ]\n", " ]\n", " lats = [c[1] for c in coords[0]]\n", " lons = [c[0] for c in coords[0]]\n", " return [min(lons), min(lats), max(lons), max(lats)]" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[-76.26178, 39.25773, -73.48833, 41.38441]" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item_bbox = get_bbox(metadata)\n", "item_bbox" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Item `geometry`\n", "\n", "Getting the geometry of the scene is a little more tricky. The bounding box will be a axis-aligned rectangle of the area the scene occupies, but will not represent the true footprint of the image - Landsat scenes are \"tilted\" according the the coordinate reference system, so there will be areas in the corner where no image data exists. When constructing a STAC Item it's best if you have the Item geometry represent the true footprint of the assets.\n", "\n", "To get the footprint of the scene we'll read in another metadata file that lives alongside the MTL - the `ANG.txt` file. This function uses the ANG file and the bbox to construct the GeoJSON polygon that represents the footprint of the scene:" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "def get_geometry(metadata: Dict[str, Any], bbox: List[float]) -> Dict[str, Any]:\n", " url = download_sidecar(\n", " metadata, metadata[\"PRODUCT_CONTENTS\"][\"FILE_NAME_ANGLE_COEFFICIENT\"]\n", " )\n", " sz = []\n", " coords = []\n", " ang_text = stac_io.read_text(pc.sign(url))\n", " if not ang_text.startswith(\"GROUP\"):\n", " raise ValueError(f\"ANG file for url {url} is incorrectly formatted\")\n", " for line in ang_text.split(\"\\n\"):\n", " if \"BAND01_NUM_L1T_LINES\" in line or \"BAND01_NUM_L1T_SAMPS\" in line:\n", " sz.append(float(line.split(\"=\")[1]))\n", " if (\n", " \"BAND01_L1T_IMAGE_CORNER_LINES\" in line\n", " or \"BAND01_L1T_IMAGE_CORNER_SAMPS\" in line\n", " ):\n", " coords.append(\n", " [float(l) for l in line.split(\"=\")[1].strip().strip(\"()\").split(\",\")]\n", " )\n", " if len(coords) == 2:\n", " break\n", " dlon = bbox[2] - bbox[0]\n", " dlat = bbox[3] - bbox[1]\n", " lons = [c / sz[1] * dlon + bbox[0] for c in coords[1]]\n", " lats = [((sz[0] - c) / sz[0]) * dlat + bbox[1] for c in coords[0]]\n", " coordinates = [\n", " [\n", " [lons[0], lats[0]],\n", " [lons[1], lats[1]],\n", " [lons[2], lats[2]],\n", " [lons[3], lats[3]],\n", " [lons[0], lats[0]],\n", " ]\n", " ]\n", "\n", " return {\"type\": \"Polygon\", \"coordinates\": coordinates}" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " \"type\": \"Polygon\",\n", " \"coordinates\": [\n", " [\n", " [\n", " -75.71075270108336,\n", " 41.3823086369878\n", " ],\n", " [\n", " -73.48924866988654,\n", " 40.980654308234485\n", " ],\n", " [\n", " -74.0425618957281,\n", " 39.25823722657151\n", " ],\n", " [\n", " -76.26093009667797,\n", " 39.66800780107756\n", " ],\n", " [\n", " -75.71075270108336,\n", " 41.3823086369878\n", " ]\n", " ]\n", " ]\n", "}\n" ] } ], "source": [ "item_geometry = get_geometry(metadata, item_bbox)\n", "print(json.dumps(item_geometry, indent=2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This would be a good time to check our work - we can print out the GeoJSON and use [geojson.io](https://geojson.io/) to check and make sure we're using scenes that overlap our location. If this footprint is somewhere unexpected in the world, make sure the Lat/Long coordinates are correct and in the right order!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Create the item\n", "\n", "Now that we have the required attributes for an Item we can create it:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "item = pystac.Item(\n", " id=item_id,\n", " datetime=item_datetime,\n", " geometry=item_geometry,\n", " bbox=item_bbox,\n", " properties={},\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "PySTAC has a `validate` method on STAC objects, which you can use to make sure you're constructing things correctly. If there's an issue the following line will throw an exception:" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json']" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.validate()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Add Ground Sample Distance to common metadata\n", "\n", "We'll add the Ground Sample Distance that is defined as part of the Item [Common Metadata](https://github.com/radiantearth/stac-spec/blob/master/item-spec/common-metadata.md). We define this on the Item level as 30 meters, which is the GSD for most of the Landsat bands. However, if some bands have a different resolution; we can account for this by setting the GSD explicitly for each of those bands below." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "item.common_metadata.gsd = 30.0" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Adding the EO extension\n", "\n", "STAC has a rich [set of extensions](https://stac-extensions.github.io/) that allow STAC objects to encode information that is not part of the core spec but is used widely and standardized. These extensions allow us to augment STAC objects with additional structured metadata that describe referenced data with semantically-meaningful fields. An example of this is the [eo extension](https://github.com/stac-extensions/eo), which captures fields needed for electro-optical data, like center wavelength and full-width half maximum values.\n", "\n", "This notebook will also rely on other extensions; but as they will apply to different objects, not just the item itself, they will be invoked later.\n", "\n", "For now, we will enable the EO extension for this item by using the `ext` property provided by the extension object:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "eo_ext = EOExtension.ext(item, add_if_missing=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Add cloud cover\n", "\n", "Here we add cloud cover from the metadata as part of the `eo` extension." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "def get_cloud_cover(metadata: Dict[str, Any]) -> float:\n", " return float(metadata[\"IMAGE_ATTRIBUTES\"][\"CLOUD_COVER\"])" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "43.42" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "eo_ext.cloud_cover = get_cloud_cover(metadata)\n", "eo_ext.cloud_cover" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Adding assets\n", "\n", "STAC Items contain a list of [Assets](https://github.com/radiantearth/stac-spec/blob/master/item-spec/item-spec.md#asset-object), which are a list of files that relate to the Item. In our case we'll be cataloging each file related to the scene, including the Landsat band files as well as the metadata files associated with the scene.\n", "\n", "Each asset will have a name, some basic properties, and then possibly some properties defined by the various extensions in use (`eo`, `raster`, and `classification`). So, we begin by defining a type alias for this package of information and some helper functions for creating them:" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [], "source": [ "class BandInfo(TypedDict):\n", " name: str\n", " asset_fields: Dict[str, str]\n", " extensions: List[Union[EOBand, RasterBand, List[Bitfield]]]\n", "\n", "\n", "def eo_band_info(\n", " common_name: str,\n", " href: str,\n", " name: str,\n", " description: str,\n", " center: float,\n", " fwhm: float,\n", " default_raster_band: Optional[RasterBand] = None,\n", "):\n", " raster_band = (\n", " RasterBand.create(\n", " spatial_resolution=30.0,\n", " scale=0.0000275,\n", " nodata=0,\n", " offset=-0.2,\n", " data_type=\"uint16\",\n", " )\n", " if default_raster_band is None\n", " else default_raster_band\n", " )\n", " return {\n", " \"name\": common_name,\n", " \"asset_fields\": {\n", " \"href\": href,\n", " \"media_type\": str(pystac.media_type.MediaType.COG),\n", " },\n", " \"extensions\": [\n", " EOBand.create(\n", " name=name,\n", " common_name=common_name,\n", " description=description,\n", " center_wavelength=center,\n", " full_width_half_max=fwhm,\n", " ),\n", " ],\n", " }\n", "\n", "\n", "def plain_band_info(name: str, href: str, title: str, ext: RasterBand):\n", " return {\n", " \"name\": name,\n", " \"asset_fields\": {\n", " \"href\": href,\n", " \"media_type\": str(pystac.media_type.MediaType.COG),\n", " \"title\": title,\n", " },\n", " \"extensions\": [ext],\n", " }" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Some common raster band information definitions will also be useful." ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "thermal_raster_band = RasterBand.create(\n", " spatial_resolution=30.0,\n", " scale=0.00341802,\n", " nodata=0,\n", " offset=149.0,\n", " data_type=\"uint6\",\n", " unit=\"kelvin\",\n", ")\n", "radiance_raster_band = RasterBand.create(\n", " unit=\"watt/steradian/square_meter/micrometer\",\n", " scale=1e-3,\n", " nodata=-9999,\n", " data_type=\"uint16\",\n", " spatial_resolution=30.0,\n", ")\n", "emissivity_transmission_raster_band = RasterBand.create(\n", " scale=1e-4,\n", " nodata=-9999,\n", " data_type=\"int16\",\n", " spatial_resolution=30.0,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Several QA bands are provided that utilize bit-wise masks which we can define using the [`classification` extension](https://github.com/stac-extensions/classification). Because these definitions can be verbose, we provide some additional helper functions to minimize the length of their definition." ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [], "source": [ "def create_bitfield(\n", " offset: int,\n", " length: int,\n", " name: str,\n", " field_names_descriptions: List[Tuple[str, str]],\n", " description: Optional[str] = None,\n", ") -> Bitfield:\n", " return Bitfield.create(\n", " offset=offset,\n", " length=length,\n", " name=name,\n", " description=description,\n", " classes=[\n", " Classification.create(value=i, name=n, description=d)\n", " for (i, (n, d)) in enumerate(field_names_descriptions)\n", " ],\n", " )\n", "\n", "\n", "def create_qa_bitfield(\n", " offset: int,\n", " class_name: str,\n", " description: Optional[Union[str, Tuple[str, str]]] = None,\n", ") -> Bitfield:\n", " if description is None:\n", " descr0 = f\"{class_name.replace('_', ' ').capitalize()} confidence is not high\"\n", " descr1 = f\"High confidence {class_name.replace('_', ' ')}\"\n", " elif isinstance(description, str):\n", " descr0 = f\"{description.capitalize()} confidence is not high\"\n", " descr1 = f\"High confidence {description.lower()}\"\n", " else:\n", " descr0 = description[0]\n", " descr1 = description[1]\n", "\n", " return create_bitfield(\n", " offset, 1, class_name, [(f\"not_{class_name}\", descr0), (class_name, descr1)]\n", " )\n", "\n", "\n", "def create_confidence_bitfield(\n", " offset: int, class_name: str, use_medium: bool = False\n", ") -> Bitfield:\n", " label = class_name.replace(\"_\", \" \")\n", " return create_bitfield(\n", " offset,\n", " 2,\n", " f\"{class_name}_confidence\",\n", " [\n", " (\"not_set\", \"No confidence level set\"),\n", " (\"low\", f\"Low confidence {label}\"),\n", " (\"medium\", f\"Medium confidence {label}\")\n", " if use_medium\n", " else (\"reserved\", \"Reserved - value not used\"),\n", " (\"high\", f\"High confidence {label}\"),\n", " ],\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can now create the `BandInfo` definitions for the Landsat scenes. This begins with the definition of a function to convert metadata into a list of `BandInfo` records, which is lengthy but ultimately straightforward." ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [], "source": [ "def landsat_band_info(\n", " metadata: Dict[str, Any], downloader: Callable[[str], str]\n", ") -> List[BandInfo]:\n", " product_contents = metadata[\"PRODUCT_CONTENTS\"]\n", " return [\n", " {\n", " \"name\": \"ang\",\n", " \"asset_fields\": {\n", " \"href\": downloader(product_contents[\"FILE_NAME_ANGLE_COEFFICIENT\"]),\n", " \"media_type\": \"text/plain\",\n", " \"title\": \"Angle coefficients\",\n", " },\n", " \"extensions\": [],\n", " },\n", " {\n", " \"name\": \"mtl.txt\",\n", " \"asset_fields\": {\n", " \"href\": downloader(product_contents[\"FILE_NAME_METADATA_ODL\"]),\n", " \"media_type\": \"text/plain\",\n", " \"title\": \"Product metadata\",\n", " },\n", " \"extensions\": [],\n", " },\n", " eo_band_info(\n", " \"coastal\",\n", " downloader(product_contents[\"FILE_NAME_BAND_1\"]),\n", " \"OLI_B1\",\n", " \"Coastal/Aerosol (Operational Land Imager)\",\n", " 0.44,\n", " 0.02,\n", " ),\n", " eo_band_info(\n", " \"blue\",\n", " downloader(product_contents[\"FILE_NAME_BAND_2\"]),\n", " \"OLI_B2\",\n", " \"Visible blue (Operational Land Imager)\",\n", " 0.48,\n", " 0.06,\n", " ),\n", " eo_band_info(\n", " \"green\",\n", " downloader(product_contents[\"FILE_NAME_BAND_3\"]),\n", " \"OLI_B3\",\n", " \"Visible green (Operational Land Imager)\",\n", " 0.56,\n", " 0.06,\n", " ),\n", " eo_band_info(\n", " \"red\",\n", " downloader(product_contents[\"FILE_NAME_BAND_4\"]),\n", " \"OLI_B4\",\n", " \"Visible red (Operational Land Imager)\",\n", " 0.65,\n", " 0.04,\n", " ),\n", " eo_band_info(\n", " \"nir08\",\n", " downloader(product_contents[\"FILE_NAME_BAND_5\"]),\n", " \"OLI_B5\",\n", " \"Near infrared (Operational Land Imager)\",\n", " 0.87,\n", " 0.03,\n", " ),\n", " eo_band_info(\n", " \"swir16\",\n", " downloader(product_contents[\"FILE_NAME_BAND_6\"]),\n", " \"OLI_B6\",\n", " \"Short-wave infrared (Operational Land Imager)\",\n", " 1.61,\n", " 0.09,\n", " ),\n", " eo_band_info(\n", " \"swir22\",\n", " downloader(product_contents[\"FILE_NAME_BAND_7\"]),\n", " \"OLI_B7\",\n", " \"Short-wave infrared (Operational Land Imager)\",\n", " 2.2,\n", " 0.19,\n", " ),\n", " eo_band_info(\n", " \"lwir11\",\n", " downloader(product_contents[\"FILE_NAME_BAND_ST_B10\"]),\n", " \"TIRS_B10\",\n", " \"Long-wave infrared (Thermal InfraRed Sensor)\",\n", " 10.9,\n", " 0.59,\n", " thermal_raster_band,\n", " ),\n", " plain_band_info(\n", " \"trad\",\n", " downloader(product_contents[\"FILE_NAME_THERMAL_RADIANCE\"]),\n", " \"Thermal radiance\",\n", " radiance_raster_band,\n", " ),\n", " plain_band_info(\n", " \"urad\",\n", " downloader(product_contents[\"FILE_NAME_UPWELL_RADIANCE\"]),\n", " \"Upwelled radiance\",\n", " radiance_raster_band,\n", " ),\n", " plain_band_info(\n", " \"drad\",\n", " downloader(product_contents[\"FILE_NAME_DOWNWELL_RADIANCE\"]),\n", " \"Downwelled radiance\",\n", " radiance_raster_band,\n", " ),\n", " plain_band_info(\n", " \"emis\",\n", " downloader(product_contents[\"FILE_NAME_EMISSIVITY\"]),\n", " \"Emissivity\",\n", " emissivity_transmission_raster_band,\n", " ),\n", " plain_band_info(\n", " \"emsd\",\n", " downloader(product_contents[\"FILE_NAME_EMISSIVITY_STDEV\"]),\n", " \"Emissivity standard deviation\",\n", " emissivity_transmission_raster_band,\n", " ),\n", " plain_band_info(\n", " \"atran\",\n", " downloader(product_contents[\"FILE_NAME_ATMOSPHERIC_TRANSMITTANCE\"]),\n", " \"Atmospheric transmission\",\n", " emissivity_transmission_raster_band,\n", " ),\n", " plain_band_info(\n", " \"cdist\",\n", " downloader(product_contents[\"FILE_NAME_CLOUD_DISTANCE\"]),\n", " \"Cloud distance\",\n", " RasterBand.create(\n", " unit=\"kilometer\",\n", " scale=1e-2,\n", " nodata=-9999,\n", " data_type=\"uint16\",\n", " spatial_resolution=30.0,\n", " ),\n", " ),\n", " {\n", " \"name\": \"qa\",\n", " \"asset_fields\": {\n", " \"href\": downloader(\n", " product_contents[\"FILE_NAME_QUALITY_L2_SURFACE_TEMPERATURE\"]\n", " ),\n", " \"title\": \"Surface Temperature Quality Assessment Band\",\n", " },\n", " \"extensions\": [\n", " RasterBand.create(\n", " unit=\"kelvin\",\n", " scale=1e-2,\n", " nodata=-9999,\n", " data_type=\"int16\",\n", " spatial_resolution=30,\n", " )\n", " ],\n", " },\n", " {\n", " \"name\": \"qa_pixel\",\n", " \"asset_fields\": {\n", " \"href\": downloader(product_contents[\"FILE_NAME_QUALITY_L1_PIXEL\"]),\n", " \"media_type\": str(pystac.media_type.MediaType.COG),\n", " \"title\": \"Pixel quality assessment\",\n", " },\n", " \"extensions\": [\n", " [\n", " create_qa_bitfield(0, \"fill\", (\"Image data\", \"Fill data\")),\n", " create_qa_bitfield(\n", " 1,\n", " \"dilated_cloud\",\n", " (\"Cloud is not dilated or no cloud\", \"Dilated cloud\"),\n", " ),\n", " create_qa_bitfield(2, \"cirrus\"),\n", " create_qa_bitfield(3, \"cloud\"),\n", " create_qa_bitfield(4, \"cloud_shadow\"),\n", " create_qa_bitfield(5, \"snow\"),\n", " create_qa_bitfield(6, \"clear\"),\n", " create_qa_bitfield(7, \"water\"),\n", " create_confidence_bitfield(8, \"cloud\", True),\n", " create_confidence_bitfield(10, \"cloud_shadow\"),\n", " create_confidence_bitfield(12, \"snow\"),\n", " create_confidence_bitfield(14, \"cirrus\"),\n", " ]\n", " ],\n", " },\n", " {\n", " \"name\": \"qa_radsat\",\n", " \"asset_fields\": {\n", " \"href\": downloader(\n", " product_contents[\"FILE_NAME_QUALITY_L1_RADIOMETRIC_SATURATION\"]\n", " ),\n", " \"media_type\": str(pystac.media_type.MediaType.COG),\n", " \"description\": \"Collection 2 Level-1 Radiometric Saturation and Terrain Occlusion Quality Assessment Band (QA_RADSAT)\",\n", " },\n", " \"extensions\": [\n", " [\n", " Bitfield.create(\n", " offset=i - 1,\n", " length=1,\n", " description=f\"Band {i} radiometric saturation\",\n", " classes=[\n", " Classification.create(\n", " 0, f\"Band {i} not saturated\", \"not_saturated\"\n", " ),\n", " Classification.create(\n", " 1, f\"Band {i} saturated\", \"saturated\"\n", " ),\n", " ],\n", " )\n", " for i in [1, 2, 3, 4, 5, 6, 7, 9]\n", " ]\n", " + [\n", " Bitfield.create(\n", " offset=11,\n", " length=1,\n", " description=\"Terrain not visible from sensor due to intervening terrain\",\n", " classes=[\n", " Classification.create(\n", " 0, \"Terrain is not occluded\", \"not_occluded\"\n", " ),\n", " Classification.create(1, \"Terrain is occluded\", \"occluded\"),\n", " ],\n", " )\n", " ]\n", " ],\n", " },\n", " {\n", " \"name\": \"qa_aerosol\",\n", " \"asset_fields\": {\n", " \"href\": downloader(product_contents[\"FILE_NAME_QUALITY_L2_AEROSOL\"]),\n", " \"media_type\": str(pystac.media_type.MediaType.COG),\n", " \"title\": \"Aerosol Quality Assessment Band\",\n", " },\n", " \"extensions\": [\n", " [\n", " create_bitfield(\n", " 0,\n", " 1,\n", " \"fill\",\n", " [(\"not_fill\", \"Pixel is not fill\"), (\"fill\", \"Pixel is fill\")],\n", " \"Image or fill data\",\n", " ),\n", " create_bitfield(\n", " 1,\n", " 1,\n", " \"retrieval\",\n", " [\n", " (\"not_valid\", \"Pixel retrieval is not valid\"),\n", " (\"valid\", \"Pixel retrieval is valid\"),\n", " ],\n", " \"Valid aerosol retrieval\",\n", " ),\n", " create_bitfield(\n", " 2,\n", " 1,\n", " \"water\",\n", " [\n", " (\"not_water\", \"Pixel is not water\"),\n", " (\"water\", \"Pixel is water\"),\n", " ],\n", " \"Water mask\",\n", " ),\n", " create_bitfield(\n", " 5,\n", " 1,\n", " \"interpolated\",\n", " [\n", " (\"not_interpolated\", \"Pixel is not interpolated\"),\n", " (\"interpolated\", \"Pixel is interpolated\"),\n", " ],\n", " \"Aerosol interpolation\",\n", " ),\n", " create_bitfield(\n", " 6,\n", " 2,\n", " \"level\",\n", " [\n", " (\"climatology\", \"No aerosol correction applied\"),\n", " (\"low\", \"Low aerosol level\"),\n", " (\"medium\", \"Medium aerosol level\"),\n", " (\"high\", \"High aerosol level\"),\n", " ],\n", " \"Aerosol level\",\n", " ),\n", " ]\n", " ],\n", " },\n", " ]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For illustration purposes, we can look at the band info records for an example scene:" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[{'name': 'ang',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ANG.txt',\n", " 'media_type': 'text/plain',\n", " 'title': 'Angle coefficients'},\n", " 'extensions': []},\n", " {'name': 'mtl.txt',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_MTL.txt',\n", " 'media_type': 'text/plain',\n", " 'title': 'Product metadata'},\n", " 'extensions': []},\n", " {'name': 'coastal',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_B1.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized'},\n", " 'extensions': []},\n", " {'name': 'blue',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_B2.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized'},\n", " 'extensions': []},\n", " {'name': 'green',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_B3.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized'},\n", " 'extensions': []},\n", " {'name': 'red',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_B4.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized'},\n", " 'extensions': []},\n", " {'name': 'nir08',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_B5.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized'},\n", " 'extensions': []},\n", " {'name': 'swir16',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_B6.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized'},\n", " 'extensions': []},\n", " {'name': 'swir22',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_B7.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized'},\n", " 'extensions': []},\n", " {'name': 'lwir11',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_B10.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized'},\n", " 'extensions': []},\n", " {'name': 'trad',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_TRAD.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", " 'title': 'Thermal radiance'},\n", " 'extensions': []},\n", " {'name': 'urad',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_URAD.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", " 'title': 'Upwelled radiance'},\n", " 'extensions': []},\n", " {'name': 'drad',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_DRAD.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", " 'title': 'Downwelled radiance'},\n", " 'extensions': []},\n", " {'name': 'emis',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_EMIS.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", " 'title': 'Emissivity'},\n", " 'extensions': []},\n", " {'name': 'emsd',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_EMSD.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", " 'title': 'Emissivity standard deviation'},\n", " 'extensions': []},\n", " {'name': 'atran',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_ATRAN.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", " 'title': 'Atmospheric transmission'},\n", " 'extensions': []},\n", " {'name': 'cdist',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_CDIST.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", " 'title': 'Cloud distance'},\n", " 'extensions': []},\n", " {'name': 'qa',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_ST_QA.TIF',\n", " 'title': 'Surface Temperature Quality Assessment Band'},\n", " 'extensions': []},\n", " {'name': 'qa_pixel',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_QA_PIXEL.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", " 'title': 'Pixel quality assessment'},\n", " 'extensions': [[, ]>,\n", " , ]>,\n", " , ]>,\n", " , ]>,\n", " , ]>,\n", " , ]>,\n", " , ]>,\n", " , ]>,\n", " , , , ]>,\n", " , , , ]>,\n", " , , , ]>,\n", " , , , ]>]]},\n", " {'name': 'qa_radsat',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_QA_RADSAT.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", " 'description': 'Collection 2 Level-1 Radiometric Saturation and Terrain Occlusion Quality Assessment Band (QA_RADSAT)'},\n", " 'extensions': [[, ]>,\n", " , ]>,\n", " , ]>,\n", " , ]>,\n", " , ]>,\n", " , ]>,\n", " , ]>,\n", " , ]>,\n", " , ]>]]},\n", " {'name': 'qa_aerosol',\n", " 'asset_fields': {'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_QA_AEROSOL.TIF',\n", " 'media_type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", " 'title': 'Aerosol Quality Assessment Band'},\n", " 'extensions': [[, ]>,\n", " , ]>,\n", " , ]>,\n", " , ]>,\n", " , , , ]>]]}]" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "landsat_band_info(metadata, download_url)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "With this information we can now define a method that adds all the relevant assets for a scene to an item:" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [], "source": [ "def add_assets(item: pystac.Item, band_info: List[BandInfo]) -> None:\n", " for band in band_info:\n", " asset = pystac.Asset(**band[\"asset_fields\"])\n", " asset.set_owner(item)\n", " for ext_data in band[\"extensions\"]:\n", " if isinstance(ext_data, EOBand):\n", " EOExtension.ext(asset, add_if_missing=True).bands = [ext_data]\n", " elif isinstance(ext_data, RasterBand):\n", " RasterExtension.ext(asset, add_if_missing=True).bands = [ext_data]\n", " elif isinstance(ext_data, list):\n", " ClassificationExtension.ext(\n", " asset, add_if_missing=True\n", " ).bitfields = ext_data\n", " item.add_asset(band[\"name\"], asset)" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [], "source": [ "add_assets(item, landsat_band_info(metadata, download_url))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can examine the item to ensure that the assets appear as expected." ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_B5.TIF',\n", " 'type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", " 'eo:bands': [{'name': 'OLI_B5',\n", " 'common_name': 'nir08',\n", " 'description': 'Near infrared (Operational Land Imager)',\n", " 'center_wavelength': 0.87,\n", " 'full_width_half_max': 0.03}]}" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.assets[\"nir08\"].to_dict()" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'href': 'https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC08_L2SP_014032_20221219_20230113_02_T1/LC08_L2SP_014032_20221219_20230113_02_T1_SR_QA_AEROSOL.TIF',\n", " 'type': 'image/tiff; application=geotiff; profile=cloud-optimized',\n", " 'title': 'Aerosol Quality Assessment Band',\n", " 'classification:bitfields': [{'offset': 0,\n", " 'length': 1,\n", " 'classes': [{'value': 0,\n", " 'name': 'not_fill',\n", " 'description': 'Pixel is not fill'},\n", " {'value': 1, 'name': 'fill', 'description': 'Pixel is fill'}],\n", " 'description': 'Image or fill data',\n", " 'name': 'fill'},\n", " {'offset': 1,\n", " 'length': 1,\n", " 'classes': [{'value': 0,\n", " 'name': 'not_valid',\n", " 'description': 'Pixel retrieval is not valid'},\n", " {'value': 1, 'name': 'valid', 'description': 'Pixel retrieval is valid'}],\n", " 'description': 'Valid aerosol retrieval',\n", " 'name': 'retrieval'},\n", " {'offset': 2,\n", " 'length': 1,\n", " 'classes': [{'value': 0,\n", " 'name': 'not_water',\n", " 'description': 'Pixel is not water'},\n", " {'value': 1, 'name': 'water', 'description': 'Pixel is water'}],\n", " 'description': 'Water mask',\n", " 'name': 'water'},\n", " {'offset': 5,\n", " 'length': 1,\n", " 'classes': [{'value': 0,\n", " 'name': 'not_interpolated',\n", " 'description': 'Pixel is not interpolated'},\n", " {'value': 1,\n", " 'name': 'interpolated',\n", " 'description': 'Pixel is interpolated'}],\n", " 'description': 'Aerosol interpolation',\n", " 'name': 'interpolated'},\n", " {'offset': 6,\n", " 'length': 2,\n", " 'classes': [{'value': 0,\n", " 'name': 'climatology',\n", " 'description': 'No aerosol correction applied'},\n", " {'value': 1, 'name': 'low', 'description': 'Low aerosol level'},\n", " {'value': 2, 'name': 'medium', 'description': 'Medium aerosol level'},\n", " {'value': 3, 'name': 'high', 'description': 'High aerosol level'}],\n", " 'description': 'Aerosol level',\n", " 'name': 'level'}]}" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.assets[\"qa_aerosol\"].to_dict()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Add projection information\n", "\n", "We can specify the EPSG code for the scene as part of the [projection extension](https://github.com/stac-extensions/projection). The below method, adapted from [stactools](https://github.com/stactools-packages/landsat/blob/9f595a9d5ed6b62a2e96338e79f5bb502a7d90d0/src/stactools/landsat/mtl_metadata.py#L86-L109), figures out the correct UTM Zone EPSG:" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [], "source": [ "def get_epsg(metadata: Dict[str, Any], min_lat: float, max_lat: float) -> Optional[int]:\n", " if \"UTM_ZONE\" in metadata[\"PROJECTION_ATTRIBUTES\"]:\n", " utm_zone_integer = metadata[\"PROJECTION_ATTRIBUTES\"][\"UTM_ZONE\"].zfill(2)\n", " return int(f\"326{utm_zone_integer}\")\n", " else:\n", " lat_ts = metadata[\"PROJECTION_ATTRIBUTES\"][\"TRUE_SCALE_LAT\"]\n", " if lat_ts == \"-71.00000\":\n", " # Antarctic\n", " return 3031\n", " elif lat_ts == \"71.00000\":\n", " # Arctic\n", " return 3995\n", " else:\n", " raise ValueError(\n", " f\"Unexpeced value for PROJECTION_ATTRIBUTES/TRUE_SCALE_LAT: {lat_ts} \"\n", " )" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "32618" ] }, "execution_count": 34, "metadata": {}, "output_type": "execute_result" } ], "source": [ "proj_ext = ProjectionExtension.ext(item, add_if_missing=True)\n", "assert item.bbox is not None\n", "proj_ext.epsg = get_epsg(metadata, item.bbox[1], item.bbox[3])\n", "proj_ext.epsg" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Add view geometry information\n", "\n", "The [View Geometry](https://github.com/stac-extensions/view) extension specifies information related to angles of sensors and other radiance angles that affect the view of resulting data. The Landsat metadata specifies two of these parameters, so we add them to our Item:" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [], "source": [ "def get_view_info(metadata: Dict[str, Any]) -> Dict[str, float]:\n", " return {\n", " \"sun_azimuth\": float(metadata[\"IMAGE_ATTRIBUTES\"][\"SUN_AZIMUTH\"]),\n", " \"sun_elevation\": float(metadata[\"IMAGE_ATTRIBUTES\"][\"SUN_ELEVATION\"]),\n", " }" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'datetime': '2022-12-19T15:40:17.729916Z',\n", " 'gsd': 30.0,\n", " 'eo:cloud_cover': 43.42,\n", " 'proj:epsg': 32618,\n", " 'view:sun_azimuth': 160.86021018,\n", " 'view:sun_elevation': 23.81656674}" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "view_ext = ViewExtension.ext(item, add_if_missing=True)\n", "view_info = get_view_info(metadata)\n", "view_ext.sun_azimuth = view_info[\"sun_azimuth\"]\n", "view_ext.sun_elevation = view_info[\"sun_elevation\"]\n", "item.properties" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we've added all the metadata to the Item, let's check the validator to make sure we've specified everything correctly. The validation logic will take into account the new extensions that have been enabled and validate against the proper schemas for those extensions." ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json',\n", " 'https://stac-extensions.github.io/eo/v1.1.0/schema.json',\n", " 'https://stac-extensions.github.io/raster/v1.1.0/schema.json',\n", " 'https://stac-extensions.github.io/classification/v1.1.0/schema.json',\n", " 'https://stac-extensions.github.io/projection/v1.1.0/schema.json',\n", " 'https://stac-extensions.github.io/view/v1.0.0/schema.json']" ] }, "execution_count": 37, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.validate()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Building the Collection\n", "\n", "Now that we know how to build an Item for a scene, let's build the Collection that will contain all the Items.\n", "\n", "If we look at the `__init__` method for `pystac.Collection`, we can see what properties are required:" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Help on function __init__ in module pystac.collection:\n", "\n", "__init__(self, id: 'str', description: 'str', extent: 'Extent', title: 'Optional[str]' = None, stac_extensions: 'Optional[List[str]]' = None, href: 'Optional[str]' = None, extra_fields: 'Optional[Dict[str, Any]]' = None, catalog_type: 'Optional[CatalogType]' = None, license: 'str' = 'proprietary', keywords: 'Optional[List[str]]' = None, providers: 'Optional[List[Provider]]' = None, summaries: 'Optional[Summaries]' = None, assets: 'Optional[Dict[str, Asset]]' = None)\n", " Initialize self. See help(type(self)) for accurate signature.\n", "\n" ] } ], "source": [ "help(pystac.Collection.__init__)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Collection `id`\n", "\n", "We'll use the location name we defined above in the ID for our Collection:" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'philly-landsat-collection-2'" ] }, "execution_count": 39, "metadata": {}, "output_type": "execute_result" } ], "source": [ "collection_id = \"{}-landsat-collection-2\".format(location_name.lower())\n", "collection_id" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Collection `title`\n", "\n", "Here we set a simple title for our collection." ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'2022 Landsat images over philly'" ] }, "execution_count": 40, "metadata": {}, "output_type": "execute_result" } ], "source": [ "collection_title = \"2022 Landsat images over {}\".format(location_name.lower())\n", "collection_title" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Collection `description`\n", "\n", "Here we give a brief description of the Collection. If this were a real Collection that were being published, I'd recommend putting a much more detailed description to ensure anyone using your STAC knows what they are working with!\n", "\n", "Notice we are using [Markdown](https://www.markdownguide.org/) to write the description. The `description` field can be Markdown to help tools that render information about STAC to display the information in a more readable way." ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "### Philly Landsat Collection 2\n", "\n", "A collection of Landsat scenes around Philly in 2022.\n", "\n" ] } ], "source": [ "collection_description = \"\"\"### {} Landsat Collection 2\n", "\n", "A collection of Landsat scenes around {} in 2022.\n", "\"\"\".format(\n", " location_name, location_name\n", ")\n", "print(collection_description)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Collection `extent`\n", "\n", "A Collection specifies the spatial and temporal extent of all the item it contains. Since Landsat spans the globe, we'll simply put a global extent here. We'll also specify an open-ended time interval.\n", "\n", "Towards the end of the notebook, we'll use a method to easily scope this down to cover the times and space the Items occupy once we've added all the items." ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [], "source": [ "spatial_extent = pystac.SpatialExtent([[-180.0, -90.0, 180.0, 90.0]])\n", "temporal_extent = pystac.TemporalExtent([[datetime(2013, 6, 1), None]])\n", "collection_extent = pystac.Extent(spatial_extent, temporal_extent)" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [], "source": [ "collection = pystac.Collection(\n", " id=collection_id,\n", " title=collection_title,\n", " description=collection_description,\n", " extent=collection_extent,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can now look at our Collection as a `dict` to check our values." ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'type': 'Collection',\n", " 'id': 'philly-landsat-collection-2',\n", " 'stac_version': '1.0.0',\n", " 'description': '### Philly Landsat Collection 2\\n\\nA collection of Landsat scenes around Philly in 2022.\\n',\n", " 'links': [],\n", " 'title': '2022 Landsat images over philly',\n", " 'extent': {'spatial': {'bbox': [[-180.0, -90.0, 180.0, 90.0]]},\n", " 'temporal': {'interval': [['2013-06-01T00:00:00Z', None]]}},\n", " 'license': 'proprietary'}" ] }, "execution_count": 44, "metadata": {}, "output_type": "execute_result" } ], "source": [ "collection.to_dict()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Set the license\n", "\n", "Notice the `license` above is `proprietary`. This is the default in PySTAC if no license is specified; however Landsat is certainly not proprietary (thankfully!), so let's change the license to the correct [SPDX](https://spdx.org/licenses/) string for public domain data:" ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [], "source": [ "collection_license = \"PDDL-1.0\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Set the providers\n", "\n", "A collection will specify the providers of the data, including what role they have played. We can set our provider information by instantiating `pystac.Provider` objects:" ] }, { "cell_type": "code", "execution_count": 46, "metadata": {}, "outputs": [], "source": [ "collection.providers = [\n", " pystac.Provider(\n", " name=\"NASA\",\n", " roles=[pystac.ProviderRole.PRODUCER, pystac.ProviderRole.LICENSOR],\n", " url=\"https://landsat.gsfc.nasa.gov/\",\n", " ),\n", " pystac.Provider(\n", " name=\"USGS\",\n", " roles=[\n", " pystac.ProviderRole.PROCESSOR,\n", " pystac.ProviderRole.PRODUCER,\n", " pystac.ProviderRole.LICENSOR,\n", " ],\n", " url=\"https://www.usgs.gov/landsat-missions/landsat-collection-2-level-2-science-products\",\n", " ),\n", " pystac.Provider(\n", " name=\"Microsoft\",\n", " roles=[pystac.ProviderRole.HOST],\n", " url=\"https://planetarycomputer.microsoft.com\",\n", " ),\n", "]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create items for each scene\n", "\n", "We created an Item for a single scene above. This method consolidates that logic into a single method that can construct an Item from a scene, so we can create an Item for every scene in our subset:" ] }, { "cell_type": "code", "execution_count": 47, "metadata": {}, "outputs": [], "source": [ "def item_from_metadata(mtl_xml_url: str) -> pystac.Item:\n", " metadata = get_metadata(mtl_xml_url)\n", " download_url = partial(download_sidecar, metadata)\n", "\n", " bbox = get_bbox(metadata)\n", " item = pystac.Item(\n", " id=get_item_id(metadata),\n", " datetime=get_datetime(metadata),\n", " geometry=get_geometry(metadata, bbox),\n", " bbox=bbox,\n", " properties={},\n", " )\n", "\n", " item.common_metadata.gsd = 30.0\n", "\n", " item_eo_ext = EOExtension.ext(item, add_if_missing=True)\n", " item_eo_ext.cloud_cover = get_cloud_cover(metadata)\n", "\n", " add_assets(item, landsat_band_info(metadata, download_url))\n", "\n", " item_proj_ext = ProjectionExtension.ext(item, add_if_missing=True)\n", " assert item.bbox is not None\n", " item_proj_ext.epsg = get_epsg(metadata, item.bbox[1], item.bbox[3])\n", "\n", " item_view_ext = ViewExtension.ext(item, add_if_missing=True)\n", " view_info = get_view_info(metadata)\n", " item_view_ext.sun_azimuth = view_info[\"sun_azimuth\"]\n", " item_view_ext.sun_elevation = view_info[\"sun_elevation\"]\n", "\n", " item.validate()\n", "\n", " return item" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here we create an item per scene and add it to our collection. Since this is reading multiple metadata files per scene from the internet, it may take a little bit to run:" ] }, { "cell_type": "code", "execution_count": 48, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ANG file for url https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2022/014/032/LC09_L2SP_014032_20221024_20221026_02_T2/LC09_L2SP_014032_20221024_20221026_02_T2_ANG.txt is incorrectly formatted\n" ] } ], "source": [ "for url in scene_mtls:\n", " try:\n", " item = item_from_metadata(url)\n", " collection.add_item(item)\n", " except Exception as e:\n", " print(e)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Reset collection extent based on items\n", "\n", "Now that we've added all the item we can use the `update_extent_from_items` method on the Collection to set the extent based on the contained items:" ] }, { "cell_type": "code", "execution_count": 49, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'spatial': {'bbox': [[-76.29048, 39.25502, -73.48833, 41.38441]]},\n", " 'temporal': {'interval': [['2022-10-08T15:40:14.577173Z',\n", " '2022-12-19T15:40:17.729916Z']]}}" ] }, "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ "collection.update_extent_from_items()\n", "collection.extent.to_dict()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Set the HREFs for everything in the catalog\n", "\n", "We've been building up our Collection and Items in memory. This has been convenient as it allows us not to think about file paths as we construct our Catalog. However, a STAC is not valid without any HREFs! \n", "\n", "We can use the `normalize_hrefs` method to set all the HREFs in the entire STAC based on a root directory. This will use the [STAC Best Practices](https://github.com/radiantearth/stac-spec/blob/master/best-practices.md#catalog-layout) recommendations for STAC file layout for each Catalog, Collection and Item in the STAC.\n", "\n", "Here we use that method and set the root directory to a subdirectory of our user's `home` directory:" ] }, { "cell_type": "code", "execution_count": 50, "metadata": {}, "outputs": [], "source": [ "from pathlib import Path\n", "\n", "root_path = str(Path.home() / \"{}-landsat-stac\".format(location_name))\n", "\n", "collection.normalize_hrefs(root_path)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we have all the Collection's data set and HREFs in place we can validate the entire STAC using `validate_all`, which recursively crawls through a catalog and validates every STAC object in the catalog:" ] }, { "cell_type": "code", "execution_count": 51, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "8" ] }, "execution_count": 51, "metadata": {}, "output_type": "execute_result" } ], "source": [ "collection.validate_all()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Write the catalog locally\n", "\n", "Now that we have our complete, validated STAC in memory, let's write it out. This is as simple as calling `save` on the Collection. We need to specify the type of catalog in order to property write out links - these types are described again in the STAC [Best Practices](https://github.com/radiantearth/stac-spec/blob/master/best-practices.md#use-of-links) documentation.\n", "\n", "We'll use the \"self contained\" type, which uses relative paths and does not specify absolute \"self\" links to any object. This makes the catalog more portable, as it remains valid even if you copy the STAC to new locations." ] }, { "cell_type": "code", "execution_count": 52, "metadata": {}, "outputs": [], "source": [ "collection.save(pystac.CatalogType.SELF_CONTAINED)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we've written our STAC out we probably want to view it. We can use the `describe` method to print out a simple representation of the catalog:" ] }, { "cell_type": "code", "execution_count": 53, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "* \n", " * \n", " * \n", " * \n", " * \n", " * \n", " * \n", " * \n", " * \n" ] } ], "source": [ "collection.describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can also use the `to_dict` method on individual STAC objects in order to see the data, as we've been doing in the tutorial:" ] }, { "cell_type": "code", "execution_count": 54, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'type': 'Collection',\n", " 'id': 'philly-landsat-collection-2',\n", " 'stac_version': '1.0.0',\n", " 'description': '### Philly Landsat Collection 2\\n\\nA collection of Landsat scenes around Philly in 2022.\\n',\n", " 'links': [{'rel': 'root',\n", " 'href': './collection.json',\n", " 'type': 'application/json',\n", " 'title': '2022 Landsat images over philly'},\n", " {'rel': 'item',\n", " 'href': './LC80140322022353/LC80140322022353.json',\n", " 'type': 'application/json'},\n", " {'rel': 'item',\n", " 'href': './LC90140322022345/LC90140322022345.json',\n", " 'type': 'application/json'},\n", " {'rel': 'item',\n", " 'href': './LC80140322022337/LC80140322022337.json',\n", " 'type': 'application/json'},\n", " {'rel': 'item',\n", " 'href': './LC90140322022329/LC90140322022329.json',\n", " 'type': 'application/json'},\n", " {'rel': 'item',\n", " 'href': './LC80140322022321/LC80140322022321.json',\n", " 'type': 'application/json'},\n", " {'rel': 'item',\n", " 'href': './LC90140322022313/LC90140322022313.json',\n", " 'type': 'application/json'},\n", " {'rel': 'item',\n", " 'href': './LC80140322022305/LC80140322022305.json',\n", " 'type': 'application/json'},\n", " {'rel': 'item',\n", " 'href': './LC90140322022281/LC90140322022281.json',\n", " 'type': 'application/json'},\n", " {'rel': 'self',\n", " 'href': '/Users/pjh/Philly-landsat-stac/collection.json',\n", " 'type': 'application/json'}],\n", " 'title': '2022 Landsat images over philly',\n", " 'extent': {'spatial': {'bbox': [[-76.29048, 39.25502, -73.48833, 41.38441]]},\n", " 'temporal': {'interval': [['2022-10-08T15:40:14.577173Z',\n", " '2022-12-19T15:40:17.729916Z']]}},\n", " 'license': 'proprietary',\n", " 'providers': [{'name': 'NASA',\n", " 'roles': [,\n", " ],\n", " 'url': 'https://landsat.gsfc.nasa.gov/'},\n", " {'name': 'USGS',\n", " 'roles': [,\n", " ,\n", " ],\n", " 'url': 'https://www.usgs.gov/landsat-missions/landsat-collection-2-level-2-science-products'},\n", " {'name': 'Microsoft',\n", " 'roles': [],\n", " 'url': 'https://planetarycomputer.microsoft.com'}]}" ] }, "execution_count": 54, "metadata": {}, "output_type": "execute_result" } ], "source": [ "collection.to_dict()" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "However, if we want to browse our STAC more interactively, we can serve the Collection from a local webserver and then browse the Collection with [stac-browser](https://github.com/radiantearth/stac-browser).\n", "\n", "We can use this simple Python server (copied from [this gist](https://gist.github.com/acdha/925e9ffc3d74ad59c3ea)) to serve our our directory at port 5555:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "from http.server import HTTPServer, SimpleHTTPRequestHandler\n", "\n", "os.chdir(root_path)\n", "\n", "\n", "class CORSRequestHandler(SimpleHTTPRequestHandler):\n", " def end_headers(self) -> None:\n", " self.send_header(\"Access-Control-Allow-Origin\", \"*\")\n", " self.send_header(\"Access-Control-Allow-Methods\", \"GET\")\n", " self.send_header(\"Cache-Control\", \"no-store, no-cache, must-revalidate\")\n", " return super(CORSRequestHandler, self).end_headers()\n", "\n", "\n", "with HTTPServer((\"localhost\", 5555), CORSRequestHandler) as httpd:\n", " httpd.serve_forever()" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Now we can browse our STAC Collection with stac-browser in a few different ways:\n", "1. Follow the [instructions](https://github.com/radiantearth/stac-browser/blob/main/local_files.md) for starting a stac-browser instance and point it at `http://localhost:5555/collection.json` to serve out the STAC.\n", "2. If you want to avoid setting up your own stac-browser instance, you can use the [STAC Browser Demo](https://radiantearth.github.io/stac-browser/) hosted by Radiant Earth: [https://radiantearth.github.io/stac-browser/#/http://localhost:5555/collection.json](https://radiantearth.github.io/stac-browser/#/http://localhost:5555/collection.json)\n", "\n", "To quit the server, use the `Kernel` -> `Interrupt` menu option." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Acknowledgements\n", "\n", "Credit to [sat-stac-landsat](https://github.com/sat-utils/sat-stac-landsat) from which a lot of this code was based." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.3" } }, "nbformat": 4, "nbformat_minor": 2 } pystac-1.9.0/docs/tutorials/how-to-create-stac-catalogs.ipynb000066400000000000000000004111171451576074700242730ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# How to create STAC Catalogs \n", "## STAC Community Sprint, Arlington, November 7th 2019" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This notebook runs through some of the basics of using PySTAC to create a static STAC. It was part of a 30 minute presentation at the [community STAC sprint](https://github.com/radiantearth/community-sprints/tree/master/11052019-arlignton-va) in Arlington, VA in November 2019, updated to work with current PySTAC." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This tutorial will require the `boto3`, `rasterio`, and `shapely` libraries:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Note: you may need to restart the kernel to use updated packages.\n" ] } ], "source": [ "%pip install boto3 rasterio shapely pystac --quiet" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can import pystac and access most of the functionality we need with the single import:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import pystac" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Creating a catalog from a local file" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To give us some material to work with, lets download a single image from the [Spacenet 5 challenge](https://www.topcoder.com/challenges/30099956). We'll use a temporary directory to save off our single-item STAC." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "import os\n", "import urllib.request\n", "from tempfile import TemporaryDirectory\n", "\n", "tmp_dir = TemporaryDirectory()\n", "img_path = os.path.join(tmp_dir.name, \"image.tif\")" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "('/tmp/tmpdsdpun_y/image.tif', )" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "url = (\n", " \"https://spacenet-dataset.s3.amazonaws.com/\"\n", " \"spacenet/SN5_roads/train/AOI_7_Moscow/MS/\"\n", " \"SN5_roads_train_AOI_7_Moscow_MS_chip996.tif\"\n", ")\n", "urllib.request.urlretrieve(url, img_path)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We want to create a Catalog. Let's check the docs for `Catalog` to see what information we'll need." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[0;31mInit signature:\u001b[0m\n", "\u001b[0mpystac\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mCatalog\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mid\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'str'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mdescription\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'str'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mtitle\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[str]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mstac_extensions\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[List[str]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mextra_fields\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Dict[str, Any]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mhref\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[str]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mcatalog_type\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'CatalogType'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mABSOLUTE_PUBLISHED\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mDocstring:\u001b[0m \n", "A PySTAC Catalog represents a STAC catalog in memory.\n", "\n", "A Catalog is a :class:`~pystac.STACObject` that may contain children,\n", "which are instances of :class:`~pystac.Catalog` or :class:`~pystac.Collection`,\n", "as well as :class:`~pystac.Item` s.\n", "\n", "Args:\n", " id : Identifier for the catalog. Must be unique within the STAC.\n", " description : Detailed multi-line description to fully explain the catalog.\n", " `CommonMark 0.29 syntax `_ MAY be used for rich\n", " text representation.\n", " title : Optional short descriptive one-line title for the catalog.\n", " stac_extensions : Optional list of extensions the Catalog implements.\n", " href : Optional HREF for this catalog, which be set as the\n", " catalog's self link's HREF.\n", " catalog_type : Optional catalog type for this catalog. Must\n", " be one of the values in :class:`~pystac.CatalogType`.\n", "\u001b[0;31mFile:\u001b[0m ~/pystac/pystac/catalog.py\n", "\u001b[0;31mType:\u001b[0m ABCMeta\n", "\u001b[0;31mSubclasses:\u001b[0m Collection" ] } ], "source": [ "?pystac.Catalog" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's just give an ID and a description. We don't have to worry about the HREF right now; that will be set later." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "catalog = pystac.Catalog(id=\"test-catalog\", description=\"Tutorial catalog.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There are no children or items in the catalog, since we haven't added anything yet." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[]\n", "[]\n" ] } ], "source": [ "print(list(catalog.get_children()))\n", "print(list(catalog.get_items()))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We'll now create an Item to represent the image. Check the pydocs to see what you need to supply:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[0;31mInit signature:\u001b[0m\n", "\u001b[0mpystac\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mItem\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mid\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'str'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mgeometry\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Dict[str, Any]]'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mbbox\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[List[float]]'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mdatetime\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Datetime]'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mproperties\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Dict[str, Any]'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mstart_datetime\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Datetime]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mend_datetime\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Datetime]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mstac_extensions\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[List[str]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mhref\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[str]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mcollection\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Union[str, Collection]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mextra_fields\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Dict[str, Any]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0massets\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Dict[str, Asset]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mDocstring:\u001b[0m \n", "An Item is the core granular entity in a STAC, containing the core metadata\n", "that enables any client to search or crawl online catalogs of spatial 'assets' -\n", "satellite imagery, derived data, DEM's, etc.\n", "\n", "Args:\n", " id : Provider identifier. Must be unique within the STAC.\n", " geometry : Defines the full footprint of the asset represented by this\n", " item, formatted according to\n", " `RFC 7946, section 3.1 (GeoJSON) `_.\n", " bbox : Bounding Box of the asset represented by this item\n", " using either 2D or 3D geometries. The length of the array must be 2*n\n", " where n is the number of dimensions. Could also be None in the case of a\n", " null geometry.\n", " datetime : datetime associated with this item. If None,\n", " a start_datetime and end_datetime must be supplied.\n", " properties : A dictionary of additional metadata for the item.\n", " start_datetime : Optional start datetime, part of common metadata. This value\n", " will override any `start_datetime` key in properties.\n", " end_datetime : Optional end datetime, part of common metadata. This value\n", " will override any `end_datetime` key in properties.\n", " stac_extensions : Optional list of extensions the Item implements.\n", " href : Optional HREF for this item, which be set as the item's\n", " self link's HREF.\n", " collection : The Collection or Collection ID that this item\n", " belongs to.\n", " extra_fields : Extra fields that are part of the top-level JSON\n", " properties of the Item.\n", " assets : A dictionary mapping string keys to :class:`~pystac.Asset` objects. All\n", " :class:`~pystac.Asset` values in the dictionary will have their\n", " :attr:`~pystac.Asset.owner` attribute set to the created Item.\n", "\u001b[0;31mFile:\u001b[0m ~/pystac/pystac/item.py\n", "\u001b[0;31mType:\u001b[0m ABCMeta\n", "\u001b[0;31mSubclasses:\u001b[0m " ] } ], "source": [ "?pystac.Item" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using [rasterio](https://rasterio.readthedocs.io/en/stable/), we can pull out the bounding box of the image to use for the image metadata. If the image contained a NoData border, we would ideally pull out the footprint and save it as the geometry; in this case, we're working with a small chip that most likely has no NoData values." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "import rasterio\n", "from shapely.geometry import Polygon, mapping\n", "\n", "\n", "def get_bbox_and_footprint(raster_uri):\n", " with rasterio.open(raster_uri) as ds:\n", " bounds = ds.bounds\n", " bbox = [bounds.left, bounds.bottom, bounds.right, bounds.top]\n", " footprint = Polygon(\n", " [\n", " [bounds.left, bounds.bottom],\n", " [bounds.left, bounds.top],\n", " [bounds.right, bounds.top],\n", " [bounds.right, bounds.bottom],\n", " ]\n", " )\n", "\n", " return (bbox, mapping(footprint))" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[37.6616853489879, 55.73478197572927, 37.66573047610874, 55.73882710285011]\n", "{'type': 'Polygon', 'coordinates': (((37.6616853489879, 55.73478197572927), (37.6616853489879, 55.73882710285011), (37.66573047610874, 55.73882710285011), (37.66573047610874, 55.73478197572927), (37.6616853489879, 55.73478197572927)),)}\n" ] } ], "source": [ "bbox, footprint = get_bbox_and_footprint(img_path)\n", "print(bbox)\n", "print(footprint)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We're also using `datetime.utcnow()` to supply the required datetime property for our Item. Since this is a required property, you might often find yourself making up a time to fill in if you don't know the exact capture time." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "from datetime import datetime\n", "\n", "item = pystac.Item(\n", " id=\"local-image\",\n", " geometry=footprint,\n", " bbox=bbox,\n", " datetime=datetime.utcnow(),\n", " properties={},\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We haven't added it to a catalog yet, so it's parent isn't set. Once we add it to the catalog, we can see it correctly links to it's parent." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "assert item.get_parent() is None" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "
\n", "
\n", "
    \n", " \n", " \n", " \n", "
  • \n", " rel\n", " \"item\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " href\n", " None\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " type\n", " \"application/json\"\n", "
  • \n", " \n", " \n", " \n", "
\n", "
\n", "
" ], "text/plain": [ ">" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "catalog.add_item(item)" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "
\n", "
\n", "
    \n", " \n", " \n", " \n", "
  • \n", " type\n", " \"Catalog\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " id\n", " \"test-catalog\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " stac_version\n", " \"1.0.0\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " description\n", " \"Tutorial catalog.\"\n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " \n", " links\n", " [] 1 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"item\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " None\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"application/json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", "
\n", "
\n", "
" ], "text/plain": [ "" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.get_parent()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`describe()` is a useful method on `Catalog` - but be careful when using it on large catalogs, as it will walk the entire tree of the STAC." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "* \n", " * \n" ] } ], "source": [ "catalog.describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Adding Assets\n", "\n", "We've created an Item, but there aren't any assets associated with it. Let's create one:" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[0;31mInit signature:\u001b[0m\n", "\u001b[0mpystac\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mAsset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mhref\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'str'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mtitle\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[str]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mdescription\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[str]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mmedia_type\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[str]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mroles\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[List[str]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mextra_fields\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Dict[str, Any]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0;34m'None'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mDocstring:\u001b[0m \n", "An object that contains a link to data associated with an Item or Collection that\n", "can be downloaded or streamed.\n", "\n", "Args:\n", " href : Link to the asset object. Relative and absolute links are both\n", " allowed.\n", " title : Optional displayed title for clients and users.\n", " description : A description of the Asset providing additional details,\n", " such as how it was processed or created. CommonMark 0.29 syntax MAY be used\n", " for rich text representation.\n", " media_type : Optional description of the media type. Registered Media Types\n", " are preferred. See :class:`~pystac.MediaType` for common media types.\n", " roles : Optional, Semantic roles (i.e. thumbnail, overview,\n", " data, metadata) of the asset.\n", " extra_fields : Optional, additional fields for this asset. This is used\n", " by extensions as a way to serialize and deserialize properties on asset\n", " object JSON.\n", "\u001b[0;31mFile:\u001b[0m ~/pystac/pystac/asset.py\n", "\u001b[0;31mType:\u001b[0m type\n", "\u001b[0;31mSubclasses:\u001b[0m " ] } ], "source": [ "?pystac.Asset" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "item.add_asset(\n", " key=\"image\", asset=pystac.Asset(href=img_path, media_type=pystac.MediaType.GEOTIFF)\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "At any time we can call `to_dict()` on STAC objects to see how the STAC JSON is shaping up. Notice the asset is now set:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " \"type\": \"Feature\",\n", " \"stac_version\": \"1.0.0\",\n", " \"id\": \"local-image\",\n", " \"properties\": {\n", " \"datetime\": \"2023-10-12T15:35:17.290343Z\"\n", " },\n", " \"geometry\": {\n", " \"type\": \"Polygon\",\n", " \"coordinates\": [\n", " [\n", " [\n", " 37.6616853489879,\n", " 55.73478197572927\n", " ],\n", " [\n", " 37.6616853489879,\n", " 55.73882710285011\n", " ],\n", " [\n", " 37.66573047610874,\n", " 55.73882710285011\n", " ],\n", " [\n", " 37.66573047610874,\n", " 55.73478197572927\n", " ],\n", " [\n", " 37.6616853489879,\n", " 55.73478197572927\n", " ]\n", " ]\n", " ]\n", " },\n", " \"links\": [\n", " {\n", " \"rel\": \"root\",\n", " \"href\": null,\n", " \"type\": \"application/json\"\n", " },\n", " {\n", " \"rel\": \"parent\",\n", " \"href\": null,\n", " \"type\": \"application/json\"\n", " }\n", " ],\n", " \"assets\": {\n", " \"image\": {\n", " \"href\": \"/tmp/tmpdsdpun_y/image.tif\",\n", " \"type\": \"image/tiff; application=geotiff\"\n", " }\n", " },\n", " \"bbox\": [\n", " 37.6616853489879,\n", " 55.73478197572927,\n", " 37.66573047610874,\n", " 55.73882710285011\n", " ],\n", " \"stac_extensions\": []\n", "}\n" ] } ], "source": [ "import json\n", "\n", "print(json.dumps(item.to_dict(), indent=4))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that the link `href` properties are `null`. This is OK, as we're working with the STAC in memory. Next, we'll talk about writing the catalog out, and how to set those HREFs." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Saving the catalog" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As the JSON above indicates, there's no HREFs set on these in-memory items. PySTAC uses the `self` link on STAC objects to track where the file lives. Because we haven't set them, they evaluate to `None`:" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "True\n", "True\n" ] } ], "source": [ "print(catalog.get_self_href() is None)\n", "print(item.get_self_href() is None)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In order to set them, we can use `normalize_hrefs`. This method will create a normalized set of HREFs for each STAC object in the catalog, according to the [best practices document](https://github.com/radiantearth/stac-spec/blob/v0.8.1/best-practices.md#catalog-layout)'s recommendations on how to lay out a catalog." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "catalog.normalize_hrefs(os.path.join(tmp_dir.name, \"stac\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we've normalized to a root directory (the temporary directory), we see that the `self` links are set:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/tmp/tmpdsdpun_y/stac/catalog.json\n", "/tmp/tmpdsdpun_y/stac/local-image/local-image.json\n" ] } ], "source": [ "print(catalog.get_self_href())\n", "print(item.get_self_href())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can now call `save` on the catalog, which will recursively save all the STAC objects to their respective self HREFs.\n", "\n", "Save requires a `CatalogType` to be set. You can review the [API docs](https://pystac.readthedocs.io/en/stable/api.html#catalogtype) on `CatalogType` to see what each type means (unfortunately `help` doesn't show docstrings for attributes)." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "catalog.save(catalog_type=pystac.CatalogType.SELF_CONTAINED)" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/tmp/tmpdsdpun_y/stac/catalog.json\n", "\n", "/tmp/tmpdsdpun_y/stac/local-image:\n", "local-image.json\n" ] } ], "source": [ "!ls {tmp_dir.name}/stac/*" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " \"type\": \"Catalog\",\n", " \"id\": \"test-catalog\",\n", " \"stac_version\": \"1.0.0\",\n", " \"description\": \"Tutorial catalog.\",\n", " \"links\": [\n", " {\n", " \"rel\": \"root\",\n", " \"href\": \"./catalog.json\",\n", " \"type\": \"application/json\"\n", " },\n", " {\n", " \"rel\": \"item\",\n", " \"href\": \"./local-image/local-image.json\",\n", " \"type\": \"application/json\"\n", " }\n", " ]\n", "}\n" ] } ], "source": [ "with open(catalog.self_href) as f:\n", " print(f.read())" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " \"type\": \"Feature\",\n", " \"stac_version\": \"1.0.0\",\n", " \"id\": \"local-image\",\n", " \"properties\": {\n", " \"datetime\": \"2023-10-12T15:35:17.290343Z\"\n", " },\n", " \"geometry\": {\n", " \"type\": \"Polygon\",\n", " \"coordinates\": [\n", " [\n", " [\n", " 37.6616853489879,\n", " 55.73478197572927\n", " ],\n", " [\n", " 37.6616853489879,\n", " 55.73882710285011\n", " ],\n", " [\n", " 37.66573047610874,\n", " 55.73882710285011\n", " ],\n", " [\n", " 37.66573047610874,\n", " 55.73478197572927\n", " ],\n", " [\n", " 37.6616853489879,\n", " 55.73478197572927\n", " ]\n", " ]\n", " ]\n", " },\n", " \"links\": [\n", " {\n", " \"rel\": \"root\",\n", " \"href\": \"../catalog.json\",\n", " \"type\": \"application/json\"\n", " },\n", " {\n", " \"rel\": \"parent\",\n", " \"href\": \"../catalog.json\",\n", " \"type\": \"application/json\"\n", " }\n", " ],\n", " \"assets\": {\n", " \"image\": {\n", " \"href\": \"/tmp/tmpdsdpun_y/image.tif\",\n", " \"type\": \"image/tiff; application=geotiff\"\n", " }\n", " },\n", " \"bbox\": [\n", " 37.6616853489879,\n", " 55.73478197572927,\n", " 37.66573047610874,\n", " 55.73882710285011\n", " ],\n", " \"stac_extensions\": []\n", "}\n" ] } ], "source": [ "with open(item.self_href) as f:\n", " print(f.read())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you can see, all links are saved with relative paths. That's because we used `catalog_type=CatalogType.SELF_CONTAINED`. If we save an Absolute Published catalog, we'll see absolute paths:" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [], "source": [ "catalog.save(catalog_type=pystac.CatalogType.ABSOLUTE_PUBLISHED)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now the links included in the STAC item are all absolute:" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " \"type\": \"Feature\",\n", " \"stac_version\": \"1.0.0\",\n", " \"id\": \"local-image\",\n", " \"properties\": {\n", " \"datetime\": \"2023-10-12T15:35:17.290343Z\"\n", " },\n", " \"geometry\": {\n", " \"type\": \"Polygon\",\n", " \"coordinates\": [\n", " [\n", " [\n", " 37.6616853489879,\n", " 55.73478197572927\n", " ],\n", " [\n", " 37.6616853489879,\n", " 55.73882710285011\n", " ],\n", " [\n", " 37.66573047610874,\n", " 55.73882710285011\n", " ],\n", " [\n", " 37.66573047610874,\n", " 55.73478197572927\n", " ],\n", " [\n", " 37.6616853489879,\n", " 55.73478197572927\n", " ]\n", " ]\n", " ]\n", " },\n", " \"links\": [\n", " {\n", " \"rel\": \"root\",\n", " \"href\": \"/tmp/tmpdsdpun_y/stac/catalog.json\",\n", " \"type\": \"application/json\"\n", " },\n", " {\n", " \"rel\": \"parent\",\n", " \"href\": \"/tmp/tmpdsdpun_y/stac/catalog.json\",\n", " \"type\": \"application/json\"\n", " },\n", " {\n", " \"rel\": \"self\",\n", " \"href\": \"/tmp/tmpdsdpun_y/stac/local-image/local-image.json\",\n", " \"type\": \"application/json\"\n", " }\n", " ],\n", " \"assets\": {\n", " \"image\": {\n", " \"href\": \"/tmp/tmpdsdpun_y/image.tif\",\n", " \"type\": \"image/tiff; application=geotiff\"\n", " }\n", " },\n", " \"bbox\": [\n", " 37.6616853489879,\n", " 55.73478197572927,\n", " 37.66573047610874,\n", " 55.73882710285011\n", " ],\n", " \"stac_extensions\": []\n", "}\n" ] } ], "source": [ "with open(item.get_self_href()) as f:\n", " print(f.read())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice that the Asset HREF is absolute in both cases. We can make the Asset HREF relative to the STAC Item by using `.make_all_asset_hrefs_relative()`:" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [], "source": [ "catalog.make_all_asset_hrefs_relative()\n", "catalog.save(catalog_type=pystac.CatalogType.SELF_CONTAINED)" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\n", " \"type\": \"Feature\",\n", " \"stac_version\": \"1.0.0\",\n", " \"id\": \"local-image\",\n", " \"properties\": {\n", " \"datetime\": \"2023-10-12T15:35:17.290343Z\"\n", " },\n", " \"geometry\": {\n", " \"type\": \"Polygon\",\n", " \"coordinates\": [\n", " [\n", " [\n", " 37.6616853489879,\n", " 55.73478197572927\n", " ],\n", " [\n", " 37.6616853489879,\n", " 55.73882710285011\n", " ],\n", " [\n", " 37.66573047610874,\n", " 55.73882710285011\n", " ],\n", " [\n", " 37.66573047610874,\n", " 55.73478197572927\n", " ],\n", " [\n", " 37.6616853489879,\n", " 55.73478197572927\n", " ]\n", " ]\n", " ]\n", " },\n", " \"links\": [\n", " {\n", " \"rel\": \"root\",\n", " \"href\": \"../catalog.json\",\n", " \"type\": \"application/json\"\n", " },\n", " {\n", " \"rel\": \"parent\",\n", " \"href\": \"../catalog.json\",\n", " \"type\": \"application/json\"\n", " }\n", " ],\n", " \"assets\": {\n", " \"image\": {\n", " \"href\": \"../../image.tif\",\n", " \"type\": \"image/tiff; application=geotiff\"\n", " }\n", " },\n", " \"bbox\": [\n", " 37.6616853489879,\n", " 55.73478197572927,\n", " 37.66573047610874,\n", " 55.73882710285011\n", " ],\n", " \"stac_extensions\": []\n", "}\n" ] } ], "source": [ "with open(item.get_self_href()) as f:\n", " print(f.read())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Creating an Item that implements the EO extension\n", "\n", "In the code above our item only implemented the core STAC Item specification. With [extensions](https://github.com/radiantearth/stac-spec/tree/v0.9.0/extensions) we can record more information and add additional functionality to the Item. Given that we know this is a World View 3 image that has earth observation data, we can enable the [eo extension](https://github.com/radiantearth/stac-spec/tree/v0.8.1/extensions/eo) to add band information." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To add eo information to an item we'll need to specify some more data. First, let's define the bands of World View 3:" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [], "source": [ "from pystac.extensions.eo import Band\n", "\n", "# From: https://www.spaceimagingme.com/downloads/sensors/datasheets/DG_WorldView3_DS_2014.pdf\n", "\n", "wv3_bands = [\n", " Band.create(\n", " name=\"Coastal\", description=\"Coastal: 400 - 450 nm\", common_name=\"coastal\"\n", " ),\n", " Band.create(name=\"Blue\", description=\"Blue: 450 - 510 nm\", common_name=\"blue\"),\n", " Band.create(name=\"Green\", description=\"Green: 510 - 580 nm\", common_name=\"green\"),\n", " Band.create(\n", " name=\"Yellow\", description=\"Yellow: 585 - 625 nm\", common_name=\"yellow\"\n", " ),\n", " Band.create(name=\"Red\", description=\"Red: 630 - 690 nm\", common_name=\"red\"),\n", " Band.create(\n", " name=\"Red Edge\", description=\"Red Edge: 705 - 745 nm\", common_name=\"rededge\"\n", " ),\n", " Band.create(\n", " name=\"Near-IR1\", description=\"Near-IR1: 770 - 895 nm\", common_name=\"nir08\"\n", " ),\n", " Band.create(\n", " name=\"Near-IR2\", description=\"Near-IR2: 860 - 1040 nm\", common_name=\"nir09\"\n", " ),\n", "]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice that we used the `.create` method create new band information." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can now create an Item, enable the eo extension, add the band information and add it to our catalog:" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [], "source": [ "eo_item = pystac.Item(\n", " id=\"local-image-eo\",\n", " geometry=footprint,\n", " bbox=bbox,\n", " datetime=datetime.utcnow(),\n", " properties={},\n", ")\n", "eo_item.ext.add(\"eo\")\n", "eo_item.ext.eo.bands = wv3_bands" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There are also [common metadata](https://github.com/radiantearth/stac-spec/blob/v0.9.0/item-spec/common-metadata.md) fields that we can use to capture additional information about the WorldView 3 imagery:" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [], "source": [ "eo_item.common_metadata.platform = \"Maxar\"\n", "eo_item.common_metadata.instruments = [\"WorldView3\"]\n", "eo_item.common_metadata.gsd = 0.3" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "
\n", "
\n", "
    \n", " \n", " \n", " \n", "
  • \n", " type\n", " \"Feature\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " stac_version\n", " \"1.0.0\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " id\n", " \"local-image-eo\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " properties\n", "
      \n", " \n", " \n", "
    • \n", " \n", " eo:bands\n", " [] 8 items\n", " \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 0\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"Coastal\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"coastal\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " description\n", " \"Coastal: 400 - 450 nm\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 1\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"Blue\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"blue\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " description\n", " \"Blue: 450 - 510 nm\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 2\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"Green\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"green\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " description\n", " \"Green: 510 - 580 nm\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 3\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"Yellow\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"yellow\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " description\n", " \"Yellow: 585 - 625 nm\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 4\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"Red\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"red\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " description\n", " \"Red: 630 - 690 nm\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 5\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"Red Edge\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"rededge\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " description\n", " \"Red Edge: 705 - 745 nm\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 6\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"Near-IR1\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"nir08\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " description\n", " \"Near-IR1: 770 - 895 nm\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 7\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"Near-IR2\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"nir09\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " description\n", " \"Near-IR2: 860 - 1040 nm\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " platform\n", " \"Maxar\"\n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " \n", " instruments\n", " [] 1 items\n", " \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 0\n", " \"WorldView3\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " gsd\n", " 0.3\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " datetime\n", " \"2023-10-12T15:35:17.781985Z\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " geometry\n", "
      \n", " \n", " \n", " \n", "
    • \n", " type\n", " \"Polygon\"\n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " \n", " coordinates\n", " [] 1 items\n", " \n", " \n", "
        \n", " \n", " \n", "
      • \n", " \n", " 0\n", " [] 5 items\n", " \n", " \n", "
          \n", " \n", " \n", "
        • \n", " \n", " 0\n", " [] 2 items\n", " \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 0\n", " 37.6616853489879\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 1\n", " 55.73478197572927\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
        • \n", " \n", " \n", "
        \n", " \n", "
          \n", " \n", " \n", "
        • \n", " \n", " 1\n", " [] 2 items\n", " \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 0\n", " 37.6616853489879\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 1\n", " 55.73882710285011\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
        • \n", " \n", " \n", "
        \n", " \n", "
          \n", " \n", " \n", "
        • \n", " \n", " 2\n", " [] 2 items\n", " \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 0\n", " 37.66573047610874\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 1\n", " 55.73882710285011\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
        • \n", " \n", " \n", "
        \n", " \n", "
          \n", " \n", " \n", "
        • \n", " \n", " 3\n", " [] 2 items\n", " \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 0\n", " 37.66573047610874\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 1\n", " 55.73478197572927\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
        • \n", " \n", " \n", "
        \n", " \n", "
          \n", " \n", " \n", "
        • \n", " \n", " 4\n", " [] 2 items\n", " \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 0\n", " 37.6616853489879\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 1\n", " 55.73478197572927\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
        • \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", "
      \n", " \n", "
    • \n", " \n", " \n", "
    \n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " \n", " links\n", " [] 0 items\n", " \n", " \n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " assets\n", "
      \n", " \n", "
    \n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " \n", " bbox\n", " [] 4 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", " 37.6616853489879\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 1\n", " 55.73478197572927\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 2\n", " 37.66573047610874\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 3\n", " 55.73882710285011\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", " \n", "
  • \n", " \n", " stac_extensions\n", " [] 1 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", " \"https://stac-extensions.github.io/eo/v1.1.0/schema.json\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", "
\n", "
\n", "
" ], "text/plain": [ "" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "eo_item" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can use the eo extension to add bands to the assets we add to the item:" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [], "source": [ "asset = pystac.Asset(href=img_path, media_type=pystac.MediaType.GEOTIFF)\n", "eo_item.add_asset(\"image\", asset)\n", "asset.ext.eo.bands = wv3_bands" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we look at the asset, we can see the appropriate band indexes are set:" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "
\n", "
\n", "
    \n", " \n", " \n", " \n", "
  • \n", " href\n", " \"/tmp/tmpdsdpun_y/image.tif\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " type\n", " \"image/tiff; application=geotiff\"\n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " \n", " eo:bands\n", " [] 8 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", "
        \n", " \n", " \n", " \n", "
      • \n", " name\n", " \"Coastal\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " common_name\n", " \"coastal\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " description\n", " \"Coastal: 400 - 450 nm\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 1\n", "
        \n", " \n", " \n", " \n", "
      • \n", " name\n", " \"Blue\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " common_name\n", " \"blue\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " description\n", " \"Blue: 450 - 510 nm\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 2\n", "
        \n", " \n", " \n", " \n", "
      • \n", " name\n", " \"Green\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " common_name\n", " \"green\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " description\n", " \"Green: 510 - 580 nm\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 3\n", "
        \n", " \n", " \n", " \n", "
      • \n", " name\n", " \"Yellow\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " common_name\n", " \"yellow\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " description\n", " \"Yellow: 585 - 625 nm\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 4\n", "
        \n", " \n", " \n", " \n", "
      • \n", " name\n", " \"Red\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " common_name\n", " \"red\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " description\n", " \"Red: 630 - 690 nm\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 5\n", "
        \n", " \n", " \n", " \n", "
      • \n", " name\n", " \"Red Edge\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " common_name\n", " \"rededge\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " description\n", " \"Red Edge: 705 - 745 nm\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 6\n", "
        \n", " \n", " \n", " \n", "
      • \n", " name\n", " \"Near-IR1\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " common_name\n", " \"nir08\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " description\n", " \"Near-IR1: 770 - 895 nm\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 7\n", "
        \n", " \n", " \n", " \n", "
      • \n", " name\n", " \"Near-IR2\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " common_name\n", " \"nir09\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " description\n", " \"Near-IR2: 860 - 1040 nm\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", "
\n", "
\n", "
" ], "text/plain": [ "" ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "asset" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's clear the in-memory catalog, add the EO item, and save to a new STAC:" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[]" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "catalog.clear_items()\n", "list(catalog.get_items())" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[]" ] }, "execution_count": 37, "metadata": {}, "output_type": "execute_result" } ], "source": [ "catalog.add_item(eo_item)\n", "list(catalog.get_items())" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [], "source": [ "catalog.normalize_and_save(\n", " root_href=os.path.join(tmp_dir.name, \"stac-eo\"),\n", " catalog_type=pystac.CatalogType.SELF_CONTAINED,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, if we read the catalog from the filesystem, PySTAC recognizes that the item implements eo and so use it's functionality, e.g. getting the bands off the asset:" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [], "source": [ "catalog2 = pystac.read_file(os.path.join(tmp_dir.name, \"stac-eo\", \"catalog.json\"))" ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[]" ] }, "execution_count": 40, "metadata": {}, "output_type": "execute_result" } ], "source": [ "assert isinstance(catalog2, pystac.Catalog)\n", "list(catalog2.get_items())" ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [], "source": [ "item: pystac.Item = next(catalog2.get_items(recursive=True))" ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [], "source": [ "assert item.ext.has(\"eo\")" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[,\n", " ,\n", " ,\n", " ,\n", " ,\n", " ,\n", " ,\n", " ]" ] }, "execution_count": 43, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.assets[\"image\"].ext.eo.bands" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Collections\n", "\n", "Collections are a subtype of Catalog that have some additional properties to make them more searchable. They also can define common properties so that items in the collection don't have to duplicate common data for each item. Let's create a collection to hold common properties between two images from the Spacenet 5 challenge.\n", "\n", "First we'll get another image, and it's bbox and footprint:" ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "('/tmp/tmpdsdpun_y/image.tif', )" ] }, "execution_count": 44, "metadata": {}, "output_type": "execute_result" } ], "source": [ "url2 = (\n", " \"https://spacenet-dataset.s3.amazonaws.com/\"\n", " \"spacenet/SN5_roads/train/AOI_7_Moscow/MS/\"\n", " \"SN5_roads_train_AOI_7_Moscow_MS_chip997.tif\"\n", ")\n", "img_path2 = os.path.join(tmp_dir.name, \"image.tif\")\n", "urllib.request.urlretrieve(url2, img_path2)" ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [], "source": [ "bbox2, footprint2 = get_bbox_and_footprint(img_path2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can take a look at the pydocs for Collection to see what information we need to supply in order to satisfy the spec." ] }, { "cell_type": "code", "execution_count": 46, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[0;31mInit signature:\u001b[0m\n", "\u001b[0mpystac\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mCollection\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mid\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'str'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mdescription\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'str'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mextent\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Extent'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mtitle\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[str]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mstac_extensions\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[List[str]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mhref\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[str]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mextra_fields\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Dict[str, Any]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mcatalog_type\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[CatalogType]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mlicense\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'str'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m'proprietary'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mkeywords\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[List[str]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mproviders\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[List[Provider]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0msummaries\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Summaries]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0massets\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Dict[str, Asset]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mDocstring:\u001b[0m \n", "A Collection extends the Catalog spec with additional metadata that helps\n", "enable discovery.\n", "\n", "Args:\n", " id : Identifier for the collection. Must be unique within the STAC.\n", " description : Detailed multi-line description to fully explain the\n", " collection. `CommonMark 0.29 syntax `_ MAY\n", " be used for rich text representation.\n", " extent : Spatial and temporal extents that describe the bounds of\n", " all items contained within this Collection.\n", " title : Optional short descriptive one-line title for the\n", " collection.\n", " stac_extensions : Optional list of extensions the Collection\n", " implements.\n", " href : Optional HREF for this collection, which be set as the\n", " collection's self link's HREF.\n", " catalog_type : Optional catalog type for this catalog. Must\n", " be one of the values in :class`~pystac.CatalogType`.\n", " license : Collection's license(s) as a\n", " `SPDX License identifier `_,\n", " `various`, or `proprietary`. If collection includes\n", " data with multiple different licenses, use `various` and add a link for\n", " each. Defaults to 'proprietary'.\n", " keywords : Optional list of keywords describing the collection.\n", " providers : Optional list of providers of this Collection.\n", " summaries : An optional map of property summaries,\n", " either a set of values or statistics such as a range.\n", " extra_fields : Extra fields that are part of the top-level\n", " JSON properties of the Collection.\n", " assets : A dictionary mapping string keys to :class:`~pystac.Asset` objects. All\n", " :class:`~pystac.Asset` values in the dictionary will have their\n", " :attr:`~pystac.Asset.owner` attribute set to the created Collection.\n", "\u001b[0;31mFile:\u001b[0m ~/pystac/pystac/collection.py\n", "\u001b[0;31mType:\u001b[0m ABCMeta\n", "\u001b[0;31mSubclasses:\u001b[0m " ] } ], "source": [ "?pystac.Collection" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Beyond what a Catalog requires, a Collection requires a license, and an `Extent` that describes the range of space and time that the items it hold occupy." ] }, { "cell_type": "code", "execution_count": 47, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[0;31mInit signature:\u001b[0m\n", "\u001b[0mpystac\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mExtent\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mspatial\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'SpatialExtent'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mtemporal\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'TemporalExtent'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m \u001b[0mextra_fields\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'Optional[Dict[str, Any]]'\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mDocstring:\u001b[0m \n", "Describes the spatiotemporal extents of a Collection.\n", "\n", "Args:\n", " spatial : Potential spatial extent covered by the collection.\n", " temporal : Potential temporal extent covered by the collection.\n", " extra_fields : Dictionary containing additional top-level fields defined on the\n", " Extent object.\n", "\u001b[0;31mFile:\u001b[0m ~/pystac/pystac/collection.py\n", "\u001b[0;31mType:\u001b[0m type\n", "\u001b[0;31mSubclasses:\u001b[0m " ] } ], "source": [ "?pystac.Extent" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "An Extent is comprised of a SpatialExtent and a TemporalExtent. These hold one or more bounding boxes and time intervals, respectively, that completely cover the items contained in the collections.\n", "\n", "Let's start with creating two new items - these will be core Items. We can set these items to implement the `eo` extension by specifying them in the `stac_extensions`." ] }, { "cell_type": "code", "execution_count": 48, "metadata": {}, "outputs": [], "source": [ "collection_item = pystac.Item(\n", " id=\"local-image-col-1\",\n", " geometry=footprint,\n", " bbox=bbox,\n", " datetime=datetime.utcnow(),\n", " properties={},\n", ")\n", "\n", "collection_item.common_metadata.gsd = 0.3\n", "collection_item.common_metadata.platform = \"Maxar\"\n", "collection_item.common_metadata.instruments = [\"WorldView3\"]\n", "\n", "asset = pystac.Asset(href=img_path, media_type=pystac.MediaType.GEOTIFF)\n", "collection_item.add_asset(\"image\", asset)\n", "asset.ext.add(\"eo\")\n", "asset.ext.eo.bands = wv3_bands\n", "\n", "collection_item2 = pystac.Item(\n", " id=\"local-image-col-2\",\n", " geometry=footprint2,\n", " bbox=bbox2,\n", " datetime=datetime.utcnow(),\n", " properties={},\n", ")\n", "\n", "collection_item2.common_metadata.gsd = 0.3\n", "collection_item2.common_metadata.platform = \"Maxar\"\n", "collection_item2.common_metadata.instruments = [\"WorldView3\"]\n", "\n", "asset2 = pystac.Asset(href=img_path, media_type=pystac.MediaType.GEOTIFF)\n", "collection_item2.add_asset(\"image\", asset2)\n", "asset2.ext.add(\"eo\")\n", "asset2.ext.eo.bands = [\n", " band for band in wv3_bands if band.name in [\"Red\", \"Green\", \"Blue\"]\n", "]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can use our two items' metadata to find out what the proper bounds are:" ] }, { "cell_type": "code", "execution_count": 49, "metadata": {}, "outputs": [], "source": [ "from shapely.geometry import shape\n", "\n", "unioned_footprint = shape(footprint).union(shape(footprint2))\n", "collection_bbox = list(unioned_footprint.bounds)\n", "spatial_extent = pystac.SpatialExtent(bboxes=[collection_bbox])" ] }, { "cell_type": "code", "execution_count": 50, "metadata": {}, "outputs": [], "source": [ "collection_interval = sorted([collection_item.datetime, collection_item2.datetime])\n", "temporal_extent = pystac.TemporalExtent(intervals=[collection_interval])" ] }, { "cell_type": "code", "execution_count": 51, "metadata": {}, "outputs": [], "source": [ "collection_extent = pystac.Extent(spatial=spatial_extent, temporal=temporal_extent)" ] }, { "cell_type": "code", "execution_count": 52, "metadata": {}, "outputs": [], "source": [ "collection = pystac.Collection(\n", " id=\"wv3-images\",\n", " description=\"Spacenet 5 images over Moscow\",\n", " extent=collection_extent,\n", " license=\"CC-BY-SA-4.0\",\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now if we add our items to our Collection, and our Collection to our Catalog, we get the following STAC that can be saved:" ] }, { "cell_type": "code", "execution_count": 53, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[>,\n", " >]" ] }, "execution_count": 53, "metadata": {}, "output_type": "execute_result" } ], "source": [ "collection.add_items([collection_item, collection_item2])" ] }, { "cell_type": "code", "execution_count": 54, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "
\n", "
\n", "
    \n", " \n", " \n", " \n", "
  • \n", " rel\n", " \"child\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " href\n", " \"./wv3-images/collection.json\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " type\n", " \"application/json\"\n", "
  • \n", " \n", " \n", " \n", "
\n", "
\n", "
" ], "text/plain": [ ">" ] }, "execution_count": 54, "metadata": {}, "output_type": "execute_result" } ], "source": [ "catalog.clear_items()\n", "catalog.clear_children()\n", "catalog.add_child(collection)" ] }, { "cell_type": "code", "execution_count": 55, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "* \n", " * \n", " * \n", " * \n" ] } ], "source": [ "catalog.describe()" ] }, { "cell_type": "code", "execution_count": 56, "metadata": {}, "outputs": [], "source": [ "catalog.normalize_and_save(\n", " root_href=os.path.join(tmp_dir.name, \"stac-collection\"),\n", " catalog_type=pystac.CatalogType.SELF_CONTAINED,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Cleanup\n", "\n", "Don't forget to clean up the temporary directory!" ] }, { "cell_type": "code", "execution_count": 57, "metadata": {}, "outputs": [], "source": [ "tmp_dir.cleanup()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Creating a STAC of imagery from Spacenet 5 data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, let's take what we've learned and create a Catalog with more data in it.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Allowing PySTAC to read from AWS S3\n", "\n", "PySTAC aims to be virtually zero-dependency (notwithstanding the why-isn't-this-in-stdlib datetime-util), so it doesn't have the ability to read from or write to anything but the local file system. However, we can hook into PySTAC's IO in the following way. Learn more about how to customize I/O in STAC from the [documentation](https://pystac.readthedocs.io/en/stable/concepts.html#i-o-in-pystac):" ] }, { "cell_type": "code", "execution_count": 58, "metadata": {}, "outputs": [], "source": [ "from typing import Union, Any\n", "from urllib.parse import urlparse\n", "\n", "import boto3\n", "from pystac import Link\n", "from pystac.stac_io import DefaultStacIO\n", "\n", "\n", "class CustomStacIO(DefaultStacIO):\n", " def __init__(self):\n", " self.s3 = boto3.resource(\"s3\")\n", "\n", " def read_text(self, source: Union[str, Link], *args: Any, **kwargs: Any) -> str:\n", " parsed = urlparse(source)\n", " if parsed.scheme == \"s3\":\n", " bucket = parsed.netloc\n", " key = parsed.path[1:]\n", "\n", " obj = self.s3.Object(bucket, key)\n", " return obj.get()[\"Body\"].read().decode(\"utf-8\")\n", " else:\n", " return super().read_text(source, *args, **kwargs)\n", "\n", " def write_text(\n", " self, dest: Union[str, Link], txt: str, *args: Any, **kwargs: Any\n", " ) -> None:\n", " parsed = urlparse(dest)\n", " if parsed.scheme == \"s3\":\n", " bucket = parsed.netloc\n", " key = parsed.path[1:]\n", " self.s3.Object(bucket, key).put(Body=txt, ContentEncoding=\"utf-8\")\n", " else:\n", " super().write_text(dest, txt, *args, **kwargs)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We'll need a utility to list keys for reading the lists of files from S3:" ] }, { "cell_type": "code", "execution_count": 59, "metadata": {}, "outputs": [], "source": [ "# From https://alexwlchan.net/2017/07/listing-s3-keys/\n", "from botocore import UNSIGNED\n", "from botocore.config import Config\n", "\n", "\n", "def get_s3_keys(bucket, prefix):\n", " \"\"\"Generate all the keys in an S3 bucket.\"\"\"\n", " s3 = boto3.client(\"s3\", config=Config(signature_version=UNSIGNED))\n", " kwargs = {\"Bucket\": bucket, \"Prefix\": prefix}\n", " while True:\n", " resp = s3.list_objects_v2(**kwargs)\n", " for obj in resp[\"Contents\"]:\n", " yield obj[\"Key\"]\n", "\n", " try:\n", " kwargs[\"ContinuationToken\"] = resp[\"NextContinuationToken\"]\n", " except KeyError:\n", " break" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's make a STAC of imagery over Moscow as part of the Spacenet 5 challenge. As a first step, we can list out the imagery and extract IDs from each of the chips." ] }, { "cell_type": "code", "execution_count": 60, "metadata": {}, "outputs": [], "source": [ "moscow_training_chip_uris = list(\n", " get_s3_keys(\n", " bucket=\"spacenet-dataset\", prefix=\"spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/\"\n", " )\n", ")" ] }, { "cell_type": "code", "execution_count": 61, "metadata": {}, "outputs": [], "source": [ "import re\n", "\n", "chip_id_to_data = {}\n", "\n", "\n", "def get_chip_id(uri):\n", " return re.search(r\".*\\_chip(\\d+)\\.\", uri).group(1)\n", "\n", "\n", "for uri in moscow_training_chip_uris:\n", " chip_id = get_chip_id(uri)\n", " chip_id_to_data[chip_id] = {\"img\": \"s3://spacenet-dataset/{}\".format(uri)}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For this tutorial, we'll only take a subset of the data." ] }, { "cell_type": "code", "execution_count": 62, "metadata": {}, "outputs": [], "source": [ "chip_id_to_data = dict(list(chip_id_to_data.items())[:10])" ] }, { "cell_type": "code", "execution_count": 63, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'0': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip0.tif'},\n", " '1': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1.tif'},\n", " '10': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip10.tif'},\n", " '100': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip100.tif'},\n", " '1000': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1000.tif'},\n", " '1001': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1001.tif'},\n", " '1002': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1002.tif'},\n", " '1003': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1003.tif'},\n", " '1004': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1004.tif'},\n", " '1005': {'img': 's3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1005.tif'}}" ] }, "execution_count": 63, "metadata": {}, "output_type": "execute_result" } ], "source": [ "chip_id_to_data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's turn each of those chips into a STAC Item that represents the image." ] }, { "cell_type": "code", "execution_count": 64, "metadata": {}, "outputs": [], "source": [ "chip_id_to_items = {}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We'll create core `Item`s for our imagery, but mark them with the `eo` extension as we did above, and store the `eo` data in a `Collection`.\n", "\n", "Note that the image CRS is in WGS:84 (Lat/Lng). If it wasn't, we'd have to reproject the footprint to WGS:84 in order to be compliant with the spec (which can easily be done with [pyproj](https://github.com/pyproj4/pyproj)).\n", "\n", "Here we're taking advantage of `rasterio`'s ability to read S3 URIs, which only grabs the GeoTIFF metadata and does not pull the whole file down." ] }, { "cell_type": "code", "execution_count": 65, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip0.tif\n", "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1.tif\n", "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip10.tif\n", "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip100.tif\n", "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1000.tif\n", "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1001.tif\n", "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1002.tif\n", "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1003.tif\n", "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1004.tif\n", "Processing s3://spacenet-dataset/spacenet/SN5_roads/train/AOI_7_Moscow/PS-MS/SN5_roads_train_AOI_7_Moscow_PS-MS_chip1005.tif\n" ] } ], "source": [ "import os\n", "\n", "os.environ[\"AWS_NO_SIGN_REQUEST\"] = \"true\"\n", "\n", "for chip_id in chip_id_to_data:\n", " img_uri = chip_id_to_data[chip_id][\"img\"]\n", " print(\"Processing {}\".format(img_uri))\n", " bbox, footprint = get_bbox_and_footprint(img_uri)\n", "\n", " item = pystac.Item(\n", " id=\"img_{}\".format(chip_id),\n", " geometry=footprint,\n", " bbox=bbox,\n", " datetime=datetime.utcnow(),\n", " properties={},\n", " )\n", "\n", " item.common_metadata.gsd = 0.3\n", " item.common_metadata.platform = \"Maxar\"\n", " item.common_metadata.instruments = [\"WorldView3\"]\n", "\n", " item.ext.add(\"eo\")\n", " item.ext.eo.bands = wv3_bands\n", " asset = pystac.Asset(href=img_uri, media_type=pystac.MediaType.COG)\n", " item.add_asset(key=\"ps-ms\", asset=asset)\n", " asset.ext.eo.bands = wv3_bands\n", " chip_id_to_items[chip_id] = item" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Creating the Collection\n", "\n", "All of these images are over Moscow. In Spacenet 5, we have a couple cities that have imagery; a good way to separate these collections of imagery. We can store all of the common `eo` metadata in the collection." ] }, { "cell_type": "code", "execution_count": 66, "metadata": {}, "outputs": [], "source": [ "from shapely.geometry import shape, MultiPolygon\n", "\n", "footprints = list(map(lambda i: shape(i.geometry).envelope, chip_id_to_items.values()))\n", "collection_bbox = MultiPolygon(footprints).bounds\n", "spatial_extent = pystac.SpatialExtent(bboxes=[collection_bbox])" ] }, { "cell_type": "code", "execution_count": 67, "metadata": {}, "outputs": [], "source": [ "datetimes = sorted(list(map(lambda i: i.datetime, chip_id_to_items.values())))\n", "temporal_extent = pystac.TemporalExtent(intervals=[[datetimes[0], datetimes[-1]]])" ] }, { "cell_type": "code", "execution_count": 68, "metadata": {}, "outputs": [], "source": [ "collection_extent = pystac.Extent(spatial=spatial_extent, temporal=temporal_extent)" ] }, { "cell_type": "code", "execution_count": 69, "metadata": {}, "outputs": [], "source": [ "collection = pystac.Collection(\n", " id=\"wv3-images\",\n", " description=\"Spacenet 5 images over Moscow\",\n", " extent=collection_extent,\n", " license=\"CC-BY-SA-4.0\",\n", ")" ] }, { "cell_type": "code", "execution_count": 70, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[>,\n", " >,\n", " >,\n", " >,\n", " >,\n", " >,\n", " >,\n", " >,\n", " >,\n", " >]" ] }, "execution_count": 70, "metadata": {}, "output_type": "execute_result" } ], "source": [ "collection.add_items(chip_id_to_items.values())" ] }, { "cell_type": "code", "execution_count": 71, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "* \n", " * \n", " * \n", " * \n", " * \n", " * \n", " * \n", " * \n", " * \n", " * \n", " * \n" ] } ], "source": [ "collection.describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, we can create a Catalog and add the collection." ] }, { "cell_type": "code", "execution_count": 72, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "
\n", "
\n", "
    \n", " \n", " \n", " \n", "
  • \n", " rel\n", " \"child\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " href\n", " None\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " type\n", " \"application/json\"\n", "
  • \n", " \n", " \n", " \n", "
\n", "
\n", "
" ], "text/plain": [ ">" ] }, "execution_count": 72, "metadata": {}, "output_type": "execute_result" } ], "source": [ "catalog = pystac.Catalog(id=\"spacenet5\", description=\"Spacenet 5 Data (Test)\")\n", "catalog.add_child(collection)" ] }, { "cell_type": "code", "execution_count": 73, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "* \n", " * \n", " * \n", " * \n", " * \n", " * \n", " * \n", " * \n", " * \n", " * \n", " * \n", " * \n" ] } ], "source": [ "catalog.describe()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.6" } }, "nbformat": 4, "nbformat_minor": 4 } pystac-1.9.0/docs/tutorials/how-to-read-data-from-stac.ipynb000066400000000000000000004426271451576074700240320ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "id": "be2c57c1-798a-4eaf-b2b8-41c261b657d1", "metadata": {}, "source": [ "# How to read data from STAC\n", "\n", "This notebook shows how to read the data in from a STAC asset using [xarray](https://docs.xarray.dev/en/stable/) and a little hidden helper library called [xpystac](https://pypi.org/project/xpystac/).\n", "\n", "## tl;dr\n", "\n", "For any PySTAC object that can be represented as an ndimensional dataset you can read the data using the following command:\n", "\n", "```python\n", "xr.open_dataset(object)\n", "```\n", "\n", "## Dependencies\n", "\n", "There are lots of optional dependencies depending on where and how the data you are interested in are stored. Here are some of the libraries that you will probably need:\n", "\n", "- dask - to delay data loading until access\n", "- fsspec - to access data from remote storage\n", "- pystac - STAC object structures\n", "- xarray, rioxarray - data structures\n", "- xpystac, stackstac - helper for loading pystac into xarray objects" ] }, { "cell_type": "code", "execution_count": 1, "id": "11dddb09-6313-4822-90ba-26eb6e5c143b", "metadata": {}, "outputs": [], "source": [ "!pip install adlfs dask 'fsspec[http]' planetary_computer stackstac xarray xpystac zarr --quiet" ] }, { "cell_type": "markdown", "id": "ad3fb6dc-3529-47bd-a5b3-f5260f23db88", "metadata": {}, "source": [ "Despite all these install instructions, the import block is very straightforward" ] }, { "cell_type": "code", "execution_count": 2, "id": "2a8afebd-b397-4e7a-b448-0f59cc030e66", "metadata": {}, "outputs": [], "source": [ "import pystac\n", "import xarray as xr" ] }, { "cell_type": "markdown", "id": "6b24745c-b2d5-43d6-9c7e-66458b3a88e3", "metadata": {}, "source": [ "## Examples\n", "\n", "Here are a few examples of the different types of objects that you can open in xarray." ] }, { "cell_type": "markdown", "id": "30da7cfd-2861-4095-b15b-9952a7d824d9", "metadata": {}, "source": [ "### COGs\n", "\n", "Read all the data from the COGs referenced by the assets on an item." ] }, { "cell_type": "code", "execution_count": 3, "id": "c77432e6-8b0d-44d2-a947-ec74a529b8cb", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "
<xarray.Dataset>\n",
       "Dimensions:                      (time: 1, y: 7802, x: 7762, band: 19)\n",
       "Coordinates: (12/32)\n",
       "  * time                         (time) datetime64[ns] 2023-04-08T23:37:51.63...\n",
       "    id                           (time) <U31 ...\n",
       "  * x                            (x) float64 3.774e+05 3.774e+05 ... 6.102e+05\n",
       "  * y                            (y) float64 -3.713e+06 ... -3.947e+06\n",
       "    proj:shape                   object ...\n",
       "    sci:doi                      <U16 ...\n",
       "    ...                           ...\n",
       "    raster:bands                 (band) object ...\n",
       "    classification:bitfields     (band) object ...\n",
       "    common_name                  (band) object ...\n",
       "    center_wavelength            (band) object ...\n",
       "    full_width_half_max          (band) object ...\n",
       "    epsg                         int64 ...\n",
       "Dimensions without coordinates: band\n",
       "Data variables: (12/19)\n",
       "    qa                           (time, y, x) float64 ...\n",
       "    red                          (time, y, x) float64 ...\n",
       "    blue                         (time, y, x) float64 ...\n",
       "    drad                         (time, y, x) float64 ...\n",
       "    emis                         (time, y, x) float64 ...\n",
       "    emsd                         (time, y, x) float64 ...\n",
       "    ...                           ...\n",
       "    swir16                       (time, y, x) float64 ...\n",
       "    swir22                       (time, y, x) float64 ...\n",
       "    coastal                      (time, y, x) float64 ...\n",
       "    qa_pixel                     (time, y, x) float64 ...\n",
       "    qa_radsat                    (time, y, x) float64 ...\n",
       "    qa_aerosol                   (time, y, x) float64 ...\n",
       "Attributes:\n",
       "    spec:        RasterSpec(epsg=32656, bounds=(377370.0, -3947130.0, 610230....\n",
       "    crs:         epsg:32656\n",
       "    transform:   | 30.00, 0.00, 377370.00|\\n| 0.00,-30.00,-3713070.00|\\n| 0.0...\n",
       "    resolution:  30.0
" ], "text/plain": [ "\n", "Dimensions: (time: 1, y: 7802, x: 7762, band: 19)\n", "Coordinates: (12/32)\n", " * time (time) datetime64[ns] 2023-04-08T23:37:51.63...\n", " id (time) \n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "
<xarray.Dataset>\n",
       "Dimensions:                  (time: 14965, y: 584, x: 284, nv: 2)\n",
       "Coordinates:\n",
       "    lat                      (y, x) float32 ...\n",
       "    lon                      (y, x) float32 ...\n",
       "  * time                     (time) datetime64[ns] 1980-01-01T12:00:00 ... 20...\n",
       "  * x                        (x) float32 -5.802e+06 -5.801e+06 ... -5.519e+06\n",
       "  * y                        (y) float32 -3.9e+04 -4e+04 ... -6.21e+05 -6.22e+05\n",
       "Dimensions without coordinates: nv\n",
       "Data variables:\n",
       "    dayl                     (time, y, x) float32 ...\n",
       "    lambert_conformal_conic  int16 ...\n",
       "    prcp                     (time, y, x) float32 ...\n",
       "    srad                     (time, y, x) float32 ...\n",
       "    swe                      (time, y, x) float32 ...\n",
       "    time_bnds                (time, nv) datetime64[ns] ...\n",
       "    tmax                     (time, y, x) float32 ...\n",
       "    tmin                     (time, y, x) float32 ...\n",
       "    vp                       (time, y, x) float32 ...\n",
       "    yearday                  (time) int16 ...\n",
       "Attributes:\n",
       "    Conventions:       CF-1.6\n",
       "    Version_data:      Daymet Data Version 4.0\n",
       "    Version_software:  Daymet Software Version 4.0\n",
       "    citation:          Please see http://daymet.ornl.gov/ for current Daymet ...\n",
       "    references:        Please see http://daymet.ornl.gov/ for current informa...\n",
       "    source:            Daymet Software Version 4.0\n",
       "    start_year:        1980
" ], "text/plain": [ "\n", "Dimensions: (time: 14965, y: 584, x: 284, nv: 2)\n", "Coordinates:\n", " lat (y, x) float32 ...\n", " lon (y, x) float32 ...\n", " * time (time) datetime64[ns] 1980-01-01T12:00:00 ... 20...\n", " * x (x) float32 -5.802e+06 -5.801e+06 ... -5.519e+06\n", " * y (y) float32 -3.9e+04 -4e+04 ... -6.21e+05 -6.22e+05\n", "Dimensions without coordinates: nv\n", "Data variables:\n", " dayl (time, y, x) float32 ...\n", " lambert_conformal_conic int16 ...\n", " prcp (time, y, x) float32 ...\n", " srad (time, y, x) float32 ...\n", " swe (time, y, x) float32 ...\n", " time_bnds (time, nv) datetime64[ns] ...\n", " tmax (time, y, x) float32 ...\n", " tmin (time, y, x) float32 ...\n", " vp (time, y, x) float32 ...\n", " yearday (time) int16 ...\n", "Attributes:\n", " Conventions: CF-1.6\n", " Version_data: Daymet Data Version 4.0\n", " Version_software: Daymet Software Version 4.0\n", " citation: Please see http://daymet.ornl.gov/ for current Daymet ...\n", " references: Please see http://daymet.ornl.gov/ for current informa...\n", " source: Daymet Software Version 4.0\n", " start_year: 1980" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "daymet_collection = pystac.Collection.from_file(\n", " \"https://planetarycomputer.microsoft.com/api/stac/v1/collections/daymet-daily-hi\"\n", ")\n", "daymet_asset = daymet_collection.assets[\"zarr-abfs\"]\n", "\n", "xr.open_dataset(daymet_asset)" ] }, { "cell_type": "markdown", "id": "fd4e0c53-90b0-4276-9caf-9014aa0a31f9", "metadata": {}, "source": [ "### Reference file\n", "\n", "If the collection has a reference file we can use that" ] }, { "cell_type": "code", "execution_count": 5, "id": "00efc688-a8b8-4b45-8ee8-1aa076a870f4", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "
<xarray.Dataset>\n",
       "Dimensions:  (time: 23741, lat: 600, lon: 1440)\n",
       "Coordinates:\n",
       "  * lat      (lat) float64 -59.88 -59.62 -59.38 -59.12 ... 89.38 89.62 89.88\n",
       "  * lon      (lon) float64 0.125 0.375 0.625 0.875 ... 359.1 359.4 359.6 359.9\n",
       "  * time     (time) datetime64[us] 1950-01-01T12:00:00 ... 2014-12-31T12:00:00\n",
       "Data variables:\n",
       "    hurs     (time, lat, lon) float32 ...\n",
       "    huss     (time, lat, lon) float32 ...\n",
       "    pr       (time, lat, lon) float32 ...\n",
       "    rlds     (time, lat, lon) float32 ...\n",
       "    rsds     (time, lat, lon) float32 ...\n",
       "    sfcWind  (time, lat, lon) float32 ...\n",
       "    tas      (time, lat, lon) float32 ...\n",
       "    tasmax   (time, lat, lon) float32 ...\n",
       "    tasmin   (time, lat, lon) float32 ...\n",
       "Attributes: (12/22)\n",
       "    Conventions:           CF-1.7\n",
       "    activity:              NEX-GDDP-CMIP6\n",
       "    cmip6_institution_id:  CSIRO-ARCCSS\n",
       "    cmip6_license:         CC-BY-SA 4.0\n",
       "    cmip6_source_id:       ACCESS-CM2\n",
       "    contact:               Dr. Rama Nemani: rama.nemani@nasa.gov, Dr. Bridget...\n",
       "    ...                    ...\n",
       "    scenario:              historical\n",
       "    source:                BCSD\n",
       "    title:                 ACCESS-CM2, r1i1p1f1, historical, global downscale...\n",
       "    tracking_id:           16d27564-470f-41ea-8077-f4cc3efa5bfe\n",
       "    variant_label:         r1i1p1f1\n",
       "    version:               1.0
" ], "text/plain": [ "\n", "Dimensions: (time: 23741, lat: 600, lon: 1440)\n", "Coordinates:\n", " * lat (lat) float64 -59.88 -59.62 -59.38 -59.12 ... 89.38 89.62 89.88\n", " * lon (lon) float64 0.125 0.375 0.625 0.875 ... 359.1 359.4 359.6 359.9\n", " * time (time) datetime64[us] 1950-01-01T12:00:00 ... 2014-12-31T12:00:00\n", "Data variables:\n", " hurs (time, lat, lon) float32 ...\n", " huss (time, lat, lon) float32 ...\n", " pr (time, lat, lon) float32 ...\n", " rlds (time, lat, lon) float32 ...\n", " rsds (time, lat, lon) float32 ...\n", " sfcWind (time, lat, lon) float32 ...\n", " tas (time, lat, lon) float32 ...\n", " tasmax (time, lat, lon) float32 ...\n", " tasmin (time, lat, lon) float32 ...\n", "Attributes: (12/22)\n", " Conventions: CF-1.7\n", " activity: NEX-GDDP-CMIP6\n", " cmip6_institution_id: CSIRO-ARCCSS\n", " cmip6_license: CC-BY-SA 4.0\n", " cmip6_source_id: ACCESS-CM2\n", " contact: Dr. Rama Nemani: rama.nemani@nasa.gov, Dr. Bridget...\n", " ... ...\n", " scenario: historical\n", " source: BCSD\n", " title: ACCESS-CM2, r1i1p1f1, historical, global downscale...\n", " tracking_id: 16d27564-470f-41ea-8077-f4cc3efa5bfe\n", " variant_label: r1i1p1f1\n", " version: 1.0" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cmip6_collection = pystac.Collection.from_file(\n", " \"https://planetarycomputer.microsoft.com/api/stac/v1/collections/nasa-nex-gddp-cmip6\"\n", ")\n", "cmip6_asset = cmip6_collection.assets[\"ACCESS-CM2.historical\"]\n", "\n", "xr.open_dataset(cmip6_asset)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.15" } }, "nbformat": 4, "nbformat_minor": 5 } pystac-1.9.0/docs/tutorials/pystac-introduction.ipynb000066400000000000000000010117721451576074700231200ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# PySTAC Introduction" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This tutorial includes a basic introduction on reading, writing, and creating STAC objects using Pystac.\n", "\n", "It is adapted from the tutorials within the [sat-stac repo](https://github.com/sat-utils/sat-stac/blob/master/tutorial-1.ipynb).\n", "\n", "It uses an example stac stored in the `../example-catalog` directory along-side this notebook. The example stac has the following format:\n", "\n", "```\n", "../example-catalog\n", "├── catalog.json\n", "└── landsat-8-l1\n", " ├── 2018-05\n", " │ └── LC80150322018141LGN00.json\n", " ├── 2018-06\n", " │ ├── LC80140332018166LGN00.json\n", " │ └── LC80300332018166LGN00.json\n", " ├── 2018-07\n", " │ └── LC80150332018189LGN00.json\n", " └── collection.json\n", "```" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import pystac" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Working with existing catalogs" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Open a root catalog from it's json file" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "cat = pystac.Catalog.from_file(\"../example-catalog/catalog.json\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see all elements of the STAC using the `describe` method" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "* \n", " * \n", " * \n", " * \n", " * \n", " * \n" ] } ], "source": [ "cat.describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Each STAC object has links that you can use to traverse the STAC tree" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[,\n", " >,\n", " >]" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cat.links" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Pystac has several methods that allow you to access links:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[>]" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Get all child links\n", "cat.get_child_links()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "or the children directly:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[]" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "list(cat.get_children())" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "
\n", "
\n", "
    \n", " \n", " \n", " \n", "
  • \n", " type\n", " \"Collection\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " id\n", " \"landsat-8-l1\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " stac_version\n", " \"1.0.0\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " description\n", " \"Landsat 8 imagery radiometrically calibrated and orthorectified using ground points and Digital Elevation Model (DEM) data to correct relief displacement.\"\n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " \n", " links\n", " [] 7 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"root\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/catalog.json\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"application/json\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"STAC for Landsat data\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 1\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"item\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-06/LC80140332018166LGN00.json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 2\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"item\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-05/LC80150322018141LGN00.json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 3\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"item\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-07/LC80150332018189LGN00.json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 4\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"item\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-06/LC80300332018166LGN00.json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 5\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"self\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/collection.json\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"application/json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 6\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"parent\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/catalog.json\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"application/json\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"STAC for Landsat data\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", " \n", "
  • \n", " \n", " stac_extensions\n", " [] 1 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", " \"https://example.com/stac/landsat-extension/1.0/schema.json\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " properties\n", "
      \n", " \n", " \n", " \n", "
    • \n", " collection\n", " \"landsat-8-l1\"\n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " \n", " instruments\n", " [] 1 items\n", " \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 0\n", " \"OLI_TIRS\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " view:sun_azimuth\n", " 149.01607154\n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " \n", " eo:bands\n", " [] 11 items\n", " \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 0\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B1\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.02\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.44\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"coastal\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 1\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B2\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.06\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.48\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"blue\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 2\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B3\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.06\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.56\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"green\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 3\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B4\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.04\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.65\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"red\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 4\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B5\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.03\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.86\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"nir\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 5\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B6\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.08\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 1.6\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"swir16\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 6\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B7\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.22\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 2.2\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"swir22\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 7\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B8\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.18\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.59\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"pan\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 8\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B9\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.02\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 1.37\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"cirrus\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 9\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B10\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.8\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 10.9\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"lwir11\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 10\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B11\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 1\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 12\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"lwir2\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " view:off_nadir\n", " 0\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " view:azimuth\n", " 0\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " platform\n", " \"landsat-8\"\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " gsd\n", " 15\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " view:sun_elevation\n", " 59.214247\n", "
    • \n", " \n", " \n", " \n", "
    \n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " title\n", " \"Landsat 8 L1\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " extent\n", "
      \n", " \n", " \n", " \n", "
    • \n", " spatial\n", "
        \n", " \n", " \n", "
      • \n", " \n", " bbox\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", "
        • \n", " \n", " 0\n", " [] 4 items\n", " \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 0\n", " -180.0\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 1\n", " -90.0\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 2\n", " 180.0\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 3\n", " 90.0\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
        • \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " temporal\n", "
        \n", " \n", " \n", "
      • \n", " \n", " interval\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", "
        • \n", " \n", " 0\n", " [] 2 items\n", " \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 0\n", " \"2018-05-21T15:44:59Z\"\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 1\n", " \"2018-07-08T15:45:34Z\"\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
        • \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " license\n", " \"proprietary\"\n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " \n", " keywords\n", " [] 3 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", " \"landsat\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 1\n", " \"earth observation\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 2\n", " \"usgs\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", " \n", "
  • \n", " \n", " providers\n", " [] 1 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", "
        \n", " \n", " \n", " \n", "
      • \n", " name\n", " \"Development Seed\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", " \n", "
        • \n", " 0\n", " \"processor\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " url\n", " \"https://github.com/sat-utils/sat-api\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", "
\n", "
\n", "
" ], "text/plain": [ "" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# or a single child by id\n", "cat.get_child(\"landsat-8-l1\")" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "
\n", "
\n", "
    \n", " \n", " \n", " \n", "
  • \n", " rel\n", " \"self\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/catalog.json\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " type\n", " \"application/json\"\n", "
  • \n", " \n", " \n", " \n", "
\n", "
\n", "
" ], "text/plain": [ "" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Get a single link by 'rel'\n", "cat.get_single_link(\"self\")" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[]" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Get item links directly within this catalog (there are none for this catalog)\n", "cat.get_item_links()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "or the items directly:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[]" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# get item objects\n", "list(cat.get_items())" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[,\n", " ,\n", " ,\n", " ]" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# get all items anywhere below this catalog on the STAC tree\n", "list(cat.get_items(recursive=True))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can access the stac item from a link using the `target` property" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ ">\n" ] } ], "source": [ "l = cat.get_single_link(\"child\")\n", "print(l)" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "print(l.target)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can convert any stac item to a python dict using the `to_dict` method." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'type': 'Catalog',\n", " 'id': 'landsat-stac-collection-catalog',\n", " 'stac_version': '1.0.0',\n", " 'description': 'STAC for Landsat data',\n", " 'links': [{'rel': 'root',\n", " 'href': './catalog.json',\n", " 'type': 'application/json',\n", " 'title': 'STAC for Landsat data'},\n", " {'rel': 'child',\n", " 'href': './landsat-8-l1/collection.json',\n", " 'title': 'Landsat 8 L1'}],\n", " 'title': 'STAC for Landsat data'}" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cat.to_dict(include_self_link=False)" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "# get first (and only in this case) sub-catalog\n", "subcat = next(cat.get_children())" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Root Catalog: landsat-stac-collection-catalog\n", "Sub Catalog: landsat-8-l1\n", "Sub Catalog parent: landsat-stac-collection-catalog\n", "Sub Catalog children:\n" ] } ], "source": [ "# print some IDs\n", "print(\"Root Catalog: \", cat.id)\n", "print(\"Sub Catalog: \", subcat.id)\n", "print(\"Sub Catalog parent: \", subcat.get_parent().id)\n", "\n", "# iterate through child catalogs of the sub-catalog\n", "print(\"Sub Catalog children:\")\n", "for child in subcat.get_children():\n", " print(\" \", child.id)" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "**Items**\n", "LC80140332018166LGN00\n", "LC80150322018141LGN00\n", "LC80150332018189LGN00\n", "LC80300332018166LGN00\n" ] } ], "source": [ "print(\"\\n**Items**\")\n", "for i in cat.get_items(recursive=True):\n", " print(i.id)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Creating new catalogs" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can initialize a new Catalog with an id and a description. Note that by default it sets a new catalog as root." ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "scrolled": true }, "outputs": [], "source": [ "# create a Catalog object with JSON\n", "mycat = pystac.Catalog(id=\"mycat\", description=\"My shiny new STAC catalog\")" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[>]" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "mycat.links" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Adding catalogs to catalogs" ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "scrolled": true }, "outputs": [], "source": [ "# add a new catalog to a root catalog\n", "kitten = pystac.Catalog(\n", " id=\"mykitten\", description=\"A child catalog of my shiny new STAC catalog\"\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When you add a child catalog to a parent catalog, the child catalog assumes the root catalog of it's parent. 'Child' and 'parent' links are also added to the parent and child catalogs, respectively." ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[>]" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "kitten.links" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "
\n", "
\n", "
    \n", " \n", " \n", " \n", "
  • \n", " rel\n", " \"child\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " href\n", " None\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " type\n", " \"application/json\"\n", "
  • \n", " \n", " \n", " \n", "
\n", "
\n", "
" ], "text/plain": [ ">" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "mycat.add_child(kitten)" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[>,\n", " >]" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "kitten.links" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[>,\n", " >]" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "mycat.links" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "* \n", " * \n" ] } ], "source": [ "mycat.describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Adding collections to catalogs" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the next two steps we will work with Pystac Collections and Items. We will pull them out of our example catalog and add them to the new STAC that we have created." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Collections are Catalogs but also include spatial and temporal extents as well as additional properties. " ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "
\n", "
\n", "
    \n", " \n", " \n", " \n", "
  • \n", " type\n", " \"Collection\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " id\n", " \"landsat-8-l1\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " stac_version\n", " \"1.0.0\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " description\n", " \"Landsat 8 imagery radiometrically calibrated and orthorectified using ground points and Digital Elevation Model (DEM) data to correct relief displacement.\"\n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " \n", " links\n", " [] 7 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"self\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/collection.json\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"application/json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 1\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"root\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"../catalog.json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 2\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"parent\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"../catalog.json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 3\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"item\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"./2018-06/LC80140332018166LGN00.json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 4\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"item\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"./2018-05/LC80150322018141LGN00.json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 5\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"item\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"./2018-07/LC80150332018189LGN00.json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 6\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"item\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"./2018-06/LC80300332018166LGN00.json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", " \n", "
  • \n", " \n", " stac_extensions\n", " [] 1 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", " \"https://example.com/stac/landsat-extension/1.0/schema.json\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " properties\n", "
      \n", " \n", " \n", " \n", "
    • \n", " collection\n", " \"landsat-8-l1\"\n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " \n", " instruments\n", " [] 1 items\n", " \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 0\n", " \"OLI_TIRS\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " view:sun_azimuth\n", " 149.01607154\n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " \n", " eo:bands\n", " [] 11 items\n", " \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 0\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B1\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.02\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.44\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"coastal\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 1\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B2\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.06\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.48\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"blue\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 2\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B3\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.06\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.56\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"green\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 3\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B4\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.04\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.65\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"red\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 4\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B5\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.03\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.86\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"nir\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 5\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B6\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.08\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 1.6\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"swir16\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 6\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B7\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.22\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 2.2\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"swir22\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 7\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B8\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.18\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 0.59\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"pan\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 8\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B9\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.02\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 1.37\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"cirrus\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 9\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B10\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 0.8\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 10.9\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"lwir11\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 10\n", "
          \n", " \n", " \n", " \n", "
        • \n", " name\n", " \"B11\"\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " full_width_half_max\n", " 1\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " center_wavelength\n", " 12\n", "
        • \n", " \n", " \n", " \n", " \n", " \n", "
        • \n", " common_name\n", " \"lwir2\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " view:off_nadir\n", " 0\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " view:azimuth\n", " 0\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " platform\n", " \"landsat-8\"\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " gsd\n", " 15\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " view:sun_elevation\n", " 59.214247\n", "
    • \n", " \n", " \n", " \n", "
    \n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " title\n", " \"Landsat 8 L1\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " extent\n", "
      \n", " \n", " \n", " \n", "
    • \n", " spatial\n", "
        \n", " \n", " \n", "
      • \n", " \n", " bbox\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", "
        • \n", " \n", " 0\n", " [] 4 items\n", " \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 0\n", " -180.0\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 1\n", " -90.0\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 2\n", " 180.0\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 3\n", " 90.0\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
        • \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " temporal\n", "
        \n", " \n", " \n", "
      • \n", " \n", " interval\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", "
        • \n", " \n", " 0\n", " [] 2 items\n", " \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 0\n", " \"2018-05-21T15:44:59Z\"\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 1\n", " \"2018-07-08T15:45:34Z\"\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
        • \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " license\n", " \"proprietary\"\n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " \n", " keywords\n", " [] 3 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", " \"landsat\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 1\n", " \"earth observation\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 2\n", " \"usgs\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", " \n", "
  • \n", " \n", " providers\n", " [] 1 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", "
        \n", " \n", " \n", " \n", "
      • \n", " name\n", " \"Development Seed\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", " \n", "
        • \n", " 0\n", " \"processor\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " url\n", " \"https://github.com/sat-utils/sat-api\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", "
\n", "
\n", "
" ], "text/plain": [ "" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# open the Landsat collection\n", "collection = pystac.Collection.from_file(\n", " \"../example-catalog/landsat-8-l1/collection.json\"\n", ")\n", "collection" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "See the spatial and temporal extent of this collection" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'spatial': {'bbox': [[-180.0, -90.0, 180.0, 90.0]]},\n", " 'temporal': {'interval': [['2018-05-21T15:44:59Z', '2018-07-08T15:45:34Z']]}}" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "collection.extent.to_dict()" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[,\n", " ,\n", " ,\n", " ,\n", " ,\n", " ,\n", " ]" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "collection.links" ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "scrolled": true }, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "
\n", "
\n", "
    \n", " \n", " \n", " \n", "
  • \n", " rel\n", " \"child\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/collection.json\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " type\n", " \"application/json\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " title\n", " \"Landsat 8 L1\"\n", "
  • \n", " \n", " \n", " \n", "
\n", "
\n", "
" ], "text/plain": [ ">" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# add it to the child catalog created above\n", "kitten.add_child(collection)" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[,\n", " >,\n", " ,\n", " ,\n", " ,\n", " ,\n", " >]" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "collection.links" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Adding items to collection" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Items are stac objects whose parents can be either Catalogs or Collections. They also have spatio-temporal information and assets. Assets point directly to the data included in the STAC." ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "
\n", "
\n", "
    \n", " \n", " \n", " \n", "
  • \n", " type\n", " \"Feature\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " stac_version\n", " \"1.0.0\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " id\n", " \"LC80150322018141LGN00\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " properties\n", "
      \n", " \n", " \n", " \n", "
    • \n", " collection\n", " \"landsat-8-l1\"\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " datetime\n", " \"2018-05-21T15:44:59Z\"\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " view:sun_azimuth\n", " 134.8082647\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " view:sun_elevation\n", " 64.00406717\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " eo:cloud_cover\n", " 4\n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " \n", " instruments\n", " [] 1 items\n", " \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 0\n", " \"OLI_TIRS\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " view:off_nadir\n", " 0\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " platform\n", " \"landsat-8\"\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " gsd\n", " 30\n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " proj:epsg\n", " 32618\n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " \n", " proj:transform\n", " [] 6 items\n", " \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 0\n", " 258885.0\n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 1\n", " 30.0\n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 2\n", " 0.0\n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 3\n", " 4584315.0\n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 4\n", " 0.0\n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 5\n", " -30.0\n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " proj:geometry\n", "
        \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"Polygon\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " coordinates\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", "
        • \n", " \n", " 0\n", " [] 5 items\n", " \n", " \n", "
            \n", " \n", " \n", "
          • \n", " \n", " 0\n", " [] 2 items\n", " \n", " \n", "
              \n", " \n", " \n", " \n", "
            • \n", " 0\n", " 258885.0\n", "
            • \n", " \n", " \n", " \n", "
            \n", " \n", "
              \n", " \n", " \n", " \n", "
            • \n", " 1\n", " 4346085.0\n", "
            • \n", " \n", " \n", " \n", "
            \n", " \n", "
          • \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", "
          • \n", " \n", " 1\n", " [] 2 items\n", " \n", " \n", "
              \n", " \n", " \n", " \n", "
            • \n", " 0\n", " 258885.0\n", "
            • \n", " \n", " \n", " \n", "
            \n", " \n", "
              \n", " \n", " \n", " \n", "
            • \n", " 1\n", " 4584315.0\n", "
            • \n", " \n", " \n", " \n", "
            \n", " \n", "
          • \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", "
          • \n", " \n", " 2\n", " [] 2 items\n", " \n", " \n", "
              \n", " \n", " \n", " \n", "
            • \n", " 0\n", " 493515.0\n", "
            • \n", " \n", " \n", " \n", "
            \n", " \n", "
              \n", " \n", " \n", " \n", "
            • \n", " 1\n", " 4584315.0\n", "
            • \n", " \n", " \n", " \n", "
            \n", " \n", "
          • \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", "
          • \n", " \n", " 3\n", " [] 2 items\n", " \n", " \n", "
              \n", " \n", " \n", " \n", "
            • \n", " 0\n", " 493515.0\n", "
            • \n", " \n", " \n", " \n", "
            \n", " \n", "
              \n", " \n", " \n", " \n", "
            • \n", " 1\n", " 4346085.0\n", "
            • \n", " \n", " \n", " \n", "
            \n", " \n", "
          • \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", "
          • \n", " \n", " 4\n", " [] 2 items\n", " \n", " \n", "
              \n", " \n", " \n", " \n", "
            • \n", " 0\n", " 258885.0\n", "
            • \n", " \n", " \n", " \n", "
            \n", " \n", "
              \n", " \n", " \n", " \n", "
            • \n", " 1\n", " 4346085.0\n", "
            • \n", " \n", " \n", " \n", "
            \n", " \n", "
          • \n", " \n", " \n", "
          \n", " \n", "
        • \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " \n", " proj:shape\n", " [] 2 items\n", " \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 0\n", " 7821\n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
        \n", " \n", " \n", " \n", "
      • \n", " 1\n", " 7941\n", "
      • \n", " \n", " \n", " \n", "
      \n", " \n", "
    • \n", " \n", " \n", "
    \n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " geometry\n", "
      \n", " \n", " \n", " \n", "
    • \n", " type\n", " \"Polygon\"\n", "
    • \n", " \n", " \n", " \n", " \n", "
    • \n", " \n", " coordinates\n", " [] 1 items\n", " \n", " \n", "
        \n", " \n", " \n", "
      • \n", " \n", " 0\n", " [] 5 items\n", " \n", " \n", "
          \n", " \n", " \n", "
        • \n", " \n", " 0\n", " [] 2 items\n", " \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 0\n", " -77.28911976020206\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 1\n", " 41.40912394323429\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
        • \n", " \n", " \n", "
        \n", " \n", "
          \n", " \n", " \n", "
        • \n", " \n", " 1\n", " [] 2 items\n", " \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 0\n", " -75.07576783500748\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 1\n", " 40.97162247589133\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
        • \n", " \n", " \n", "
        \n", " \n", "
          \n", " \n", " \n", "
        • \n", " \n", " 2\n", " [] 2 items\n", " \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 0\n", " -75.66872631473827\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 1\n", " 39.23210949585851\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
        • \n", " \n", " \n", "
        \n", " \n", "
          \n", " \n", " \n", "
        • \n", " \n", " 3\n", " [] 2 items\n", " \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 0\n", " -77.87946700654118\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 1\n", " 39.67679918442899\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
        • \n", " \n", " \n", "
        \n", " \n", "
          \n", " \n", " \n", "
        • \n", " \n", " 4\n", " [] 2 items\n", " \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 0\n", " -77.28911976020206\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
            \n", " \n", " \n", " \n", "
          • \n", " 1\n", " 41.40912394323429\n", "
          • \n", " \n", " \n", " \n", "
          \n", " \n", "
        • \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", "
      \n", " \n", "
    • \n", " \n", " \n", "
    \n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " \n", " links\n", " [] 4 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"self\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/2018-05/LC80150322018141LGN00.json\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"application/json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 1\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"parent\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"../collection.json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 2\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"collection\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"../collection.json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 3\n", "
        \n", " \n", " \n", " \n", "
      • \n", " rel\n", " \"root\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"../../catalog.json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " assets\n", "
      \n", " \n", " \n", " \n", "
    • \n", " index\n", "
        \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/index.html\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"text/html\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"HTML index page\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 0 items\n", " \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " thumbnail\n", "
        \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_thumb_large.jpg\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"image/jpeg\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"Thumbnail image\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", " \n", "
        • \n", " 0\n", " \"thumbnail\"\n", "
        • \n", " \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " B1\n", "
        \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B1.TIF\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"image/tiff\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"Band 1 (coastal)\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " eo:bands\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", " \n", "
        • \n", " 0\n", "
            \n", " \n", " \n", " \n", "
          • \n", " name\n", " \"B1\"\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " full_width_half_max\n", " 0.02\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " center_wavelength\n", " 0.44\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " common_name\n", " \"coastal\"\n", "
          • \n", " \n", " \n", " \n", "
          \n", "
        • \n", " \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 0 items\n", " \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " B2\n", "
        \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B2.TIF\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"image/tiff\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"Band 2 (blue)\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " eo:bands\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", " \n", "
        • \n", " 0\n", "
            \n", " \n", " \n", " \n", "
          • \n", " name\n", " \"B2\"\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " full_width_half_max\n", " 0.06\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " center_wavelength\n", " 0.48\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " common_name\n", " \"blue\"\n", "
          • \n", " \n", " \n", " \n", "
          \n", "
        • \n", " \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 0 items\n", " \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " B3\n", "
        \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B3.TIF\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"image/tiff\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"Band 3 (green)\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " eo:bands\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", " \n", "
        • \n", " 0\n", "
            \n", " \n", " \n", " \n", "
          • \n", " name\n", " \"B3\"\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " full_width_half_max\n", " 0.06\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " center_wavelength\n", " 0.56\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " common_name\n", " \"green\"\n", "
          • \n", " \n", " \n", " \n", "
          \n", "
        • \n", " \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 0 items\n", " \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " B4\n", "
        \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B4.TIF\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"image/tiff\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"Band 4 (red)\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " eo:bands\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", " \n", "
        • \n", " 0\n", "
            \n", " \n", " \n", " \n", "
          • \n", " name\n", " \"B4\"\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " full_width_half_max\n", " 0.04\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " center_wavelength\n", " 0.65\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " common_name\n", " \"red\"\n", "
          • \n", " \n", " \n", " \n", "
          \n", "
        • \n", " \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 0 items\n", " \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " B5\n", "
        \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B5.TIF\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"image/tiff\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"Band 5 (nir)\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " eo:bands\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", " \n", "
        • \n", " 0\n", "
            \n", " \n", " \n", " \n", "
          • \n", " name\n", " \"B5\"\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " full_width_half_max\n", " 0.03\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " center_wavelength\n", " 0.86\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " common_name\n", " \"nir\"\n", "
          • \n", " \n", " \n", " \n", "
          \n", "
        • \n", " \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 0 items\n", " \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " B6\n", "
        \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B6.TIF\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"image/tiff\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"Band 6 (swir16)\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " eo:bands\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", " \n", "
        • \n", " 0\n", "
            \n", " \n", " \n", " \n", "
          • \n", " name\n", " \"B6\"\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " full_width_half_max\n", " 0.08\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " center_wavelength\n", " 1.6\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " common_name\n", " \"swir16\"\n", "
          • \n", " \n", " \n", " \n", "
          \n", "
        • \n", " \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 0 items\n", " \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " B7\n", "
        \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B7.TIF\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"image/tiff\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"Band 7 (swir22)\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " eo:bands\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", " \n", "
        • \n", " 0\n", "
            \n", " \n", " \n", " \n", "
          • \n", " name\n", " \"B7\"\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " full_width_half_max\n", " 0.22\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " center_wavelength\n", " 2.2\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " common_name\n", " \"swir22\"\n", "
          • \n", " \n", " \n", " \n", "
          \n", "
        • \n", " \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 0 items\n", " \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " B8\n", "
        \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B8.TIF\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"image/tiff\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"Band 8 (pan)\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " eo:bands\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", " \n", "
        • \n", " 0\n", "
            \n", " \n", " \n", " \n", "
          • \n", " name\n", " \"B8\"\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " full_width_half_max\n", " 0.18\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " center_wavelength\n", " 0.59\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " common_name\n", " \"pan\"\n", "
          • \n", " \n", " \n", " \n", "
          \n", "
        • \n", " \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 0 items\n", " \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " B9\n", "
        \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B9.TIF\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"image/tiff\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"Band 9 (cirrus)\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " eo:bands\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", " \n", "
        • \n", " 0\n", "
            \n", " \n", " \n", " \n", "
          • \n", " name\n", " \"B9\"\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " full_width_half_max\n", " 0.02\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " center_wavelength\n", " 1.37\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " common_name\n", " \"cirrus\"\n", "
          • \n", " \n", " \n", " \n", "
          \n", "
        • \n", " \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 0 items\n", " \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " B10\n", "
        \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B10.TIF\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"image/tiff\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"Band 10 (lwir)\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " eo:bands\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", " \n", "
        • \n", " 0\n", "
            \n", " \n", " \n", " \n", "
          • \n", " name\n", " \"B10\"\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " full_width_half_max\n", " 0.8\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " center_wavelength\n", " 10.9\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " common_name\n", " \"lwir11\"\n", "
          • \n", " \n", " \n", " \n", "
          \n", "
        • \n", " \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 0 items\n", " \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " B11\n", "
        \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_B11.TIF\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"image/tiff\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"Band 11 (lwir)\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " eo:bands\n", " [] 1 items\n", " \n", " \n", "
          \n", " \n", " \n", " \n", "
        • \n", " 0\n", "
            \n", " \n", " \n", " \n", "
          • \n", " name\n", " \"B11\"\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " full_width_half_max\n", " 1\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " center_wavelength\n", " 12\n", "
          • \n", " \n", " \n", " \n", " \n", " \n", "
          • \n", " common_name\n", " \"lwir2\"\n", "
          • \n", " \n", " \n", " \n", "
          \n", "
        • \n", " \n", " \n", " \n", "
        \n", " \n", "
      • \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 0 items\n", " \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " ANG\n", "
        \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_ANG.txt\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"text/plain\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"Angle coefficients file\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 0 items\n", " \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " MTL\n", "
        \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_MTL.txt\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"text/plain\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"original metadata file\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 0 items\n", " \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", " \n", " \n", "
    • \n", " BQA\n", "
        \n", " \n", " \n", " \n", "
      • \n", " href\n", " \"https://s3-us-west-2.amazonaws.com/landsat-pds/c1/L8/015/032/LC08_L1TP_015032_20180521_20180605_01_T1/LC08_L1TP_015032_20180521_20180605_01_T1_BQA.TIF\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " type\n", " \"image/tiff\"\n", "
      • \n", " \n", " \n", " \n", " \n", " \n", "
      • \n", " title\n", " \"Band quality data\"\n", "
      • \n", " \n", " \n", " \n", " \n", "
      • \n", " \n", " roles\n", " [] 0 items\n", " \n", " \n", "
      • \n", " \n", " \n", "
      \n", "
    • \n", " \n", " \n", " \n", "
    \n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " \n", " bbox\n", " [] 4 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", " -77.88298\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 1\n", " 39.23073\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 2\n", " -75.07535\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 3\n", " 41.41022\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", " \n", "
  • \n", " \n", " stac_extensions\n", " [] 3 items\n", " \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 0\n", " \"https://stac-extensions.github.io/eo/v1.1.0/schema.json\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 1\n", " \"https://stac-extensions.github.io/view/v1.0.0/schema.json\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
      \n", " \n", " \n", " \n", "
    • \n", " 2\n", " \"https://stac-extensions.github.io/projection/v1.1.0/schema.json\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", " collection\n", " \"landsat-8-l1\"\n", "
  • \n", " \n", " \n", " \n", "
\n", "
\n", "
" ], "text/plain": [ "" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# open a Landsat item\n", "item = pystac.read_file(\n", " \"../example-catalog/landsat-8-l1/2018-05/LC80150322018141LGN00.json\"\n", ")\n", "item" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[,\n", " ,\n", " ,\n", " ]" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.links" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'index': ,\n", " 'thumbnail': ,\n", " 'B1': ,\n", " 'B2': ,\n", " 'B3': ,\n", " 'B4': ,\n", " 'B5': ,\n", " 'B6': ,\n", " 'B7': ,\n", " 'B8': ,\n", " 'B9': ,\n", " 'B10': ,\n", " 'B11': ,\n", " 'ANG': ,\n", " 'MTL': ,\n", " 'BQA': }" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item.assets" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "
\n", "
\n", "
    \n", " \n", " \n", " \n", "
  • \n", " rel\n", " \"item\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " href\n", " \"/home/jsignell/pystac/docs/example-catalog/landsat-8-l1/LC80150322018141LGN00/LC80150322018141LGN00.json\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " type\n", " \"application/json\"\n", "
  • \n", " \n", " \n", " \n", "
\n", "
\n", "
" ], "text/plain": [ ">" ] }, "execution_count": 34, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# add it to the collection created above\n", "collection.add_item(item)" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "* \n", " * \n", " * \n", " * \n", " * \n", " * \n", " * \n", " * \n" ] } ], "source": [ "# now look at the catalog we've created\n", "mycat.describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Currently, this STAC only exists in memory. We can use `normalize_and_save` to save off the STAC with the canonical \"absolute published\" form:" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [], "source": [ "mycat.normalize_and_save(\n", " \"pystac-example-absolute\", catalog_type=pystac.CatalogType.ABSOLUTE_PUBLISHED\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice now that the 'parent' link of an item is a absolute HREF:" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'/home/jsignell/pystac/docs/tutorials/pystac-example-absolute/mykitten/landsat-8-l1/collection.json'" ] }, "execution_count": 37, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item = next(mycat.get_items(recursive=True))\n", "item.get_single_link(\"parent\").get_href()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can also normalize and save the catalog to the other types described in the best practices documentation: \"relative published\" and \"self contained\". A self contained catalog contains all relative links, and no self links. Notice how saving a self contained catalog will produce relative links:" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [], "source": [ "mycat.normalize_and_save(\n", " \"pystac-example-relative\", catalog_type=pystac.CatalogType.SELF_CONTAINED\n", ")" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'../collection.json'" ] }, "execution_count": 39, "metadata": {}, "output_type": "execute_result" } ], "source": [ "item = next(mycat.get_items(recursive=True))\n", "item.get_single_link(\"parent\").get_href()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.6" } }, "nbformat": 4, "nbformat_minor": 4 } pystac-1.9.0/docs/tutorials/pystac-spacenet-tutorial.ipynb000066400000000000000000000242551451576074700240410ustar00rootroot00000000000000{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Create and manipulate SpaceNet Vegas STAC" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This tutorial shows how to create and manipulate STACs using pystac.\n", "\n", "- Create (in memory) a pystac catalog of [SpaceNet 2 imagery from the Las Vegas AOI](https://spacenetchallenge.github.io/AOI_Lists/AOI_2_Vegas.html) using data hosted in a public s3 bucket\n", "- Set relative paths for all STAC object\n", "- Normalize links from a root directory and save the STAC there" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "import sys\n", "\n", "sys.path.append(\"..\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You may need install the following packages that are not included in the Python 3 standard library. If you do not have any of these installed, you can do do with pip:\n", "\n", "[boto3](https://pypi.org/project/boto3/): `pip install boto3` \n", "[botocore](https://pypi.org/project/botocore/): `pip install botocore` \n", "[rasterio](https://pypi.org/project/rasterio/): `pip install rasterio` \n", "[shapely](https://pypi.org/project/Shapely/): `pip install Shapely` \n", "[rio-cogeo](https://github.com/cogeotiff/rio-cogeo): `pip install rio-cogeo`" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "from datetime import datetime\n", "from os.path import basename, join\n", "\n", "import boto3\n", "import rasterio\n", "import pystac\n", "from shapely.geometry import GeometryCollection, box, shape, mapping" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create SpaceNet Vegas STAC" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Initialize a STAC for the SpaceNet 2 dataset" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "spacenet = pystac.Catalog(id=\"spacenet\", description=\"SpaceNet 2 STAC\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We do not yet know the spatial extent of the Vegas AOI. We will need to determine it when we download all of the images. As a placeholder we will create a spatial extent of null values." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "sp_extent = pystac.SpatialExtent([None, None, None, None])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The capture date for SpaceNet 2 Vegas imagery is October 22, 2015. Create a python datetime object using that date" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "capture_date = datetime.strptime(\"2015-10-22\", \"%Y-%m-%d\")\n", "tmp_extent = pystac.TemporalExtent([(capture_date, None)])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Create an Extent object that will define both the spatial and temporal extents of the Vegas collection" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "extent = pystac.Extent(sp_extent, tmp_extent)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Create a collection that will encompass the Vegas data and add to the spacenet catalog" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "
\n", "
\n", "
    \n", " \n", " \n", " \n", "
  • \n", " rel\n", " \"child\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " href\n", " None\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", "
  • \n", " type\n", " \"application/json\"\n", "
  • \n", " \n", " \n", " \n", "
\n", "
\n", "
" ], "text/plain": [ ">" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vegas = pystac.Collection(\n", " id=\"vegas\", description=\"Vegas SpaceNet 2 dataset\", extent=extent\n", ")\n", "spacenet.add_child(vegas)" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "* \n", " * \n" ] } ], "source": [ "spacenet.describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Find the locations of SpaceNet images. In order to make this example quicker, we will limit the number of scenes that we use to 10." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "client = boto3.client(\"s3\")\n", "scenes = client.list_objects(\n", " Bucket=\"spacenet-dataset\",\n", " Prefix=\"spacenet/SN2_buildings/train/AOI_2_Vegas/PS-RGB/\",\n", " MaxKeys=20,\n", ")\n", "scenes = [s[\"Key\"] for s in scenes[\"Contents\"] if s[\"Key\"].endswith(\".tif\")][0:10]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For each scene, create and item with a defined bounding box. Each item will include the geotiff as an asset. We will add labels in the next section." ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "for scene in scenes:\n", " uri = join(\"s3://spacenet-dataset/\", scene)\n", " params = {}\n", " params[\"id\"] = basename(uri).split(\".\")[0]\n", " with rasterio.open(uri) as src:\n", " params[\"bbox\"] = list(src.bounds)\n", " params[\"geometry\"] = mapping(box(*params[\"bbox\"]))\n", " params[\"datetime\"] = capture_date\n", " params[\"properties\"] = {}\n", " i = pystac.Item(**params)\n", " i.add_asset(\n", " key=\"image\",\n", " asset=pystac.Asset(\n", " href=uri, title=\"Geotiff\", media_type=pystac.MediaType.GEOTIFF\n", " ),\n", " )\n", " vegas.add_item(i)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now reset the spatial extent of the Vegas collection using the geometry objects from from the items we just added." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "bounds = [\n", " list(\n", " GeometryCollection(\n", " [shape(s.geometry) for s in spacenet.get_items(recursive=True)]\n", " ).bounds\n", " )\n", "]\n", "vegas.extent.spatial = pystac.SpatialExtent(bounds)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Currently, this STAC only exists in memory. We need to set all of the paths based on the root directory we want to save off that catalog too, and then save a \"self contained\" catalog, which will have all links be relative and contain no 'self' links. We can do this by using the `normalize` method to set the HREFs of all of our STAC objects. We'll then validate the catalog, and then save:" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [], "source": [ "spacenet.normalize_hrefs(\"spacenet-stac\")" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "10" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "spacenet.validate_all()" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "spacenet.save(catalog_type=pystac.CatalogType.SELF_CONTAINED)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.3" } }, "nbformat": 4, "nbformat_minor": 4 } pystac-1.9.0/pyproject.toml000066400000000000000000000053301451576074700157610ustar00rootroot00000000000000[project] name = "pystac" description = "Python library for working with the SpatioTemporal Asset Catalog (STAC) specification" readme = "README.md" authors = [ { name = "Rob Emanuele", email = "rdemanuele@gmail.com" }, { name = "Jon Duckworth", email = "duckontheweb@gmail.com" }, ] maintainers = [{ name = "Pete Gadomski", email = "pete.gadomski@gmail.com" }] keywords = ["pystac", "imagery", "raster", "catalog", "STAC"] license = { text = "Apache-2.0" } classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", ] requires-python = ">=3.9" dependencies = ["python-dateutil>=2.7.0"] dynamic = ["version"] [project.optional-dependencies] bench = ["asv~=0.6.0", "packaging~=23.1", "virtualenv~=20.22"] docs = [ "Sphinx~=6.2", "boto3~=1.28", "ipython~=8.12", "jinja2<4.0", "jupyter~=1.0", "nbsphinx~=0.9.0", "pydata-sphinx-theme~=0.13", "rasterio~=1.3", "shapely~=2.0", "sphinx-autobuild==2021.3.14", "sphinx-design~=0.5.0", "sphinxcontrib-fulltoc~=1.2", ] jinja2 = ["jinja2<4.0"] orjson = ["orjson>=3.5"] test = [ "black~=23.3", "codespell~=2.2", "coverage~=7.2", "doc8~=1.1", "html5lib~=1.1", "jinja2<4.0", "jsonschema~=4.18", "mypy~=1.2", "orjson~=3.8", "pre-commit~=3.2", "pytest-cov~=4.0", "pytest-mock~=3.10", "pytest-recording~=0.13.0", "pytest~=7.3", "requests-mock~=1.11", "ruff==0.1.1", "types-html5lib~=1.1", "types-orjson~=3.6", "types-jsonschema~=4.18", "types-python-dateutil~=2.8", "types-urllib3~=1.26", ] urllib3 = ["urllib3>=1.26"] validation = ["jsonschema~=4.18"] [project.urls] Homepage = "https://github.com/stac-utils/pystac" Documentation = "https://pystac.readthedocs.io" Repository = "https://github.com/stac-utils/pystac.git" Changelog = "https://github.com/stac-utils/pystac/blob/main/CHANGELOG.md" Discussions = "https://github.com/radiantearth/stac-spec/discussions/categories/stac-software" [tool.setuptools.packages.find] include = ["pystac*"] exclude = ["tests*", "benchmarks*"] [tool.setuptools.dynamic] version = { attr = "pystac.version.__version__" } [tool.mypy] show_error_codes = true strict = true [[tool.mypy.overrides]] module = ["jinja2"] ignore_missing_imports = true [tool.ruff] line-length = 88 select = ["E", "F", "I"] [tool.pytest.ini_options] filterwarnings = ["error"] [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" pystac-1.9.0/pystac/000077500000000000000000000000001451576074700143475ustar00rootroot00000000000000pystac-1.9.0/pystac/__init__.py000066400000000000000000000177401451576074700164710ustar00rootroot00000000000000# isort: skip_file """ PySTAC is a library for working with SpatioTemporal Asset Catalogs (STACs) """ __all__ = [ "__version__", "TemplateError", "STACError", "STACTypeError", "DuplicateObjectKeyError", "ExtensionAlreadyExistsError", "ExtensionNotImplemented", "ExtensionTypeError", "RequiredPropertyMissing", "STACValidationError", "DeprecatedWarning", "MediaType", "RelType", "StacIO", "STACObject", "STACObjectType", "Link", "HIERARCHICAL_LINKS", "Catalog", "CatalogType", "Collection", "Extent", "SpatialExtent", "TemporalExtent", "Summaries", "CommonMetadata", "RangeSummary", "Item", "Asset", "ItemCollection", "Provider", "ProviderRole", "read_file", "read_dict", "write_file", "get_stac_version", "set_stac_version", ] import os import warnings from typing import Any, Optional from pystac.errors import ( TemplateError, STACError, STACTypeError, DuplicateObjectKeyError, ExtensionAlreadyExistsError, ExtensionNotImplemented, ExtensionTypeError, RequiredPropertyMissing, STACValidationError, DeprecatedWarning, ) from pystac.version import ( __version__, get_stac_version, set_stac_version, ) from pystac.media_type import MediaType from pystac.rel_type import RelType from pystac.stac_io import StacIO from pystac.stac_object import STACObject, STACObjectType from pystac.link import Link, HIERARCHICAL_LINKS from pystac.catalog import Catalog, CatalogType from pystac.collection import ( Collection, Extent, SpatialExtent, TemporalExtent, ) from pystac.common_metadata import CommonMetadata from pystac.summaries import RangeSummary, Summaries from pystac.asset import Asset from pystac.item import Item from pystac.item_collection import ItemCollection from pystac.provider import ProviderRole, Provider from pystac.utils import HREF import pystac.validation import pystac.extensions.hooks import pystac.extensions.classification import pystac.extensions.datacube import pystac.extensions.eo import pystac.extensions.file import pystac.extensions.grid import pystac.extensions.item_assets with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) import pystac.extensions.label import pystac.extensions.mgrs import pystac.extensions.pointcloud import pystac.extensions.projection import pystac.extensions.raster import pystac.extensions.sar import pystac.extensions.sat import pystac.extensions.scientific import pystac.extensions.storage import pystac.extensions.table import pystac.extensions.timestamps import pystac.extensions.version import pystac.extensions.view import pystac.extensions.xarray_assets EXTENSION_HOOKS = pystac.extensions.hooks.RegisteredExtensionHooks( [ pystac.extensions.classification.CLASSIFICATION_EXTENSION_HOOKS, pystac.extensions.datacube.DATACUBE_EXTENSION_HOOKS, pystac.extensions.eo.EO_EXTENSION_HOOKS, pystac.extensions.file.FILE_EXTENSION_HOOKS, pystac.extensions.grid.GRID_EXTENSION_HOOKS, pystac.extensions.item_assets.ITEM_ASSETS_EXTENSION_HOOKS, pystac.extensions.label.LABEL_EXTENSION_HOOKS, pystac.extensions.mgrs.MGRS_EXTENSION_HOOKS, pystac.extensions.pointcloud.POINTCLOUD_EXTENSION_HOOKS, pystac.extensions.projection.PROJECTION_EXTENSION_HOOKS, pystac.extensions.raster.RASTER_EXTENSION_HOOKS, pystac.extensions.sar.SAR_EXTENSION_HOOKS, pystac.extensions.sat.SAT_EXTENSION_HOOKS, pystac.extensions.scientific.SCIENTIFIC_EXTENSION_HOOKS, pystac.extensions.storage.STORAGE_EXTENSION_HOOKS, pystac.extensions.table.TABLE_EXTENSION_HOOKS, pystac.extensions.timestamps.TIMESTAMPS_EXTENSION_HOOKS, pystac.extensions.version.VERSION_EXTENSION_HOOKS, pystac.extensions.view.VIEW_EXTENSION_HOOKS, pystac.extensions.xarray_assets.XARRAY_ASSETS_EXTENSION_HOOKS, ] ) def read_file(href: HREF, stac_io: Optional[StacIO] = None) -> STACObject: """Reads a STAC object from a file. This method will return either a Catalog, a Collection, or an Item based on what the file contains. This is a convenience method for :meth:`StacIO.read_stac_object ` Args: href : The HREF to read the object from. stac_io: Optional :class:`~StacIO` instance to use for I/O operations. If not provided, will use :meth:`StacIO.default` to create an instance. Returns: The specific STACObject implementation class that is represented by the JSON read from the file located at HREF. Raises: STACTypeError : If the file at ``href`` does not represent a valid :class:`~pystac.STACObject`. Note that an :class:`~pystac.ItemCollection` is not a :class:`~pystac.STACObject` and must be read using :meth:`ItemCollection.from_file ` """ if stac_io is None: stac_io = StacIO.default() return stac_io.read_stac_object(href) def write_file( obj: STACObject, include_self_link: bool = True, dest_href: Optional[HREF] = None, stac_io: Optional[StacIO] = None, ) -> None: """Writes a STACObject to a file. This will write only the Catalog, Collection or Item ``obj``. It will not attempt to write any other objects that are linked to ``obj``; if you'd like functionality to save off catalogs recursively see :meth:`Catalog.save `. This method will write the JSON of the object to the object's assigned "self" link or to the dest_href if provided. To set the self link, see :meth:`STACObject.set_self_href `. Convenience method for :meth:`STACObject.from_file ` Args: obj : The STACObject to save. include_self_link : If ``True``, include the ``"self"`` link with this object. Otherwise, leave out the self link. dest_href : Optional HREF to save the file to. If ``None``, the object will be saved to the object's ``"self"`` href. stac_io: Optional :class:`~StacIO` instance to use for I/O operations. If not provided, will use :meth:`StacIO.default` to create an instance. """ if stac_io is None: stac_io = StacIO.default() dest_href = None if dest_href is None else str(os.fspath(dest_href)) obj.save_object( include_self_link=include_self_link, dest_href=dest_href, stac_io=stac_io ) def read_dict( d: dict[str, Any], href: Optional[str] = None, root: Optional[Catalog] = None, stac_io: Optional[StacIO] = None, ) -> STACObject: """Reads a :class:`~STACObject` or :class:`~ItemCollection` from a JSON-like dict representing a serialized STAC object. This method will return either a :class:`~Catalog`, :class:`~Collection`, or :class`~Item` based on the contents of the dict. This is a convenience method for either :meth:`StacIO.stac_object_from_dict `. Args: d : The dict to parse. href : Optional href that is the file location of the object being parsed. root : Optional root of the catalog for this object. If provided, the root's resolved object cache can be used to search for previously resolved instances of the STAC object. stac_io: Optional :class:`~StacIO` instance to use for reading. If ``None``, the default instance will be used. Raises: STACTypeError : If the ``d`` dictionary does not represent a valid :class:`~pystac.STACObject`. Note that an :class:`~pystac.ItemCollection` is not a :class:`~pystac.STACObject` and must be read using :meth:`ItemCollection.from_dict ` """ if stac_io is None: stac_io = StacIO.default() return stac_io.stac_object_from_dict(d, href, root) pystac-1.9.0/pystac/asset.py000066400000000000000000000312271451576074700160450ustar00rootroot00000000000000from __future__ import annotations import os import shutil from copy import copy, deepcopy from html import escape from typing import TYPE_CHECKING, Any, Protocol, TypeVar from pystac import MediaType, STACError, common_metadata, utils from pystac.html.jinja_env import get_jinja_env from pystac.utils import is_absolute_href, make_absolute_href, make_relative_href if TYPE_CHECKING: from pystac.common_metadata import CommonMetadata from pystac.extensions.ext import AssetExt A = TypeVar("A", bound="Asset") class Asset: """An object that contains a link to data associated with an Item or Collection that can be downloaded or streamed. Args: href : Link to the asset object. Relative and absolute links are both allowed. title : Optional displayed title for clients and users. description : A description of the Asset providing additional details, such as how it was processed or created. CommonMark 0.29 syntax MAY be used for rich text representation. media_type : Optional description of the media type. Registered Media Types are preferred. See :class:`~pystac.MediaType` for common media types. roles : Optional, Semantic roles (i.e. thumbnail, overview, data, metadata) of the asset. extra_fields : Optional, additional fields for this asset. This is used by extensions as a way to serialize and deserialize properties on asset object JSON. """ href: str """Link to the asset object. Relative and absolute links are both allowed.""" title: str | None """Optional displayed title for clients and users.""" description: str | None """A description of the Asset providing additional details, such as how it was processed or created. CommonMark 0.29 syntax MAY be used for rich text representation.""" media_type: str | None """Optional description of the media type. Registered Media Types are preferred. See :class:`~pystac.MediaType` for common media types.""" roles: list[str] | None """Optional, Semantic roles (i.e. thumbnail, overview, data, metadata) of the asset.""" owner: Assets | None """The :class:`~pystac.Item` or :class:`~pystac.Collection` that this asset belongs to, or ``None`` if it has no owner.""" extra_fields: dict[str, Any] """Optional, additional fields for this asset. This is used by extensions as a way to serialize and deserialize properties on asset object JSON.""" def __init__( self, href: str, title: str | None = None, description: str | None = None, media_type: str | None = None, roles: list[str] | None = None, extra_fields: dict[str, Any] | None = None, ) -> None: self.href = utils.make_posix_style(href) self.title = title self.description = description self.media_type = media_type self.roles = roles self.extra_fields = extra_fields or {} # The Item which owns this Asset. self.owner = None def set_owner(self, obj: Assets) -> None: """Sets the owning item of this Asset. The owning item will be used to resolve relative HREFs of this asset. Args: obj: The Collection or Item that owns this asset. """ self.owner = obj def get_absolute_href(self) -> str | None: """Gets the absolute href for this asset, if possible. If this Asset has no associated Item, and the asset HREF is a relative path, this method will return ``None``. If the Item that owns the Asset has no self HREF, this will also return ``None``. Returns: str: The absolute HREF of this asset, or None if an absolute HREF could not be determined. """ if utils.is_absolute_href(self.href): return self.href else: if self.owner is not None: item_self = self.owner.get_self_href() if item_self is not None: return utils.make_absolute_href(self.href, item_self) return None def to_dict(self) -> dict[str, Any]: """Returns this Asset as a dictionary. Returns: dict: A serialization of the Asset. """ d: dict[str, Any] = {"href": self.href} if self.media_type is not None: d["type"] = self.media_type if self.title is not None: d["title"] = self.title if self.description is not None: d["description"] = self.description if self.extra_fields is not None and len(self.extra_fields) > 0: for k, v in self.extra_fields.items(): d[k] = v if self.roles is not None: d["roles"] = self.roles return d def clone(self) -> Asset: """Clones this asset. Makes a ``deepcopy`` of the :attr:`~pystac.Asset.extra_fields`. Returns: Asset: The clone of this asset. """ cls = self.__class__ return cls( href=self.href, title=self.title, description=self.description, media_type=self.media_type, roles=self.roles, extra_fields=deepcopy(self.extra_fields), ) def has_role(self, role: str) -> bool: """Check if a role exists in the Asset role list. Args: role: Role to check for existence. Returns: bool: True if role exists, else False. """ if self.roles is None: return False else: return role in self.roles @property def common_metadata(self) -> CommonMetadata: """Access the asset's common metadata fields as a :class:`~pystac.CommonMetadata` object.""" return common_metadata.CommonMetadata(self) def __repr__(self) -> str: return f"" def _repr_html_(self) -> str: jinja_env = get_jinja_env() if jinja_env: template = jinja_env.get_template("JSON.jinja2") return str(template.render(dict=self.to_dict())) else: return escape(repr(self)) @classmethod def from_dict(cls: type[A], d: dict[str, Any]) -> A: """Constructs an Asset from a dict. Returns: Asset: The Asset deserialized from the JSON dict. """ d = copy(d) href = d.pop("href") media_type = d.pop("type", None) title = d.pop("title", None) description = d.pop("description", None) roles = d.pop("roles", None) properties = None if any(d): properties = d return cls( href=href, media_type=media_type, title=title, description=description, roles=roles, extra_fields=properties, ) def move(self, href: str) -> Asset: """Moves this asset's file to a new location on the local filesystem, setting the asset href accordingly. Modifies the asset in place, and returns the same asset. Args: href: The new asset location. Must be a local path. If relative it must be relative to the owner object. Returns: Asset: The asset with the updated href. """ src = _absolute_href(self.href, self.owner, "move") dst = _absolute_href(href, self.owner, "move") shutil.move(src, dst) self.href = href return self def copy(self, href: str) -> Asset: """Copies this asset's file to a new location on the local filesystem, setting the asset href accordingly. Modifies the asset in place, and returns the same asset. Args: href: The new asset location. Must be a local path. If relative it must be relative to the owner object. Returns: Asset: The asset with the updated href. """ src = _absolute_href(self.href, self.owner, "copy") dst = _absolute_href(href, self.owner, "copy") shutil.copy2(src, dst) self.href = href return self def delete(self) -> None: """Delete this asset's file. Does not delete the asset from the item that owns it. See :func:`~pystac.Item.delete_asset` for that. Does not modify the asset. """ href = _absolute_href(self.href, self.owner, "delete") os.remove(href) @property def ext(self) -> AssetExt: """Accessor for extension classes on this asset Example:: asset.ext.proj.epsg = 4326 """ from pystac.extensions.ext import AssetExt return AssetExt(stac_object=self) class Assets(Protocol): """Protocol, with functionality, for STAC objects that have assets.""" assets: dict[str, Asset] """The asset dictionary.""" def get_assets( self, media_type: str | MediaType | None = None, role: str | None = None, ) -> dict[str, Asset]: """Get this object's assets. Args: media_type: If set, filter the assets such that only those with a matching ``media_type`` are returned. role: If set, filter the assets such that only those with a matching ``role`` are returned. Returns: Dict[str, Asset]: A dictionary of assets that match ``media_type`` and/or ``role`` if set or else all of this object's assets. """ return { k: deepcopy(v) for k, v in self.assets.items() if (media_type is None or v.media_type == media_type) and (role is None or v.has_role(role)) } def add_asset(self, key: str, asset: Asset) -> None: """Adds an Asset to this object. Args: key : The unique key of this asset. asset : The Asset to add. """ asset.set_owner(self) self.assets[key] = asset def delete_asset(self, key: str) -> None: """Deletes the asset at the given key, and removes the asset's data file from the local filesystem. It is an error to attempt to delete an asset's file if it is on a remote filesystem. To delete the asset without removing the file, use `del item.assets["key"]`. Args: key: The unique key of this asset. """ asset = self.assets[key] asset.set_owner(self) asset.delete() del self.assets[key] def make_asset_hrefs_relative(self) -> Assets: """Modify each asset's HREF to be relative to this object's self HREF. Returns: Item: self """ self_href = self.get_self_href() for asset in self.assets.values(): if is_absolute_href(asset.href): if self_href is None: raise STACError( "Cannot make asset HREFs relative if no self_href is set." ) asset.href = make_relative_href(asset.href, self_href) return self def make_asset_hrefs_absolute(self) -> Assets: """Modify each asset's HREF to be absolute. Any asset HREFs that are relative will be modified to absolute based on this item's self HREF. Returns: Assets: self """ self_href = self.get_self_href() for asset in self.assets.values(): if not is_absolute_href(asset.href): if self_href is None: raise STACError( "Cannot make relative asset HREFs absolute " "if no self_href is set." ) asset.href = make_absolute_href(asset.href, self_href) return self def get_self_href(self) -> str | None: """Abstract definition of STACObject.get_self_href. Needed to make the `make_asset_hrefs_{absolute|relative}` methods pass type checking. Refactoring out all the link behavior in STACObject to its own protocol would be too heavy, so we just use this stub instead. """ ... def _absolute_href(href: str, owner: Assets | None, action: str = "access") -> str: if utils.is_absolute_href(href): return href else: item_self = owner.get_self_href() if owner else None if item_self is None: raise ValueError( f"Cannot {action} file if asset href ('{href}') is relative " "and owner item is not set. Hint: try using " ":func:`~pystac.Item.make_asset_hrefs_absolute`" ) return utils.make_absolute_href(href, item_self) pystac-1.9.0/pystac/cache.py000066400000000000000000000277421451576074700160000ustar00rootroot00000000000000from __future__ import annotations from collections import ChainMap from copy import copy from typing import TYPE_CHECKING, Any, cast import pystac if TYPE_CHECKING: from pystac.collection import Collection from pystac.stac_object import STACObject def get_cache_key(stac_object: STACObject) -> tuple[str, bool]: """Produce a cache key for the given STAC object. If a self href is set, use that as the cache key. If not, use a key that combines this object's ID with it's parents' IDs. Returns: Tuple[str, bool]: A tuple with the cache key as the first element and a boolean that is true if the cache key is the object's HREF as the second element. """ href = stac_object.get_self_href() if href is not None: return href, True else: ids: list[str] = [] obj: pystac.STACObject | None = stac_object while obj is not None: ids.append(obj.id) obj = obj.get_parent() return "/".join(ids), False class ResolvedObjectCache: """This class tracks resolved objects tied to root catalogs. A STAC object is 'resolved' when it is a Python Object; a link to a STAC object such as a Catalog or Item is considered "unresolved" if it's target is pointed at an HREF of the object. Tracking resolved objects allows us to tie together the same instances when there are loops in the Graph of the STAC catalog (e.g. a LabelItem can link to a rel:source, and if that STAC Item exists in the same root catalog they should refer to the same Python object). Resolution tracking is important when copying STACs in-memory: In order for object links to refer to the copy of STAC Objects rather than their originals, we have to keep track of the resolved STAC Objects and replace them with their copies. Args: id_keys_to_objects : Existing cache of a key made up of the STACObject and it's parents IDs mapped to the cached STACObject. hrefs_to_objects : STAC Object HREFs matched to their cached object. ids_to_collections : Map of collection IDs to collections. """ id_keys_to_objects: dict[str, STACObject] """Existing cache of a key made up of the STACObject and it's parents IDs mapped to the cached STACObject.""" hrefs_to_objects: dict[str, STACObject] """STAC Object HREFs matched to their cached object.""" ids_to_collections: dict[str, Collection] """Map of collection IDs to collections.""" _collection_cache: ResolvedObjectCollectionCache | None def __init__( self, id_keys_to_objects: dict[str, STACObject] | None = None, hrefs_to_objects: dict[str, STACObject] | None = None, ids_to_collections: dict[str, Collection] | None = None, ): self.id_keys_to_objects = id_keys_to_objects or {} self.hrefs_to_objects = hrefs_to_objects or {} self.ids_to_collections = ids_to_collections or {} self._collection_cache = None def get_or_cache(self, obj: STACObject) -> STACObject: """Gets the STACObject that is the cached version of the given STACObject; or, if none exists, sets the cached object to the given object. Args: obj : The given object who's cache key will be checked against the cache. Returns: STACObject: Either the cached object that has the same cache key as the given object, or the given object. """ key, is_href = get_cache_key(obj) if is_href: if key in self.hrefs_to_objects: return self.hrefs_to_objects[key] else: self.cache(obj) return obj else: if key in self.id_keys_to_objects: return self.id_keys_to_objects[key] else: self.cache(obj) return obj def get(self, obj: STACObject) -> STACObject | None: """Get the cached object that has the same cache key as the given object. Args: obj : The given object who's cache key will be checked against the cache. Returns: STACObject or None: Either the cached object that has the same cache key as the given object, or None """ key, is_href = get_cache_key(obj) if is_href: return self.get_by_href(key) else: return self.id_keys_to_objects.get(key) def get_by_href(self, href: str) -> STACObject | None: """Gets the cached object at href. Args: href : The href to use as the key for the cached object. Returns: STACObject or None: Returns the STACObject if cached, otherwise None. """ return self.hrefs_to_objects.get(href) def get_collection_by_id(self, id: str) -> Collection | None: """Retrieved a cached Collection by its ID. Args: id : The ID of the collection. Returns: Collection or None: Returns the collection if there is one cached with the given ID, otherwise None. """ return self.ids_to_collections.get(id) def cache(self, obj: STACObject) -> None: """Set the given object into the cache. Args: obj : The object to cache """ key, is_href = get_cache_key(obj) if is_href: self.hrefs_to_objects[key] = obj else: self.id_keys_to_objects[key] = obj if isinstance(obj, pystac.Collection): self.ids_to_collections[obj.id] = obj def remove(self, obj: STACObject) -> None: """Removes any cached object that matches the given object's cache key. Args: obj : The object to remove """ key, is_href = get_cache_key(obj) if is_href: self.hrefs_to_objects.pop(key, None) else: self.id_keys_to_objects.pop(key, None) if obj.STAC_OBJECT_TYPE == pystac.STACObjectType.COLLECTION: self.id_keys_to_objects.pop(obj.id, None) def __contains__(self, obj: STACObject) -> bool: key, is_href = get_cache_key(obj) return ( key in self.hrefs_to_objects if is_href else key in self.id_keys_to_objects ) def contains_collection_id(self, collection_id: str) -> bool: """Returns True if there is a collection with given collection ID is cached.""" return collection_id in self.ids_to_collections def as_collection_cache(self) -> CollectionCache: if self._collection_cache is None: self._collection_cache = ResolvedObjectCollectionCache(self) return self._collection_cache @staticmethod def merge( first: ResolvedObjectCache, second: ResolvedObjectCache ) -> ResolvedObjectCache: """Merges two ResolvedObjectCache. The merged cache will give preference to the first argument; that is, if there are cached keys that exist in both the first and second cache, the object cached in the first will be cached in the resulting merged ResolvedObjectCache. Args: first : The first cache to merge. This cache will be the preferred cache for objects in the case of ID conflicts. second : The second cache to merge. Returns: ResolvedObjectCache: The resulting merged cache. """ merged = ResolvedObjectCache( id_keys_to_objects=dict( ChainMap( copy(first.id_keys_to_objects), copy(second.id_keys_to_objects) ) ), hrefs_to_objects=dict( ChainMap(copy(first.hrefs_to_objects), copy(second.hrefs_to_objects)) ), ids_to_collections=dict( ChainMap( copy(first.ids_to_collections), copy(second.ids_to_collections) ) ), ) merged._collection_cache = ResolvedObjectCollectionCache.merge( merged, first._collection_cache, second._collection_cache ) return merged class CollectionCache: """Cache of collections that can be used to avoid re-reading Collection JSON in :func:`pystac.serialization.merge_common_properties `. The CollectionCache will contain collections as either as dicts or PySTAC Collections, and will set Collection JSON that it reads in order to merge in common properties. """ cached_ids: dict[str, Collection | dict[str, Any]] cached_hrefs: dict[str, Collection | dict[str, Any]] def __init__( self, cached_ids: dict[str, Collection | dict[str, Any]] | None = None, cached_hrefs: dict[str, Collection | dict[str, Any]] | None = None, ): self.cached_ids = cached_ids or {} self.cached_hrefs = cached_hrefs or {} def get_by_id(self, collection_id: str) -> Collection | dict[str, Any] | None: return self.cached_ids.get(collection_id) def get_by_href(self, href: str) -> Collection | dict[str, Any] | None: return self.cached_hrefs.get(href) def contains_id(self, collection_id: str) -> bool: return collection_id in self.cached_ids def cache( self, collection: Collection | dict[str, Any], href: str | None = None, ) -> None: """Caches a collection JSON.""" if isinstance(collection, pystac.Collection): self.cached_ids[collection.id] = collection else: self.cached_ids[collection["id"]] = collection if href is not None: self.cached_hrefs[href] = collection class ResolvedObjectCollectionCache(CollectionCache): resolved_object_cache: ResolvedObjectCache def __init__( self, resolved_object_cache: ResolvedObjectCache, cached_ids: dict[str, Collection | dict[str, Any]] | None = None, cached_hrefs: dict[str, Collection | dict[str, Any]] | None = None, ): super().__init__(cached_ids, cached_hrefs) self.resolved_object_cache = resolved_object_cache def get_by_id(self, collection_id: str) -> Collection | dict[str, Any] | None: result = self.resolved_object_cache.get_collection_by_id(collection_id) if result is None: return super().get_by_id(collection_id) else: return result def get_by_href(self, href: str) -> Collection | dict[str, Any] | None: result = self.resolved_object_cache.get_by_href(href) if result is None: return super().get_by_href(href) else: return cast(pystac.Collection, result) def contains_id(self, collection_id: str) -> bool: return self.resolved_object_cache.contains_collection_id( collection_id ) or super().contains_id(collection_id) def cache( self, collection: Collection | dict[str, Any], href: str | None = None, ) -> None: super().cache(collection, href) @staticmethod def merge( resolved_object_cache: ResolvedObjectCache, first: ResolvedObjectCollectionCache | None, second: ResolvedObjectCollectionCache | None, ) -> ResolvedObjectCollectionCache: first_cached_ids = {} if first is not None: first_cached_ids = copy(first.cached_ids) second_cached_ids = {} if second is not None: second_cached_ids = copy(second.cached_ids) first_cached_hrefs = {} if first is not None: first_cached_hrefs = copy(first.cached_hrefs) second_cached_hrefs = {} if second is not None: second_cached_hrefs = copy(second.cached_hrefs) return ResolvedObjectCollectionCache( resolved_object_cache, cached_ids=dict(ChainMap(first_cached_ids, second_cached_ids)), cached_hrefs=dict(ChainMap(first_cached_hrefs, second_cached_hrefs)), ) pystac-1.9.0/pystac/catalog.py000066400000000000000000001321341451576074700163370ustar00rootroot00000000000000from __future__ import annotations import os import warnings from collections.abc import Iterable, Iterator from copy import deepcopy from itertools import chain from typing import ( TYPE_CHECKING, Any, Callable, TypeVar, Union, cast, ) import pystac from pystac.cache import ResolvedObjectCache from pystac.errors import STACTypeError from pystac.layout import ( BestPracticesLayoutStrategy, HrefLayoutStrategy, LayoutTemplate, ) from pystac.link import Link from pystac.serialization import ( identify_stac_object, identify_stac_object_type, migrate_to_latest, ) from pystac.stac_object import STACObject, STACObjectType from pystac.utils import ( HREF, StringEnum, is_absolute_href, make_absolute_href, make_relative_href, ) if TYPE_CHECKING: from pystac.asset import Asset from pystac.collection import Collection from pystac.extensions.ext import CatalogExt from pystac.item import Item C = TypeVar("C", bound="Catalog") class CatalogType(StringEnum): SELF_CONTAINED = "SELF_CONTAINED" """A 'self-contained catalog' is one that is designed for portability. Users may want to download an online catalog from and be able to use it on their local computer, so all links need to be relative. See: :stac-spec:`The best practices documentation on self-contained catalogs ` """ ABSOLUTE_PUBLISHED = "ABSOLUTE_PUBLISHED" """ Absolute Published Catalog is a catalog that uses absolute links for everything, both in the links objects and in the asset hrefs. See: :stac-spec:`The best practices documentation on published catalogs ` """ RELATIVE_PUBLISHED = "RELATIVE_PUBLISHED" """ Relative Published Catalog is a catalog that uses relative links for everything, but includes an absolute self link at the root catalog, to identify its online location. See: :stac-spec:`The best practices documentation on published catalogs ` """ @classmethod def determine_type(cls, stac_json: dict[str, Any]) -> CatalogType | None: """Determines the catalog type based on a STAC JSON dict. Only applies to Catalogs or Collections Args: stac_json : The STAC JSON dict to determine the catalog type Returns: Optional[CatalogType]: The catalog type of the catalog or collection. Will return None if it cannot be determined. """ self_link = None relative = False for link in stac_json["links"]: if link["rel"] == pystac.RelType.SELF: self_link = link else: relative |= not is_absolute_href(link["href"]) if self_link: if relative: return cls.RELATIVE_PUBLISHED else: return cls.ABSOLUTE_PUBLISHED else: if relative: return cls.SELF_CONTAINED else: return None class Catalog(STACObject): """A PySTAC Catalog represents a STAC catalog in memory. A Catalog is a :class:`~pystac.STACObject` that may contain children, which are instances of :class:`~pystac.Catalog` or :class:`~pystac.Collection`, as well as :class:`~pystac.Item` s. Args: id : Identifier for the catalog. Must be unique within the STAC. description : Detailed multi-line description to fully explain the catalog. `CommonMark 0.29 syntax `_ MAY be used for rich text representation. title : Optional short descriptive one-line title for the catalog. stac_extensions : Optional list of extensions the Catalog implements. href : Optional HREF for this catalog, which be set as the catalog's self link's HREF. catalog_type : Optional catalog type for this catalog. Must be one of the values in :class:`~pystac.CatalogType`. """ catalog_type: CatalogType """The catalog type. Defaults to :attr:`CatalogType.ABSOLUTE_PUBLISHED`.""" description: str """Detailed multi-line description to fully explain the catalog.""" extra_fields: dict[str, Any] """Extra fields that are part of the top-level JSON properties of the Catalog.""" id: str """Identifier for the catalog.""" links: list[Link] """A list of :class:`~pystac.Link` objects representing all links associated with this Catalog.""" title: str | None """Optional short descriptive one-line title for the catalog.""" stac_extensions: list[str] """List of extensions the Catalog implements.""" _resolved_objects: ResolvedObjectCache STAC_OBJECT_TYPE = pystac.STACObjectType.CATALOG _stac_io: pystac.StacIO | None = None """Optional instance of StacIO that will be used by default for any IO operations on objects contained by this catalog. Set while reading in a catalog. This is set when a catalog is read by a StacIO instance.""" DEFAULT_FILE_NAME = "catalog.json" """Default file name that will be given to this STAC object in a canonical format. """ def __init__( self, id: str, description: str, title: str | None = None, stac_extensions: list[str] | None = None, extra_fields: dict[str, Any] | None = None, href: str | None = None, catalog_type: CatalogType = CatalogType.ABSOLUTE_PUBLISHED, ): super().__init__(stac_extensions or []) self.id = id self.description = description self.title = title if extra_fields is None: self.extra_fields = {} else: self.extra_fields = extra_fields self._resolved_objects = ResolvedObjectCache() self.add_link(Link.root(self)) if href is not None: self.set_self_href(href) self.catalog_type: CatalogType = catalog_type self._resolved_objects.cache(self) def __repr__(self) -> str: return f"" def set_root(self, root: Catalog | None) -> None: STACObject.set_root(self, root) if root is not None: root._resolved_objects = ResolvedObjectCache.merge( root._resolved_objects, self._resolved_objects ) # Walk through resolved object links and update the root for link in self.links: if link.rel == pystac.RelType.CHILD or link.rel == pystac.RelType.ITEM: target = link.target if isinstance(target, STACObject): target.set_root(root) def is_relative(self) -> bool: return self.catalog_type in [ CatalogType.RELATIVE_PUBLISHED, CatalogType.SELF_CONTAINED, ] def add_child( self, child: Catalog | Collection, title: str | None = None, strategy: HrefLayoutStrategy | None = None, set_parent: bool = True, ) -> Link: """Adds a link to a child :class:`~pystac.Catalog` or :class:`~pystac.Collection`. This method will set the child's parent to this object and potentially override its self_link (unless ``set_parent`` is False). It will always set its root to this Catalog's root. Args: child : The child to add. title : Optional title to give to the :class:`~pystac.Link` strategy : The layout strategy to use for setting the self href of the child. If not provided, defaults to :class:`~pystac.layout.BestPracticesLayoutStrategy`. set_parent : Whether to set the parent on the child as well. Defaults to True. Returns: Link: The link created for the child """ # Prevent typo confusion if isinstance(child, pystac.Item): raise pystac.STACError("Cannot add item as child. Use add_item instead.") if strategy is None: strategy = BestPracticesLayoutStrategy() child.set_root(self.get_root()) if set_parent: child.set_parent(self) else: child._allow_parent_to_override_href = False # set self link self_href = self.get_self_href() if self_href and set_parent: child_href = strategy.get_href(child, os.path.dirname(self_href)) child.set_self_href(child_href) child_link = Link.child(child, title=title) self.add_link(child_link) return child_link def add_children(self, children: Iterable[Catalog | Collection]) -> list[Link]: """Adds links to multiple :class:`~pystac.Catalog` or `~pystac.Collection` objects. This method will set each child's parent to this object, and their root to this Catalog's root. Args: children : The children to add. Returns: List[Link]: An array of links created for the children """ return [self.add_child(child) for child in children] def add_item( self, item: Item, title: str | None = None, strategy: HrefLayoutStrategy | None = None, set_parent: bool = True, ) -> Link: """Adds a link to an :class:`~pystac.Item`. This method will set the item's parent to this object and potentially override its self_link (unless ``set_parent`` is False) It will always set its root to this Catalog's root. Args: item : The item to add. title : Optional title to give to the :class:`~pystac.Link` strategy : The layout strategy to use for setting the self href of the item. If not provided, defaults to :class:`~pystac.layout.BestPracticesLayoutStrategy`. set_parent : Whether to set the parent on the item as well. Defaults to True. Returns: Link: The link created for the item """ # Prevent typo confusion if isinstance(item, pystac.Catalog): raise pystac.STACError("Cannot add catalog as item. Use add_child instead.") if strategy is None: strategy = BestPracticesLayoutStrategy() item.set_root(self.get_root()) if set_parent: item.set_parent(self) else: item._allow_parent_to_override_href = False # set self link self_href = self.get_self_href() if self_href and set_parent: item_href = strategy.get_href(item, os.path.dirname(self_href)) item.set_self_href(item_href) item_link = Link.item(item, title=title) self.add_link(item_link) return item_link def add_items( self, items: Iterable[Item], strategy: HrefLayoutStrategy | None = None, ) -> list[Link]: """Adds links to multiple :class:`Items `. This method will set each item's parent to this object, and their root to this Catalog's root. Args: items : The items to add. strategy : The layout strategy to use for setting the self href of the items. If not provided, defaults to :class:`~pystac.layout.BestPracticesLayoutStrategy`. Returns: List[Link]: A list of links created for the item """ return [self.add_item(item, strategy=strategy) for item in items] def get_child( self, id: str, recursive: bool = False, sort_links_by_id: bool = True ) -> Catalog | Collection | None: """Gets the child of this catalog with the given ID, if it exists. Args: id : The ID of the child to find. recursive : If True, search this catalog and all children for the item; otherwise, only search the children of this catalog. Defaults to False. sort_links_by_id : If True, links containing the ID will be checked first. If links do not contain the ID then setting this to False will improve performance. Defaults to True. Return: Optional Catalog or Collection: The child with the given ID, or None if not found. """ if not recursive: children: Iterable[pystac.Catalog | pystac.Collection] if not sort_links_by_id: children = self.get_children() else: def sort_function(links: list[Link]) -> list[Link]: return sorted( links, key=lambda x: (href := x.get_href()) is None or id not in href, ) children = map( lambda x: cast(Union[pystac.Catalog, pystac.Collection], x), self.get_stac_objects( pystac.RelType.CHILD, modify_links=sort_function ), ) return next((c for c in children if c.id == id), None) else: for root, _, _ in self.walk(): child = root.get_child(id, recursive=False) if child is not None: return child return None def get_children(self) -> Iterable[Catalog | Collection]: """Return all children of this catalog. Return: Iterable[Catalog or Collection]: Iterable of children who's parent is this catalog. """ return map( lambda x: cast(Union[pystac.Catalog, pystac.Collection], x), self.get_stac_objects(pystac.RelType.CHILD), ) def get_collections(self) -> Iterable[Collection]: """Return all children of this catalog that are :class:`~pystac.Collection` instances.""" return map( lambda x: cast(pystac.Collection, x), self.get_stac_objects(pystac.RelType.CHILD, pystac.Collection), ) def get_all_collections(self) -> Iterable[Collection]: """Get all collections from this catalog and all subcatalogs. Will traverse any subcatalogs recursively.""" yield from self.get_collections() for child in self.get_children(): yield from child.get_collections() def get_child_links(self) -> list[Link]: """Return all child links of this catalog. Return: List[Link]: List of links of this catalog with ``rel == 'child'`` """ return self.get_links(pystac.RelType.CHILD) def clear_children(self) -> None: """Removes all children from this catalog. Return: Catalog: Returns ``self`` """ child_ids = [child.id for child in self.get_children()] for child_id in child_ids: self.remove_child(child_id) def remove_child(self, child_id: str) -> None: """Removes an child from this catalog. Args: child_id : The ID of the child to remove. """ new_links: list[pystac.Link] = [] root = self.get_root() for link in self.links: if link.rel != pystac.RelType.CHILD: new_links.append(link) else: link.resolve_stac_object(root=root) child = cast("Catalog", link.target) if child.id != child_id: new_links.append(link) else: child.set_parent(None) child.set_root(None) self.links = new_links def get_item(self, id: str, recursive: bool = False) -> Item | None: """ DEPRECATED. .. deprecated:: 1.8 Use :meth:`next(pystac.Catalog.get_items(id), None)` instead. Returns an item with a given ID. Args: id : The ID of the item to find. recursive : If True, search this catalog and all children for the item; otherwise, only search the items of this catalog. Defaults to False. Return: Item or None: The item with the given ID, or None if not found. """ warnings.warn( "get_item is deprecated and will be removed in v2. " "Use next(self.get_items(id), None) instead", DeprecationWarning, ) if not recursive: return next((i for i in self.get_items() if i.id == id), None) else: for root, _, _ in self.walk(): item = root.get_item(id, recursive=False) if item is not None: return item return None def get_items(self, *ids: str, recursive: bool = False) -> Iterator[Item]: """Return all items or specific items of this catalog. Args: *ids : The IDs of the items to include. recursive : If True, search this catalog and all children for the item; otherwise, only search the items of this catalog. Defaults to False. Return: Iterator[Item]: Generator of items whose parent is this catalog, and (if recursive) all catalogs or collections connected to this catalog through child links. """ items: Iterator[Item] if not recursive: items = map( lambda x: cast(pystac.Item, x), self.get_stac_objects(pystac.RelType.ITEM), ) else: items = chain( self.get_items(recursive=False), *(child.get_items(recursive=True) for child in self.get_children()), ) if ids: yield from (i for i in items if i.id in ids) else: yield from items def clear_items(self) -> None: """Removes all items from this catalog. Return: Catalog: Returns ``self`` """ for link in self.get_item_links(): if link.is_resolved(): item = cast(pystac.Item, link.target) item.set_parent(None) item.set_root(None) self.links = [link for link in self.links if link.rel != pystac.RelType.ITEM] def remove_item(self, item_id: str) -> None: """Removes an item from this catalog. Args: item_id : The ID of the item to remove. """ new_links: list[pystac.Link] = [] root = self.get_root() for link in self.links: if link.rel != pystac.RelType.ITEM: new_links.append(link) else: link.resolve_stac_object(root=root) item = cast(pystac.Item, link.target) if item.id != item_id: new_links.append(link) else: item.set_parent(None) item.set_root(None) self.links = new_links def get_all_items(self) -> Iterator[Item]: """ DEPRECATED. .. deprecated:: 1.8 Use :meth:`pystac.Catalog.get_items(recursive=True)` instead. Get all items from this catalog and all subcatalogs. Will traverse any subcatalogs recursively. Returns: Generator[Item]: All items that belong to this catalog, and all catalogs or collections connected to this catalog through child links. """ warnings.warn( "get_all_items is deprecated and will be removed in v2", DeprecationWarning, ) return chain( self.get_items(), *(child.get_items(recursive=True) for child in self.get_children()), ) def get_item_links(self) -> list[Link]: """Return all item links of this catalog. Return: List[Link]: List of links of this catalog with ``rel == 'item'`` """ return self.get_links(pystac.RelType.ITEM) def to_dict( self, include_self_link: bool = True, transform_hrefs: bool = True ) -> dict[str, Any]: links = [ x for x in self.links if x.rel != pystac.RelType.ROOT or x.get_href(transform_hrefs) is not None ] if not include_self_link: links = [x for x in links if x.rel != pystac.RelType.SELF] d: dict[str, Any] = { "type": self.STAC_OBJECT_TYPE.value.title(), "id": self.id, "stac_version": pystac.get_stac_version(), "description": self.description, "links": [link.to_dict(transform_href=transform_hrefs) for link in links], } if self.stac_extensions: d["stac_extensions"] = self.stac_extensions for key in self.extra_fields: d[key] = self.extra_fields[key] if self.title is not None: d["title"] = self.title return d def clone(self) -> Catalog: cls = self.__class__ clone = cls( id=self.id, description=self.description, title=self.title, stac_extensions=self.stac_extensions.copy(), extra_fields=deepcopy(self.extra_fields), catalog_type=self.catalog_type, ) clone._resolved_objects.cache(clone) for link in self.links: if link.rel == pystac.RelType.ROOT: # Catalog __init__ sets correct root to clone; don't reset # if the root link points to self root_is_self = link.is_resolved() and link.target is self if not root_is_self: clone.set_root(None) clone.add_link(link.clone()) else: clone.add_link(link.clone()) return clone def make_all_asset_hrefs_relative(self) -> None: """Recursively makes all the HREFs of assets in this catalog relative""" for item in self.get_items(recursive=True): item.make_asset_hrefs_relative() for collection in self.get_all_collections(): collection.make_asset_hrefs_relative() def make_all_asset_hrefs_absolute(self) -> None: """Recursively makes all the HREFs of assets in this catalog absolute""" for item in self.get_items(recursive=True): item.make_asset_hrefs_absolute() for collection in self.get_all_collections(): collection.make_asset_hrefs_absolute() def normalize_and_save( self, root_href: str, catalog_type: CatalogType | None = None, strategy: HrefLayoutStrategy | None = None, stac_io: pystac.StacIO | None = None, skip_unresolved: bool = False, ) -> None: """Normalizes link HREFs to the given root_href, and saves the catalog. This is a convenience method that simply calls :func:`Catalog.normalize_hrefs ` and :func:`Catalog.save ` in sequence. Args: root_href : The absolute HREF that all links will be normalized against. catalog_type : The catalog type that dictates the structure of the catalog to save. Use a member of :class:`~pystac.CatalogType`. Defaults to the root catalog.catalog_type or the current catalog catalog_type if there is no root catalog. strategy : The layout strategy to use in setting the HREFS for this catalog. If not provided, defaults to :class:`~pystac.layout.BestPracticesLayoutStrategy` stac_io : Optional instance of :class:`~pystac.StacIO` to use. If not provided, will use the instance set while reading in the catalog, or the default instance if this is not available. skip_unresolved : Skip unresolved links when normalizing the tree. Defaults to False. Because unresolved links are not saved, this argument can be used to normalize and save only newly-added objects. """ self.normalize_hrefs( root_href, strategy=strategy, skip_unresolved=skip_unresolved ) self.save(catalog_type, stac_io=stac_io) def normalize_hrefs( self, root_href: str, strategy: HrefLayoutStrategy | None = None, skip_unresolved: bool = False, ) -> None: """Normalize HREFs will regenerate all link HREFs based on an absolute root_href and the canonical catalog layout as specified in the STAC specification's best practices. This method mutates the entire catalog tree, unless ``skip_unresolved`` is True, in which case only resolved links are modified. This is useful in the case when you have loaded a large catalog and you've added a few items/children, and you only want to update those newly-added objects, not the whole tree. Args: root_href : The absolute HREF that all links will be normalized against. strategy : The layout strategy to use in setting the HREFS for this catalog. If not provided, defaults to :class:`~pystac.layout.BestPracticesLayoutStrategy` skip_unresolved : Skip unresolved links when normalizing the tree. Defaults to False. See: :stac-spec:`STAC best practices document ` for the canonical layout of a STAC. """ if strategy is None: _strategy: HrefLayoutStrategy = BestPracticesLayoutStrategy() else: _strategy = strategy # Normalizing requires an absolute path if not is_absolute_href(root_href): root_href = make_absolute_href(root_href, os.getcwd(), start_is_dir=True) def process_item( item: Item, _root_href: str, parent: Catalog | None ) -> Callable[[], None] | None: if not skip_unresolved: item.resolve_links() # Abort as the intended parent is not the actual parent # https://github.com/stac-utils/pystac/issues/1116 if parent is not None and item.get_parent() != parent: return None new_self_href = _strategy.get_href(item, _root_href) def fn() -> None: item.set_self_href(new_self_href) return fn def process_catalog( cat: Catalog, _root_href: str, is_root: bool, parent: Catalog | None = None, ) -> list[Callable[[], None]]: setter_funcs: list[Callable[[], None]] = [] if not skip_unresolved: cat.resolve_links() # Abort as the intended parent is not the actual parent # https://github.com/stac-utils/pystac/issues/1116 if parent is not None and cat.get_parent() != parent: return setter_funcs new_self_href = _strategy.get_href(cat, _root_href, is_root) new_root = os.path.dirname(new_self_href) for link in cat.get_links(): if skip_unresolved and not link.is_resolved(): continue elif link.rel == pystac.RelType.ITEM: link.resolve_stac_object(root=self.get_root()) item_fn = process_item( cast(pystac.Item, link.target), new_root, cat ) if item_fn is not None: setter_funcs.append(item_fn) elif link.rel == pystac.RelType.CHILD: link.resolve_stac_object(root=self.get_root()) setter_funcs.extend( process_catalog( cast(Union[pystac.Catalog, pystac.Collection], link.target), new_root, is_root=False, parent=cat, ) ) def fn() -> None: cat.set_self_href(new_self_href) setter_funcs.append(fn) return setter_funcs # Collect functions that will actually mutate the objects. # Delay mutation as setting hrefs while walking the catalog # can result in bad links. setter_funcs = process_catalog(self, root_href, is_root=True) for fn in setter_funcs: fn() def generate_subcatalogs( self, template: str, defaults: dict[str, Any] | None = None, parent_ids: list[str] | None = None, ) -> list[Catalog]: """Walks through the catalog and generates subcatalogs for items based on the template string. See :class:`~pystac.layout.LayoutTemplate` for details on the construction of template strings. This template string will be applied to the items, and subcatalogs will be created that separate and organize the items based on template values. Args: template : A template string that can be consumed by a :class:`~pystac.layout.LayoutTemplate` defaults : Default values for the template variables that will be used if the property cannot be found on the item. parent_ids : Optional list of the parent catalogs' identifiers. If the bottom-most subcatalogs already match the template, no subcatalog is added. Returns: [catalog]: List of new catalogs created """ result: list[Catalog] = [] parent_ids = parent_ids or list() parent_ids.append(self.id) for child in self.get_children(): result.extend( child.generate_subcatalogs( template, defaults=defaults, parent_ids=parent_ids.copy() ) ) layout_template = LayoutTemplate(template, defaults=defaults) keep_item_links: list[Link] = [] item_links = [lk for lk in self.links if lk.rel == pystac.RelType.ITEM] for link in item_links: link.resolve_stac_object(root=self.get_root()) item = cast(pystac.Item, link.target) subcat_ids = layout_template.substitute(item).split("/") id_iter = reversed(parent_ids) if all([f"{id}" == next(id_iter, None) for id in reversed(subcat_ids)]): # Skip items for which the sub-catalog structure already # matches the template. The list of parent IDs can include more # elements on the root side, so compare the reversed sequences. keep_item_links.append(link) continue curr_parent = self for subcat_id in subcat_ids: subcat = curr_parent.get_child(subcat_id) if subcat is None: subcat_desc = "Catalog of items from {} with id {}".format( curr_parent.id, subcat_id ) subcat = pystac.Catalog(id=subcat_id, description=subcat_desc) curr_parent.add_child(subcat) result.append(subcat) curr_parent = subcat # resolve collection link so when added back points to correct location col_link = item.get_single_link(pystac.RelType.COLLECTION) if col_link is not None: col_link.resolve_stac_object() curr_parent.add_item(item) # keep only non-item links and item links that have not been moved elsewhere self.links = [ lk for lk in self.links if lk.rel != pystac.RelType.ITEM ] + keep_item_links return result def save( self, catalog_type: CatalogType | None = None, dest_href: str | None = None, stac_io: pystac.StacIO | None = None, ) -> None: """Save this catalog and all it's children/item to files determined by the object's self link HREF or a specified path. Args: catalog_type : The catalog type that dictates the structure of the catalog to save. Use a member of :class:`~pystac.CatalogType`. If not supplied, the catalog_type of this catalog will be used. If that attribute is not set, an exception will be raised. dest_href : The location where the catalog is to be saved. If not supplied, the catalog's self link HREF is used to determine the location of the catalog file and children's files. stac_io : Optional instance of :class:`~pystac.StacIO` to use. If not provided, will use the instance set while reading in the catalog, or the default instance if this is not available. Note: If the catalog type is ``CatalogType.ABSOLUTE_PUBLISHED``, all self links will be included, and hierarchical links be absolute URLs. If the catalog type is ``CatalogType.RELATIVE_PUBLISHED``, this catalog's self link will be included, but no child catalog will have self links, and hierarchical links will be relative URLs If the catalog type is ``CatalogType.SELF_CONTAINED``, no self links will be included and hierarchical links will be relative URLs. """ root = self.get_root() if root is None: raise Exception("There is no root catalog") if catalog_type is not None: root.catalog_type = catalog_type items_include_self_link = root.catalog_type in [CatalogType.ABSOLUTE_PUBLISHED] for child_link in self.get_child_links(): if child_link.is_resolved(): child = cast(Catalog, child_link.target) if dest_href is not None: rel_href = make_relative_href(child.self_href, self.self_href) child_dest_href = make_absolute_href( rel_href, dest_href, start_is_dir=True ) child.save( dest_href=os.path.dirname(child_dest_href), stac_io=stac_io, ) else: child.save(stac_io=stac_io) for item_link in self.get_item_links(): if item_link.is_resolved(): item = cast(pystac.Item, item_link.target) if dest_href is not None: rel_href = make_relative_href(item.self_href, self.self_href) item_dest_href = make_absolute_href( rel_href, dest_href, start_is_dir=True ) item.save_object( include_self_link=items_include_self_link, dest_href=item_dest_href, stac_io=stac_io, ) else: item.save_object( include_self_link=items_include_self_link, stac_io=stac_io ) include_self_link = False # include a self link if this is the root catalog # or if ABSOLUTE_PUBLISHED catalog if root.catalog_type == CatalogType.ABSOLUTE_PUBLISHED: include_self_link = True elif root.catalog_type != CatalogType.SELF_CONTAINED: root_link = self.get_root_link() if root_link and root_link.get_absolute_href() == self.get_self_href(): include_self_link = True catalog_dest_href = None if dest_href is not None: rel_href = make_relative_href(self.self_href, self.self_href) catalog_dest_href = make_absolute_href( rel_href, dest_href, start_is_dir=True ) self.save_object( include_self_link=include_self_link, dest_href=catalog_dest_href, stac_io=stac_io, ) if catalog_type is not None: self.catalog_type = catalog_type def walk( self, ) -> Iterable[tuple[Catalog, Iterable[Catalog], Iterable[Item]]]: """Walks through children and items of catalogs. For each catalog in the STAC's tree rooted at this catalog (including this catalog itself), it yields a 3-tuple (root, subcatalogs, items). The root in that 3-tuple refers to the current catalog being walked, the subcatalogs are any catalogs or collections for which the root is a parent, and items represents any items that have the root as a parent. This has similar functionality to Python's :func:`os.walk`. Returns: Generator[(Catalog, Generator[Catalog], Generator[Item])]: A generator that yields a 3-tuple (parent_catalog, children, items). """ children = self.get_children() items = self.get_items() yield self, children, items for child in self.get_children(): yield from child.walk() def fully_resolve(self) -> None: """Resolves every link in this catalog. Useful if, e.g., you'd like to read a catalog from a filesystem, upgrade every object in the catalog to the latest STAC version, and save it back to the filesystem. By default, :py:meth:`~pystac.Catalog.save` skips unresolved links. """ for _, _, items in self.walk(): # items is a generator, so we need to consume it to resolve the # items for item in items: pass def validate_all(self, max_items: int | None = None, recursive: bool = True) -> int: """Validates each catalog, collection, item contained within this catalog. Walks through the children and items of the catalog and validates each stac object. Args: max_items : The maximum number of STAC items to validate. Default is None which means, validate them all. recursive : Whether to validate catalog, collections, and items contained within child objects. Returns: int : Number of STAC items validated. Raises: STACValidationError: Raises this error on any item that is invalid. Will raise on the first invalid stac object encountered. """ n = 0 self.validate() for child in self.get_children(): if recursive: inner_max_items = None if max_items is None else max_items - n n += child.validate_all(max_items=inner_max_items, recursive=True) else: child.validate() for item in self.get_items(): if max_items is not None and n >= max_items: break item.validate() n += 1 return n def _object_links(self) -> list[str | pystac.RelType]: return [ pystac.RelType.CHILD, pystac.RelType.ITEM, *pystac.EXTENSION_HOOKS.get_extended_object_links(self), ] def map_items( self, item_mapper: Callable[[Item], Item | list[Item]], ) -> Catalog: """Creates a copy of a catalog, with each item passed through the item_mapper function. Args: item_mapper : A function that takes in an item, and returns either an item or list of items. The item that is passed into the item_mapper is a copy, so the method can mutate it safely. Returns: Catalog: A full copy of this catalog, with items manipulated according to the item_mapper function. """ new_cat = self.full_copy() def process_catalog(catalog: Catalog) -> None: for child in catalog.get_children(): process_catalog(child) item_links: list[Link] = [] for item_link in catalog.get_item_links(): item_link.resolve_stac_object(root=self.get_root()) mapped = item_mapper(cast(pystac.Item, item_link.target)) if mapped is None: raise Exception("item_mapper cannot return None.") if isinstance(mapped, pystac.Item): item_link.target = mapped item_links.append(item_link) else: for i in mapped: new_link = item_link.clone() new_link.target = i item_links.append(new_link) catalog.clear_items() catalog.add_links(item_links) process_catalog(new_cat) return new_cat def map_assets( self, asset_mapper: Callable[ [str, Asset], Asset | tuple[str, Asset] | dict[str, Asset], ], ) -> Catalog: """Creates a copy of a catalog, with each Asset for each Item passed through the asset_mapper function. Args: asset_mapper : A function that takes in an key and an Asset, and returns either an Asset, a (key, Asset), or a dictionary of Assets with unique keys. The Asset that is passed into the item_mapper is a copy, so the method can mutate it safely. Returns: Catalog: A full copy of this catalog, with assets manipulated according to the asset_mapper function. """ def apply_asset_mapper( tup: tuple[str, Asset] ) -> list[tuple[str, pystac.Asset]]: k, v = tup result = asset_mapper(k, v) if result is None: raise Exception("asset_mapper cannot return None.") if isinstance(result, pystac.Asset): return [(k, result)] elif isinstance(result, tuple): return [result] else: assets = list(result.items()) if len(assets) < 1: raise Exception("asset_mapper must return a non-empty list") return assets def item_mapper(item: pystac.Item) -> pystac.Item: new_assets = [ x for result in map(apply_asset_mapper, item.assets.items()) for x in result ] item.assets = dict(new_assets) return item return self.map_items(item_mapper) def describe(self, include_hrefs: bool = False, _indent: int = 0) -> None: """Prints out information about this Catalog and all contained STACObjects. Args: include_hrefs (bool) - If True, print out each object's self link HREF along with the object ID. """ s = "{}* {}".format(" " * _indent, self) if include_hrefs: s += f" {self.get_self_href()}" print(s) for child in self.get_children(): child.describe(include_hrefs=include_hrefs, _indent=_indent + 4) for item in self.get_items(): s = "{}* {}".format(" " * (_indent + 2), item) if include_hrefs: s += f" {item.get_self_href()}" print(s) @classmethod def from_dict( cls: type[C], d: dict[str, Any], href: str | None = None, root: Catalog | None = None, migrate: bool = False, preserve_dict: bool = True, ) -> C: if migrate: info = identify_stac_object(d) d = migrate_to_latest(d, info) if not cls.matches_object_type(d): raise STACTypeError(d, cls) catalog_type = CatalogType.determine_type(d) if preserve_dict: d = deepcopy(d) id = d.pop("id") description = d.pop("description") title = d.pop("title", None) stac_extensions = d.pop("stac_extensions", None) links = d.pop("links") d.pop("stac_version") cat = cls( id=id, description=description, title=title, stac_extensions=stac_extensions, extra_fields=d, href=href, catalog_type=catalog_type or CatalogType.ABSOLUTE_PUBLISHED, ) for link in links: if link["rel"] == pystac.RelType.ROOT: # Remove the link that's generated in Catalog's constructor. cat.remove_links(pystac.RelType.ROOT) if link["rel"] != pystac.RelType.SELF or href is None: cat.add_link(Link.from_dict(link)) if root: cat.set_root(root) return cat def full_copy( self, root: Catalog | None = None, parent: Catalog | None = None ) -> Catalog: return cast(Catalog, super().full_copy(root, parent)) @classmethod def from_file(cls: type[C], href: HREF, stac_io: pystac.StacIO | None = None) -> C: if stac_io is None: stac_io = pystac.StacIO.default() result = super().from_file(href, stac_io) result._stac_io = stac_io return result @classmethod def matches_object_type(cls, d: dict[str, Any]) -> bool: return identify_stac_object_type(d) == STACObjectType.CATALOG @property def ext(self) -> CatalogExt: """Accessor for extension classes on this catalog Example:: print(collection.ext.version) """ from pystac.extensions.ext import CatalogExt return CatalogExt(stac_object=self) pystac-1.9.0/pystac/collection.py000066400000000000000000000620631451576074700170630ustar00rootroot00000000000000from __future__ import annotations import warnings from collections.abc import Iterable from copy import deepcopy from datetime import datetime from typing import ( TYPE_CHECKING, Any, Optional, TypeVar, Union, cast, ) from dateutil import tz import pystac from pystac import CatalogType, STACObjectType from pystac.asset import Asset, Assets from pystac.catalog import Catalog from pystac.errors import DeprecatedWarning, ExtensionNotImplemented, STACTypeError from pystac.layout import HrefLayoutStrategy from pystac.link import Link from pystac.provider import Provider from pystac.serialization import ( identify_stac_object, identify_stac_object_type, migrate_to_latest, ) from pystac.summaries import Summaries from pystac.utils import ( datetime_to_str, str_to_datetime, ) if TYPE_CHECKING: from pystac.extensions.ext import CollectionExt from pystac.item import Item C = TypeVar("C", bound="Collection") Bboxes = list[list[Union[float, int]]] TemporalIntervals = Union[list[list[datetime]], list[list[Optional[datetime]]]] TemporalIntervalsLike = Union[ TemporalIntervals, list[datetime], list[Optional[datetime]] ] class SpatialExtent: """Describes the spatial extent of a Collection. Args: bboxes : A list of bboxes that represent the spatial extent of the collection. Each bbox can be 2D or 3D. The length of the bbox array must be 2*n where n is the number of dimensions. For example, a 2D Collection with only one bbox would be [[xmin, ymin, xmax, ymax]] extra_fields : Dictionary containing additional top-level fields defined on the Spatial Extent object. """ bboxes: Bboxes """A list of bboxes that represent the spatial extent of the collection. Each bbox can be 2D or 3D. The length of the bbox array must be 2*n where n is the number of dimensions. For example, a 2D Collection with only one bbox would be [[xmin, ymin, xmax, ymax]]""" extra_fields: dict[str, Any] """Dictionary containing additional top-level fields defined on the Spatial Extent object.""" def __init__( self, bboxes: Bboxes | list[float | int], extra_fields: dict[str, Any] | None = None, ) -> None: if not isinstance(bboxes, list): raise TypeError("bboxes must be a list") # A common mistake is to pass in a single bbox instead of a list of bboxes. # Account for this by transforming the input in that case. if isinstance(bboxes[0], (float, int)): self.bboxes = [cast(list[Union[float, int]], bboxes)] else: self.bboxes = cast(Bboxes, bboxes) self.extra_fields = extra_fields or {} def to_dict(self) -> dict[str, Any]: """Returns this spatial extent as a dictionary. Returns: dict: A serialization of the SpatialExtent. """ d = {"bbox": self.bboxes, **self.extra_fields} return d def clone(self) -> SpatialExtent: """Clones this object. Returns: SpatialExtent: The clone of this object. """ cls = self.__class__ return cls( bboxes=deepcopy(self.bboxes), extra_fields=deepcopy(self.extra_fields) ) @staticmethod def from_dict(d: dict[str, Any]) -> SpatialExtent: """Constructs a SpatialExtent from a dict. Returns: SpatialExtent: The SpatialExtent deserialized from the JSON dict. """ return SpatialExtent( bboxes=d["bbox"], extra_fields={k: v for k, v in d.items() if k != "bbox"} ) @staticmethod def from_coordinates( coordinates: list[Any], extra_fields: dict[str, Any] | None = None ) -> SpatialExtent: """Constructs a SpatialExtent from a set of coordinates. This method will only produce a single bbox that covers all points in the coordinate set. Args: coordinates : Coordinates to derive the bbox from. extra_fields : Dictionary containing additional top-level fields defined on the SpatialExtent object. Returns: SpatialExtent: A SpatialExtent with a single bbox that covers the given coordinates. """ def process_coords( coord_lists: list[Any], xmin: float | None = None, ymin: float | None = None, xmax: float | None = None, ymax: float | None = None, ) -> tuple[float | None, float | None, float | None, float | None]: for coord in coord_lists: if isinstance(coord[0], list): xmin, ymin, xmax, ymax = process_coords( coord, xmin, ymin, xmax, ymax ) else: x, y = coord if xmin is None or x < xmin: xmin = x elif xmax is None or xmax < x: xmax = x if ymin is None or y < ymin: ymin = y elif ymax is None or ymax < y: ymax = y return xmin, ymin, xmax, ymax xmin, ymin, xmax, ymax = process_coords(coordinates) if xmin is None or ymin is None or xmax is None or ymax is None: raise ValueError( f"Could not determine bounds from coordinate sequence {coordinates}" ) return SpatialExtent( bboxes=[[xmin, ymin, xmax, ymax]], extra_fields=extra_fields ) class TemporalExtent: """Describes the temporal extent of a Collection. Args: intervals : A list of two datetimes wrapped in a list, representing the temporal extent of a Collection. Open date ranges are supported by setting either the start (the first element of the interval) or the end (the second element of the interval) to None. extra_fields : Dictionary containing additional top-level fields defined on the Temporal Extent object. Note: Datetimes are required to be in UTC. """ intervals: TemporalIntervals """A list of two datetimes wrapped in a list, representing the temporal extent of a Collection. Open date ranges are represented by either the start (the first element of the interval) or the end (the second element of the interval) being None.""" extra_fields: dict[str, Any] """Dictionary containing additional top-level fields defined on the Temporal Extent object.""" def __init__( self, intervals: TemporalIntervals | list[datetime | None], extra_fields: dict[str, Any] | None = None, ): if not isinstance(intervals, list): raise TypeError("intervals must be a list") # A common mistake is to pass in a single interval instead of a # list of intervals. Account for this by transforming the input # in that case. if isinstance(intervals[0], datetime) or intervals[0] is None: self.intervals = [cast(list[Optional[datetime]], intervals)] else: self.intervals = cast(TemporalIntervals, intervals) self.extra_fields = extra_fields or {} def to_dict(self) -> dict[str, Any]: """Returns this temporal extent as a dictionary. Returns: dict: A serialization of the TemporalExtent. """ encoded_intervals: list[list[str | None]] = [] for i in self.intervals: start = None end = None if i[0] is not None: start = datetime_to_str(i[0]) if i[1] is not None: end = datetime_to_str(i[1]) encoded_intervals.append([start, end]) d = {"interval": encoded_intervals, **self.extra_fields} return d def clone(self) -> TemporalExtent: """Clones this object. Returns: TemporalExtent: The clone of this object. """ cls = self.__class__ return cls( intervals=deepcopy(self.intervals), extra_fields=deepcopy(self.extra_fields) ) @staticmethod def from_dict(d: dict[str, Any]) -> TemporalExtent: """Constructs an TemporalExtent from a dict. Returns: TemporalExtent: The TemporalExtent deserialized from the JSON dict. """ parsed_intervals: list[list[datetime | None]] = [] for i in d["interval"]: if isinstance(i, str): # d["interval"] is a list of strings, so we correct the list and # try again # https://github.com/stac-utils/pystac/issues/1221 warnings.warn( "A collection's temporal extent should be a list of lists, but " "is instead a " "list of strings. pystac is fixing this issue and continuing " "deserialization, but note that the source " "collection is invalid STAC.", UserWarning, ) d["interval"] = [d["interval"]] return TemporalExtent.from_dict(d) start = None end = None if i[0]: start = str_to_datetime(i[0]) if i[1]: end = str_to_datetime(i[1]) parsed_intervals.append([start, end]) return TemporalExtent( intervals=parsed_intervals, extra_fields={k: v for k, v in d.items() if k != "interval"}, ) @staticmethod def from_now() -> TemporalExtent: """Constructs an TemporalExtent with a single open interval that has the start time as the current time. Returns: TemporalExtent: The resulting TemporalExtent. """ return TemporalExtent( intervals=[[datetime.utcnow().replace(microsecond=0), None]] ) class Extent: """Describes the spatiotemporal extents of a Collection. Args: spatial : Potential spatial extent covered by the collection. temporal : Potential temporal extent covered by the collection. extra_fields : Dictionary containing additional top-level fields defined on the Extent object. """ spatial: SpatialExtent """Potential spatial extent covered by the collection.""" temporal: TemporalExtent """Potential temporal extent covered by the collection.""" extra_fields: dict[str, Any] """Dictionary containing additional top-level fields defined on the Extent object.""" def __init__( self, spatial: SpatialExtent, temporal: TemporalExtent, extra_fields: dict[str, Any] | None = None, ): self.spatial = spatial self.temporal = temporal self.extra_fields = extra_fields or {} def to_dict(self) -> dict[str, Any]: """Returns this extent as a dictionary. Returns: dict: A serialization of the Extent. """ d = { "spatial": self.spatial.to_dict(), "temporal": self.temporal.to_dict(), **self.extra_fields, } return d def clone(self) -> Extent: """Clones this object. Returns: Extent: The clone of this extent. """ cls = self.__class__ return cls( spatial=self.spatial.clone(), temporal=self.temporal.clone(), extra_fields=deepcopy(self.extra_fields), ) @staticmethod def from_dict(d: dict[str, Any]) -> Extent: """Constructs an Extent from a dict. Returns: Extent: The Extent deserialized from the JSON dict. """ return Extent( spatial=SpatialExtent.from_dict(d["spatial"]), temporal=TemporalExtent.from_dict(d["temporal"]), extra_fields={ k: v for k, v in d.items() if k not in {"spatial", "temporal"} }, ) @staticmethod def from_items( items: Iterable[Item], extra_fields: dict[str, Any] | None = None ) -> Extent: """Create an Extent based on the datetimes and bboxes of a list of items. Args: items : A list of items to derive the extent from. extra_fields : Optional dictionary containing additional top-level fields defined on the Extent object. Returns: Extent: An Extent that spatially and temporally covers all of the given items. """ bounds_values: list[list[float]] = [ [float("inf")], [float("inf")], [float("-inf")], [float("-inf")], ] datetimes: list[datetime] = [] starts: list[datetime] = [] ends: list[datetime] = [] for item in items: if item.bbox is not None: for i in range(0, 4): bounds_values[i].append(item.bbox[i]) if item.datetime is not None: datetimes.append(item.datetime) if item.common_metadata.start_datetime is not None: starts.append(item.common_metadata.start_datetime) if item.common_metadata.end_datetime is not None: ends.append(item.common_metadata.end_datetime) if not any(datetimes + starts): start_timestamp = None else: start_timestamp = min( [ dt if dt.tzinfo else dt.replace(tzinfo=tz.UTC) for dt in datetimes + starts ] ) if not any(datetimes + ends): end_timestamp = None else: end_timestamp = max( [ dt if dt.tzinfo else dt.replace(tzinfo=tz.UTC) for dt in datetimes + ends ] ) spatial = SpatialExtent( [ [ min(bounds_values[0]), min(bounds_values[1]), max(bounds_values[2]), max(bounds_values[3]), ] ] ) temporal = TemporalExtent([[start_timestamp, end_timestamp]]) return Extent(spatial=spatial, temporal=temporal, extra_fields=extra_fields) class Collection(Catalog, Assets): """A Collection extends the Catalog spec with additional metadata that helps enable discovery. Args: id : Identifier for the collection. Must be unique within the STAC. description : Detailed multi-line description to fully explain the collection. `CommonMark 0.29 syntax `_ MAY be used for rich text representation. extent : Spatial and temporal extents that describe the bounds of all items contained within this Collection. title : Optional short descriptive one-line title for the collection. stac_extensions : Optional list of extensions the Collection implements. href : Optional HREF for this collection, which be set as the collection's self link's HREF. catalog_type : Optional catalog type for this catalog. Must be one of the values in :class`~pystac.CatalogType`. license : Collection's license(s) as a `SPDX License identifier `_, `various`, or `proprietary`. If collection includes data with multiple different licenses, use `various` and add a link for each. Defaults to 'proprietary'. keywords : Optional list of keywords describing the collection. providers : Optional list of providers of this Collection. summaries : An optional map of property summaries, either a set of values or statistics such as a range. extra_fields : Extra fields that are part of the top-level JSON properties of the Collection. assets : A dictionary mapping string keys to :class:`~pystac.Asset` objects. All :class:`~pystac.Asset` values in the dictionary will have their :attr:`~pystac.Asset.owner` attribute set to the created Collection. """ description: str """Detailed multi-line description to fully explain the collection.""" extent: Extent """Spatial and temporal extents that describe the bounds of all items contained within this Collection.""" id: str """Identifier for the collection.""" stac_extensions: list[str] """List of extensions the Collection implements.""" title: str | None """Optional short descriptive one-line title for the collection.""" keywords: list[str] | None """Optional list of keywords describing the collection.""" providers: list[Provider] | None """Optional list of providers of this Collection.""" summaries: Summaries """A map of property summaries, either a set of values or statistics such as a range.""" links: list[Link] """A list of :class:`~pystac.Link` objects representing all links associated with this Collection.""" extra_fields: dict[str, Any] """Extra fields that are part of the top-level JSON properties of the Collection.""" STAC_OBJECT_TYPE = STACObjectType.COLLECTION DEFAULT_FILE_NAME = "collection.json" """Default file name that will be given to this STAC object in a canonical format.""" def __init__( self, id: str, description: str, extent: Extent, title: str | None = None, stac_extensions: list[str] | None = None, href: str | None = None, extra_fields: dict[str, Any] | None = None, catalog_type: CatalogType | None = None, license: str = "proprietary", keywords: list[str] | None = None, providers: list[Provider] | None = None, summaries: Summaries | None = None, assets: dict[str, Asset] | None = None, ): super().__init__( id, description, title, stac_extensions, extra_fields, href, catalog_type or CatalogType.ABSOLUTE_PUBLISHED, ) self.extent = extent self.license = license self.stac_extensions: list[str] = stac_extensions or [] self.keywords = keywords self.providers = providers self.summaries = summaries or Summaries.empty() self.assets = {} if assets is not None: for k, asset in assets.items(): self.add_asset(k, asset) def __repr__(self) -> str: return f"" def add_item( self, item: Item, title: str | None = None, strategy: HrefLayoutStrategy | None = None, set_parent: bool = True, ) -> Link: link = super().add_item(item, title, strategy, set_parent) item.set_collection(self) return link def to_dict( self, include_self_link: bool = True, transform_hrefs: bool = True ) -> dict[str, Any]: d = super().to_dict( include_self_link=include_self_link, transform_hrefs=transform_hrefs ) d["extent"] = self.extent.to_dict() d["license"] = self.license if self.stac_extensions: d["stac_extensions"] = self.stac_extensions if self.keywords: d["keywords"] = self.keywords if self.providers: d["providers"] = list(map(lambda x: x.to_dict(), self.providers)) if not self.summaries.is_empty(): d["summaries"] = self.summaries.to_dict() if any(self.assets): d["assets"] = {k: v.to_dict() for k, v in self.assets.items()} return d def clone(self) -> Collection: cls = self.__class__ clone = cls( id=self.id, description=self.description, extent=self.extent.clone(), title=self.title, stac_extensions=self.stac_extensions.copy(), extra_fields=deepcopy(self.extra_fields), catalog_type=self.catalog_type, license=self.license, keywords=self.keywords.copy() if self.keywords is not None else None, providers=deepcopy(self.providers), summaries=self.summaries.clone(), assets={k: asset.clone() for k, asset in self.assets.items()}, ) clone._resolved_objects.cache(clone) for link in self.links: if link.rel == pystac.RelType.ROOT: # Collection __init__ sets correct root to clone; don't reset # if the root link points to self root_is_self = link.is_resolved() and link.target is self if not root_is_self: clone.set_root(None) clone.add_link(link.clone()) else: clone.add_link(link.clone()) return clone @classmethod def from_dict( cls: type[C], d: dict[str, Any], href: str | None = None, root: Catalog | None = None, migrate: bool = False, preserve_dict: bool = True, ) -> C: from pystac.extensions.version import CollectionVersionExtension if migrate: info = identify_stac_object(d) d = migrate_to_latest(d, info) if not cls.matches_object_type(d): raise STACTypeError(d, cls) catalog_type = CatalogType.determine_type(d) if preserve_dict: d = deepcopy(d) id = d.pop("id") description = d.pop("description") license = d.pop("license") extent = Extent.from_dict(d.pop("extent")) title = d.pop("title", None) stac_extensions = d.pop("stac_extensions", None) keywords = d.pop("keywords", None) providers = d.pop("providers", None) if providers is not None: providers = list(map(lambda x: pystac.Provider.from_dict(x), providers)) summaries = d.pop("summaries", None) if summaries is not None: summaries = Summaries(summaries) assets = d.pop("assets", None) if assets: assets = {k: Asset.from_dict(v) for k, v in assets.items()} links = d.pop("links") d.pop("stac_version") collection = cls( id=id, description=description, extent=extent, title=title, stac_extensions=stac_extensions, extra_fields=d, license=license, keywords=keywords, providers=providers, summaries=summaries, href=href, catalog_type=catalog_type, assets=assets, ) for link in links: if link["rel"] == pystac.RelType.ROOT: # Remove the link that's generated in Catalog's constructor. collection.remove_links(pystac.RelType.ROOT) if link["rel"] != pystac.RelType.SELF or href is None: collection.add_link(Link.from_dict(link)) if root: collection.set_root(root) try: version = CollectionVersionExtension.ext(collection) if version.deprecated: warnings.warn( f"The collection '{collection.id}' is deprecated.", DeprecatedWarning, ) # Collection asset deprecation checks pending version extension support except ExtensionNotImplemented: pass return collection def get_item(self, id: str, recursive: bool = False) -> Item | None: """Returns an item with a given ID. Args: id : The ID of the item to find. recursive : If True, search this collection and all children for the item; otherwise, only search the items of this collection. Defaults to False. Return: Item or None: The item with the given ID, or None if not found. """ try: return next(self.get_items(id, recursive=recursive), None) except TypeError as e: if any("recursive" in arg for arg in e.args): # For inherited classes that do not yet support recursive # See https://github.com/stac-utils/pystac-client/issues/485 return super().get_item(id, recursive=recursive) raise e def update_extent_from_items(self) -> None: """ Update datetime and bbox based on all items to a single bbox and time window. """ self.extent = Extent.from_items(self.get_items(recursive=True)) def full_copy( self, root: Catalog | None = None, parent: Catalog | None = None ) -> Collection: return cast(Collection, super().full_copy(root, parent)) @classmethod def matches_object_type(cls, d: dict[str, Any]) -> bool: return identify_stac_object_type(d) == STACObjectType.COLLECTION @property def ext(self) -> CollectionExt: """Accessor for extension classes on this collection Example:: print(collection.ext.xarray) """ from pystac.extensions.ext import CollectionExt return CollectionExt(stac_object=self) pystac-1.9.0/pystac/common_metadata.py000066400000000000000000000165201451576074700200550ustar00rootroot00000000000000from __future__ import annotations from datetime import datetime from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast import pystac from pystac import utils from pystac.errors import STACError if TYPE_CHECKING: from pystac.asset import Asset from pystac.item import Item from pystac.provider import Provider P = TypeVar("P") class CommonMetadata: """Object containing fields that are not included in core item schema but are still commonly used. All attributes are defined within the properties of this item and are optional Args: properties : Dictionary of attributes that is the Item's properties """ object: Asset | Item """The object from which common metadata is obtained.""" def __init__(self, object: Asset | Item): self.object = object def _set_field(self, prop_name: str, v: Any | None) -> None: if hasattr(self.object, prop_name): setattr(self.object, prop_name, v) elif hasattr(self.object, "properties"): item = cast(pystac.Item, self.object) if v is None: item.properties.pop(prop_name, None) else: item.properties[prop_name] = v elif hasattr(self.object, "extra_fields") and isinstance( self.object.extra_fields, dict ): if v is None: self.object.extra_fields.pop(prop_name, None) else: self.object.extra_fields[prop_name] = v else: raise pystac.STACError(f"Cannot set field {prop_name} on {self}.") def _get_field(self, prop_name: str, _typ: type[P]) -> P | None: if hasattr(self.object, prop_name): return cast(Optional[P], getattr(self.object, prop_name)) elif hasattr(self.object, "properties"): item = cast(pystac.Item, self.object) return item.properties.get(prop_name) elif hasattr(self.object, "extra_fields") and isinstance( self.object.extra_fields, dict ): return self.object.extra_fields.get(prop_name) else: raise STACError(f"Cannot get field {prop_name} from {self}.") # Basics @property def title(self) -> str | None: """Gets or set the object's title.""" return self._get_field("title", str) @title.setter def title(self, v: str | None) -> None: self._set_field("title", v) @property def description(self) -> str | None: """Gets or set the object's description.""" return self._get_field("description", str) @description.setter def description(self, v: str | None) -> None: self._set_field("description", v) # Date and Time Range @property def start_datetime(self) -> datetime | None: """Get or set the object's start_datetime.""" return utils.map_opt( utils.str_to_datetime, self._get_field("start_datetime", str) ) @start_datetime.setter def start_datetime(self, v: datetime | None) -> None: self._set_field("start_datetime", utils.map_opt(utils.datetime_to_str, v)) @property def end_datetime(self) -> datetime | None: """Get or set the item's end_datetime.""" return utils.map_opt( utils.str_to_datetime, self._get_field("end_datetime", str) ) @end_datetime.setter def end_datetime(self, v: datetime | None) -> None: self._set_field("end_datetime", utils.map_opt(utils.datetime_to_str, v)) # License @property def license(self) -> str | None: """Get or set the current license.""" return self._get_field("license", str) @license.setter def license(self, v: str | None) -> None: self._set_field("license", v) # Providers @property def providers(self) -> list[Provider] | None: """Get or set a list of the object's providers.""" return utils.map_opt( lambda providers: [pystac.Provider.from_dict(d) for d in providers], self._get_field("providers", list[dict[str, Any]]), ) @providers.setter def providers(self, v: list[Provider] | None) -> None: self._set_field( "providers", utils.map_opt(lambda providers: [p.to_dict() for p in providers], v), ) # Instrument @property def platform(self) -> str | None: """Gets or set the object's platform attribute.""" return self._get_field("platform", str) @platform.setter def platform(self, v: str | None) -> None: self._set_field("platform", v) @property def instruments(self) -> list[str] | None: """Gets or sets the names of the instruments used.""" return self._get_field("instruments", list[str]) @instruments.setter def instruments(self, v: list[str] | None) -> None: self._set_field("instruments", v) @property def constellation(self) -> str | None: """Gets or set the name of the constellation associate with an object.""" return self._get_field("constellation", str) @constellation.setter def constellation(self, v: str | None) -> None: self._set_field("constellation", v) @property def mission(self) -> str | None: """Gets or set the name of the mission associated with an object.""" return self._get_field("mission", str) @mission.setter def mission(self, v: str | None) -> None: self._set_field("mission", v) @property def gsd(self) -> float | None: """Gets or sets the Ground Sample Distance at the sensor.""" return self._get_field("gsd", float) @gsd.setter def gsd(self, v: float | None) -> None: self._set_field("gsd", v) # Metadata @property def created(self) -> datetime | None: """Get or set the metadata file's creation date. All datetime attributes have setters that can take either a string or a datetime, but always stores the attribute as a string. Note: ``created`` has a different meaning depending on the type of STAC object. On an :class:`~pystac.Item`, it refers to the creation time of the metadata. On an :class:`~pystac.Asset`, it refers to the creation time of the actual data linked to in :attr:`Asset.href None: self._set_field("created", utils.map_opt(utils.datetime_to_str, v)) @property def updated(self) -> datetime | None: """Get or set the metadata file's update date. All datetime attributes have setters that can take either a string or a datetime, but always stores the attribute as a string Note: ``updated`` has a different meaning depending on the type of STAC object. On an :class:`~pystac.Item`, it refers to the update time of the metadata. On an :class:`~pystac.Asset`, it refers to the update time of the actual data linked to in :attr:`Asset.href None: self._set_field("updated", utils.map_opt(utils.datetime_to_str, v)) pystac-1.9.0/pystac/errors.py000066400000000000000000000072001451576074700162340ustar00rootroot00000000000000from typing import Any, Optional, Union class TemplateError(Exception): """Exception thrown when an error occurs during converting a template string into data for :class:`~pystac.layout.LayoutTemplate` """ pass class STACError(Exception): """A STACError is raised for errors relating to STAC, e.g. for invalid formats or trying to operate on a STAC that does not have the required information available. """ pass class STACTypeError(Exception): """A STACTypeError is raised when encountering a representation of a STAC entity that is not correct for the context; for example, if a Catalog JSON was read in as an Item. """ def __init__( self, bad_dict: dict[str, Any], expected: type, extra_message: Optional[str] = "", ): """ Construct an exception with an appropriate error message from bad_dict and the expected that it didn't align with. Args: bad_dict: Dictionary that did not match the expected type expected: The expected type. extra_message: message that will be appended to the exception message. """ message = ( f"JSON (id = {bad_dict.get('id', 'unknown')}) does not represent" f" a {expected.__name__} instance." ) if extra_message: message += " " + extra_message super().__init__(message) class DuplicateObjectKeyError(Exception): """Raised when deserializing a JSON object containing a duplicate key.""" pass class ExtensionTypeError(Exception): """An ExtensionTypeError is raised when an extension is used against an object that the extension does not apply to """ pass class ExtensionAlreadyExistsError(Exception): """An ExtensionAlreadyExistsError is raised when extension hooks are registered with PySTAC if there are already hooks registered for an extension with the same ID.""" pass class ExtensionNotImplemented(Exception): """Attempted to extend a STAC object that does not implement the given extension.""" class RequiredPropertyMissing(Exception): """This error is raised when a required value was expected to be there but was missing or None. This will happen, for example, in an extension that has required properties, where the required property is missing from the extended object Args: obj: Description of the object that will have a property missing. Should include a __repr__ that identifies the object for the error message, or be a string that describes the object. prop: The property that is missing """ def __init__( self, obj: Union[str, Any], prop: str, msg: Optional[str] = None ) -> None: msg = msg or f"{repr(obj)} does not have required property {prop}" super().__init__(msg) class STACLocalValidationError(Exception): """Schema not available locally""" class STACValidationError(Exception): """Represents a validation error. Thrown by validation calls if the STAC JSON is invalid. Args: source : Source of the exception. Type will be determined by the validation implementation. For the default JsonSchemaValidator this will a the ``jsonschema.ValidationError``. """ def __init__(self, message: str, source: Optional[Any] = None): super().__init__(message) self.source = source class DeprecatedWarning(FutureWarning): """Issued when converting a dictionary to a STAC Item or Collection and the version extension ``deprecated`` field is present and set to ``True``.""" pass pystac-1.9.0/pystac/extensions/000077500000000000000000000000001451576074700165465ustar00rootroot00000000000000pystac-1.9.0/pystac/extensions/__init__.py000066400000000000000000000000001451576074700206450ustar00rootroot00000000000000pystac-1.9.0/pystac/extensions/base.py000066400000000000000000000241531451576074700200370ustar00rootroot00000000000000from __future__ import annotations import re import warnings from abc import ABC, abstractmethod from collections.abc import Iterable from typing import ( TYPE_CHECKING, Any, Generic, TypeVar, cast, ) import pystac if TYPE_CHECKING: from pystac.extensions.item_assets import AssetDefinition VERSION_REGEX = re.compile("/v[0-9].[0-9].*/") class SummariesExtension: """Base class for extending the properties in :attr:`pystac.Collection.summaries` to include properties defined by a STAC Extension. This class should generally not be instantiated directly. Instead, create an extension-specific class that inherits from this class and instantiate that. See :class:`~pystac.extensions.eo.SummariesEOExtension` for an example.""" summaries: pystac.Summaries """The summaries for the :class:`~pystac.Collection` being extended.""" def __init__(self, collection: pystac.Collection) -> None: self.summaries = collection.summaries def _set_summary( self, prop_key: str, v: list[Any] | pystac.RangeSummary[Any] | dict[str, Any] | None, ) -> None: if v is None: self.summaries.remove(prop_key) else: self.summaries.add(prop_key, v) P = TypeVar("P") class PropertiesExtension(ABC): """Abstract base class for extending the properties of an :class:`~pystac.Item` to include properties defined by a STAC Extension. This class should not be instantiated directly. Instead, create an extension-specific class that inherits from this class and instantiate that. See :class:`~pystac.extensions.eo.PropertiesEOExtension` for an example. """ properties: dict[str, Any] """The properties that this extension wraps. The extension which implements PropertiesExtension can use ``_get_property`` and ``_set_property`` to get and set values on this instance. Note that _set_properties mutates the properties directly.""" additional_read_properties: Iterable[dict[str, Any]] | None = None """Additional read-only properties accessible from the extended object. These are used when extending an :class:`~pystac.Asset` to give access to the properties of the owning :class:`~pystac.Item`. If a property exists in both ``additional_read_properties`` and ``properties``, the value in ``additional_read_properties`` will take precedence. """ def _get_property(self, prop_name: str, _typ: type[P]) -> P | None: maybe_property: P | None = self.properties.get(prop_name) if maybe_property is not None: return maybe_property if self.additional_read_properties is not None: for props in self.additional_read_properties: maybe_additional_property: P | None = props.get(prop_name) if maybe_additional_property is not None: return maybe_additional_property return None def _set_property( self, prop_name: str, v: Any | None, pop_if_none: bool = True ) -> None: if v is None and pop_if_none: self.properties.pop(prop_name, None) elif isinstance(v, list): self.properties[prop_name] = [ x.to_dict() if hasattr(x, "to_dict") else x for x in v ] else: self.properties[prop_name] = v S = TypeVar("S", bound=pystac.STACObject) class ExtensionManagementMixin(Generic[S], ABC): """Abstract base class with methods for adding and removing extensions from STAC Objects. This class is generic over the type of object being extended (e.g. :class:`~pystac.Item`). Concrete extension implementations should inherit from this class and either provide a concrete type or a bounded type variable. See :class:`~pystac.extensions.eo.EOExtension` for an example implementation. """ @classmethod @abstractmethod def get_schema_uri(cls) -> str: """Gets the schema URI associated with this extension.""" raise NotImplementedError @classmethod def get_schema_uris(cls) -> list[str]: """Gets a list of schema URIs associated with this extension.""" warnings.warn( "get_schema_uris is deprecated and will be removed in v2", DeprecationWarning, ) return [cls.get_schema_uri()] @classmethod def add_to(cls, obj: S) -> None: """Add the schema URI for this extension to the :attr:`~pystac.STACObject.stac_extensions` list for the given object, if it is not already present.""" if obj.stac_extensions is None: obj.stac_extensions = [cls.get_schema_uri()] elif not cls.has_extension(obj): obj.stac_extensions.append(cls.get_schema_uri()) @classmethod def remove_from(cls, obj: S) -> None: """Remove the schema URI for this extension from the :attr:`pystac.STACObject.stac_extensions` list for the given object.""" if obj.stac_extensions is not None: obj.stac_extensions = [ uri for uri in obj.stac_extensions if uri != cls.get_schema_uri() ] @classmethod def has_extension(cls, obj: S) -> bool: """Check if the given object implements this extension by checking :attr:`pystac.STACObject.stac_extensions` for this extension's schema URI.""" schema_startswith = VERSION_REGEX.split(cls.get_schema_uri())[0] + "/" return obj.stac_extensions is not None and any( uri.startswith(schema_startswith) for uri in obj.stac_extensions ) @classmethod def validate_owner_has_extension( cls, asset: pystac.Asset | AssetDefinition, add_if_missing: bool = False, ) -> None: """ DEPRECATED .. deprecated:: 1.9 Use :meth:`ensure_owner_has_extension` instead. Given an :class:`~pystac.Asset`, checks if the asset's owner has this extension's schema URI in its :attr:`~pystac.STACObject.stac_extensions` list. If ``add_if_missing`` is ``True``, the schema URI will be added to the owner. Args: asset : The asset whose owner should be validated. add_if_missing : Whether to add the schema URI to the owner if it is not already present. Defaults to False. Raises: STACError : If ``add_if_missing`` is ``True`` and ``asset.owner`` is ``None``. """ warnings.warn( "ensure_owner_has_extension is deprecated. " "Use ensure_owner_has_extension instead", DeprecationWarning, ) return cls.ensure_owner_has_extension(asset, add_if_missing) @classmethod def ensure_owner_has_extension( cls, asset_or_link: pystac.Asset | AssetDefinition | pystac.Link, add_if_missing: bool = False, ) -> None: """Given an :class:`~pystac.Asset`, checks if the asset's owner has this extension's schema URI in its :attr:`~pystac.STACObject.stac_extensions` list. If ``add_if_missing`` is ``True``, the schema URI will be added to the owner. Args: asset : The asset whose owner should be validated. add_if_missing : Whether to add the schema URI to the owner if it is not already present. Defaults to False. Raises: STACError : If ``add_if_missing`` is ``True`` and ``asset.owner`` is ``None``. """ if asset_or_link.owner is None: if add_if_missing: raise pystac.STACError( f"Attempted to use add_if_missing=True for a {type(asset_or_link)} " "with no owner. Use .set_owner or set add_if_missing=False." ) else: return return cls.ensure_has_extension(cast(S, asset_or_link.owner), add_if_missing) @classmethod def validate_has_extension(cls, obj: S, add_if_missing: bool = False) -> None: """ DEPRECATED .. deprecated:: 1.9 Use :meth:`ensure_has_extension` instead. Given a :class:`~pystac.STACObject`, checks if the object has this extension's schema URI in its :attr:`~pystac.STACObject.stac_extensions` list. If ``add_if_missing`` is ``True``, the schema URI will be added to the object. Args: obj : The object to validate. add_if_missing : Whether to add the schema URI to the object if it is not already present. Defaults to False. """ warnings.warn( "validate_has_extension is deprecated. Use ensure_has_extension instead", DeprecationWarning, ) return cls.ensure_has_extension(obj, add_if_missing) @classmethod def ensure_has_extension(cls, obj: S, add_if_missing: bool = False) -> None: """Given a :class:`~pystac.STACObject`, checks if the object has this extension's schema URI in its :attr:`~pystac.STACObject.stac_extensions` list. If ``add_if_missing`` is ``True``, the schema URI will be added to the object. Args: obj : The object to validate. add_if_missing : Whether to add the schema URI to the object if it is not already present. Defaults to False. """ if add_if_missing: cls.add_to(obj) if not cls.has_extension(obj): name = getattr(cls, "name", cls.__name__) suggestion = ( f"``obj.ext.add('{name}')" if hasattr(cls, "name") else f"``{name}.add_to(obj)``" ) raise pystac.ExtensionNotImplemented( f"Extension '{name}' is not implemented on object." f"STAC producers can add the extension using {suggestion}" ) @classmethod def _ext_error_message(cls, obj: Any) -> str: contents = [f"{cls.__name__} does not apply to type '{type(obj).__name__}'"] if hasattr(cls, "summaries") and isinstance(obj, pystac.Collection): hint = f"Hint: Did you mean to use `{cls.__name__}.summaries` instead?" contents.append(hint) return ". ".join(contents) pystac-1.9.0/pystac/extensions/classification.py000066400000000000000000000522551451576074700221240ustar00rootroot00000000000000"""Implements the :stac-ext:`Classification `.""" from __future__ import annotations import re import warnings from collections.abc import Iterable from re import Pattern from typing import ( Any, Generic, Literal, TypeVar, Union, cast, ) import pystac from pystac.extensions import item_assets from pystac.extensions.base import ( ExtensionManagementMixin, PropertiesExtension, SummariesExtension, ) from pystac.extensions.hooks import ExtensionHooks from pystac.extensions.raster import RasterBand from pystac.serialization.identify import STACJSONDescription, STACVersionID from pystac.utils import get_required, map_opt T = TypeVar("T", pystac.Item, pystac.Asset, item_assets.AssetDefinition, RasterBand) SCHEMA_URI_PATTERN: str = ( "https://stac-extensions.github.io/classification/v{version}/schema.json" ) DEFAULT_VERSION: str = "1.1.0" SUPPORTED_VERSIONS: list[str] = ["1.1.0", "1.0.0"] # Field names PREFIX: str = "classification:" BITFIELDS_PROP: str = PREFIX + "bitfields" CLASSES_PROP: str = PREFIX + "classes" RASTER_BANDS_PROP: str = "raster:bands" COLOR_HINT_PATTERN: Pattern[str] = re.compile("^([0-9A-Fa-f]{6})$") class Classification: """Represents a single category of a classification. Use Classification.create to create a new Classification. """ properties: dict[str, Any] def __init__(self, properties: dict[str, Any]) -> None: self.properties = properties def apply( self, value: int, description: str, name: str | None = None, color_hint: str | None = None, ) -> None: """ Set the properties for a new Classification. Args: value: The integer value corresponding to this class description: The description of this class name: The optional human-readable short name for this class color_hint: An optional hexadecimal string-encoded representation of the RGB color that is suggested to represent this class (six hexadecimal characters, all capitalized) """ self.value = value self.name = name self.description = description self.color_hint = color_hint if color_hint is not None: match = COLOR_HINT_PATTERN.match(color_hint) assert ( color_hint is None or match is not None and match.group() == color_hint ), "Must format color hints as '^([0-9A-F]{6})$'" if color_hint is not None: match = COLOR_HINT_PATTERN.match(color_hint) assert ( color_hint is None or match is not None and match.group() == color_hint ), "Must format color hints as '^([0-9A-F]{6})$'" @classmethod def create( cls, value: int, description: str, name: str | None = None, color_hint: str | None = None, ) -> Classification: """ Create a new Classification. Args: value: The integer value corresponding to this class name: The human-readable short name for this class description: The optional long-form description of this class color_hint: An optional hexadecimal string-encoded representation of the RGB color that is suggested to represent this class (six hexadecimal characters, all capitalized) """ c = cls({}) c.apply( value=value, name=name, description=description, color_hint=color_hint, ) return c @property def value(self) -> int: """Get or set the class value Returns: int """ return get_required(self.properties.get("value"), self, "value") @value.setter def value(self, v: int) -> None: self.properties["value"] = v @property def description(self) -> str: """Get or set the description of the class Returns: str """ return get_required(self.properties.get("description"), self, "description") @description.setter def description(self, v: str) -> None: self.properties["description"] = v @property def name(self) -> str | None: """Get or set the name of the class Returns: Optional[str] """ return self.properties.get("name") @name.setter def name(self, v: str | None) -> None: if v is not None: self.properties["name"] = v else: self.properties.pop("name", None) @property def color_hint(self) -> str | None: """Get or set the optional color hint for this class. The color hint must be a six-character string of capitalized hexadecimal characters ([0-9A-F]). Returns: Optional[str] """ return self.properties.get("color_hint") @color_hint.setter def color_hint(self, v: str | None) -> None: if v is not None: match = COLOR_HINT_PATTERN.match(v) assert ( v is None or match is not None and match.group() == v ), "Must format color hints as '^([0-9A-F]{6})$'" self.properties["color_hint"] = v else: self.properties.pop("color_hint", None) def to_dict(self) -> dict[str, Any]: """Returns the dictionary encoding of this class Returns: dict: The serialization of the Classification """ return self.properties def __eq__(self, other: object) -> bool: if not isinstance(other, Classification): raise NotImplementedError return ( self.value == other.value and self.description == other.description and self.name == other.name and self.color_hint == other.color_hint ) def __repr__(self) -> str: return f"" class Bitfield: """Encodes the representation of values as bits in an integer. Use Bitfield.create to create a new Bitfield. """ properties: dict[str, Any] def __init__(self, properties: dict[str, Any]): self.properties = properties def apply( self, offset: int, length: int, classes: list[Classification], roles: list[str] | None = None, description: str | None = None, name: str | None = None, ) -> None: """Sets the properties for this Bitfield. Args: offset: describes the position of the least significant bit captured by this bitfield, with zero indicating the least significant binary digit length: the number of bits described by this bitfield classes: a list of Classification objects describing the various levels captured by this bitfield roles: the optional role of this bitfield (see `Asset Roles `) description: an optional short description of the classification name: the optional name of the class for machine readability """ self.offset = offset self.length = length self.classes = classes self.roles = roles self.description = description self.name = name assert offset >= 0, "Non-negative offsets only" assert length >= 1, "Positive field lengths only" assert len(classes) > 0, "Must specify at least one class" assert ( roles is None or len(roles) > 0 ), "When set, roles must contain at least one item" @classmethod def create( cls, offset: int, length: int, classes: list[Classification], roles: list[str] | None = None, description: str | None = None, name: str | None = None, ) -> Bitfield: """Sets the properties for this Bitfield. Args: offset: describes the position of the least significant bit captured by this bitfield, with zero indicating the least significant binary digit length: the number of bits described by this bitfield classes: a list of Classification objects describing the various levels captured by this bitfield roles: the optional role of this bitfield (see `Asset Roles `) description: an optional short description of the classification name: the optional name of the class for machine readability """ b = cls({}) b.apply( offset=offset, length=length, classes=classes, roles=roles, description=description, name=name, ) return b @property def offset(self) -> int: """Get or set the offset of the bitfield. Describes the position of the least significant bit captured by this bitfield, with zero indicating the least significant binary digit Returns: int """ return get_required(self.properties.get("offset"), self, "offset") @offset.setter def offset(self, v: int) -> None: self.properties["offset"] = v @property def length(self) -> int: """Get or set the length (number of bits) of the bitfield Returns: int """ return get_required(self.properties.get("length"), self, "length") @length.setter def length(self, v: int) -> None: self.properties["length"] = v @property def classes(self) -> list[Classification]: """Get or set the class definitions for the bitfield Returns: List[Classification] """ return [ Classification(d) for d in cast( list[dict[str, Any]], get_required( self.properties.get("classes"), self, "classes", ), ) ] @classes.setter def classes(self, v: list[Classification]) -> None: self.properties["classes"] = [c.to_dict() for c in v] @property def roles(self) -> list[str] | None: """Get or set the role of the bitfield. See `Asset Roles ` Returns: Optional[List[str]] """ return self.properties.get("roles") @roles.setter def roles(self, v: list[str] | None) -> None: if v is not None: self.properties["roles"] = v else: self.properties.pop("roles", None) @property def description(self) -> str | None: """Get or set the optional description of a bitfield. Returns: Optional[str] """ return self.properties.get("description") @description.setter def description(self, v: str | None) -> None: if v is not None: self.properties["description"] = v else: self.properties.pop("description", None) @property def name(self) -> str | None: """Get or set the optional name of the bitfield. Returns: Optional[str] """ return self.properties.get("name") @name.setter def name(self, v: str | None) -> None: if v is not None: self.properties["name"] = v else: self.properties.pop("name", None) def __repr__(self) -> str: return ( f"" ) def to_dict(self) -> dict[str, Any]: """Returns the dictionary encoding of this bitfield Returns: dict: The serialization of the Bitfield """ return self.properties class ClassificationExtension( Generic[T], PropertiesExtension, ExtensionManagementMixin[Union[pystac.Item, pystac.Collection]], ): """An abstract class that can be used to extend the properties of :class:`~pystac.Item`, :class:`~pystac.Asset`, :class:`~pystac.extension.raster.RasterBand`, or :class:`~pystac.extension.item_assets.AssetDefinition` with properties from the :stac-ext:`Classification Extension `. This class is generic over the type of STAC object being extended. This class is not to be instantiated directly. One can either directly use the subclass corresponding to the object you are extending, or the `ext` class method can be used to construct the proper class for you. """ name: Literal["classification"] = "classification" properties: dict[str, Any] """The :class:`~pystac.Asset` fields, including extension properties.""" def apply( self, classes: list[Classification] | None = None, bitfields: list[Bitfield] | None = None, ) -> None: """Applies the classifiation extension fields to the extended object. Note: one may set either the classes or bitfields objects, but not both. Args: classes: a list of :class:`~pystac.extension.classification.Classification` objects describing the various classes in the classification """ assert ( classes is None and bitfields is not None or bitfields is None and classes is not None ), "Must set exactly one of `classes` or `bitfields`" self.classes = classes self.bitfields = bitfields @property def classes(self) -> list[Classification] | None: """Get or set the classes for the base object Note: Setting the classes will clear the object's bitfields if they are not None Returns: Optional[List[Classification]] """ return self._get_classes() @classes.setter def classes(self, v: list[Classification] | None) -> None: if self._get_bitfields() is not None: self.bitfields = None self._set_property( CLASSES_PROP, map_opt(lambda classes: [c.to_dict() for c in classes], v) ) def _get_classes(self) -> list[Classification] | None: return map_opt( lambda classes: [Classification(c) for c in classes], self._get_property(CLASSES_PROP, list[dict[str, Any]]), ) @property def bitfields(self) -> list[Bitfield] | None: """Get or set the bitfields for the base object Note: Setting the bitfields will clear the object's classes if they are not None Returns: Optional[List[Bitfield]] """ return self._get_bitfields() @bitfields.setter def bitfields(self, v: list[Bitfield] | None) -> None: if self._get_classes() is not None: self.classes = None self._set_property( BITFIELDS_PROP, map_opt(lambda bitfields: [b.to_dict() for b in bitfields], v), ) def _get_bitfields(self) -> list[Bitfield] | None: return map_opt( lambda bitfields: [Bitfield(b) for b in bitfields], self._get_property(BITFIELDS_PROP, list[dict[str, Any]]), ) @classmethod def get_schema_uri(cls) -> str: return SCHEMA_URI_PATTERN.format(version=DEFAULT_VERSION) @classmethod def get_schema_uris(cls) -> list[str]: warnings.warn( "get_schema_uris is deprecated and will be removed in v2", DeprecationWarning, ) return [SCHEMA_URI_PATTERN.format(version=v) for v in SUPPORTED_VERSIONS] @classmethod def ext(cls, obj: T, add_if_missing: bool = False) -> ClassificationExtension[T]: """Extends the given STAC object with propertied from the :stac-ext:`Classification Extension ` This extension can be applied to instances of :class:`~pystac.Item`, :class:`~pystac.Asset`, :class:`~pystac.extensions.item_assets.AssetDefinition`, or :class:`~pystac.extension.raster.RasterBand`. Raises: pystac.ExtensionTypeError : If an invalid object type is passed """ if isinstance(obj, pystac.Item): cls.ensure_has_extension(obj, add_if_missing) return cast(ClassificationExtension[T], ItemClassificationExtension(obj)) elif isinstance(obj, pystac.Asset): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(ClassificationExtension[T], AssetClassificationExtension(obj)) elif isinstance(obj, item_assets.AssetDefinition): cls.ensure_owner_has_extension(obj, add_if_missing) return cast( ClassificationExtension[T], ItemAssetsClassificationExtension(obj) ) elif isinstance(obj, RasterBand): return cast( ClassificationExtension[T], RasterBandClassificationExtension(obj) ) else: raise pystac.ExtensionTypeError(cls._ext_error_message(obj)) @classmethod def summaries( cls, obj: pystac.Collection, add_if_missing: bool = False ) -> SummariesClassificationExtension: cls.ensure_has_extension(obj, add_if_missing) return SummariesClassificationExtension(obj) class ItemClassificationExtension(ClassificationExtension[pystac.Item]): item: pystac.Item properties: dict[str, Any] def __init__(self, item: pystac.Item): self.item = item self.properties = item.properties def __repr__(self) -> str: return f"" class AssetClassificationExtension(ClassificationExtension[pystac.Asset]): asset: pystac.Asset asset_href: str properties: dict[str, Any] additional_read_properties: Iterable[dict[str, Any]] | None def __init__(self, asset: pystac.Asset): self.asset = asset self.asset_href = asset.href self.properties = asset.extra_fields if asset.owner and isinstance(asset.owner, pystac.Item): self.additional_read_properties = [asset.owner.properties] def __repr__(self) -> str: return f"" class ItemAssetsClassificationExtension( ClassificationExtension[item_assets.AssetDefinition] ): properties: dict[str, Any] asset_defn: item_assets.AssetDefinition def __init__(self, item_asset: item_assets.AssetDefinition): self.asset_defn = item_asset self.properties = item_asset.properties def __repr__(self) -> str: return f" str: return f"" class SummariesClassificationExtension(SummariesExtension): @property def classes(self) -> list[Classification] | None: return map_opt( lambda classes: [Classification(c) for c in classes], self.summaries.get_list(CLASSES_PROP), ) @classes.setter def classes(self, v: list[Classification] | None) -> None: self._set_summary(CLASSES_PROP, map_opt(lambda x: [c.to_dict() for c in x], v)) @property def bitfields(self) -> list[Bitfield] | None: return map_opt( lambda bitfields: [Bitfield(b) for b in bitfields], self.summaries.get_list(BITFIELDS_PROP), ) @bitfields.setter def bitfields(self, v: list[Bitfield] | None) -> None: self._set_summary( BITFIELDS_PROP, map_opt(lambda x: [b.to_dict() for b in x], v) ) class ClassificationExtensionHooks(ExtensionHooks): schema_uri: str = SCHEMA_URI_PATTERN.format(version=DEFAULT_VERSION) prev_extension_ids = { SCHEMA_URI_PATTERN.format(version=v) for v in SUPPORTED_VERSIONS if v != DEFAULT_VERSION } stac_object_types = {pystac.STACObjectType.ITEM} def migrate( self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription ) -> None: if SCHEMA_URI_PATTERN.format(version="1.0.0") in info.extensions: for asset in obj.get("assets", {}).values(): classification_classes = asset.get(CLASSES_PROP, None) if classification_classes is None or not isinstance( classification_classes, list ): continue for class_object in classification_classes: if "color-hint" in class_object: class_object["color_hint"] = class_object["color-hint"] del class_object["color-hint"] super().migrate(obj, version, info) CLASSIFICATION_EXTENSION_HOOKS: ExtensionHooks = ClassificationExtensionHooks() pystac-1.9.0/pystac/extensions/datacube.py000066400000000000000000000617311451576074700207000ustar00rootroot00000000000000"""Implements the :stac-ext:`Datacube Extension `.""" from __future__ import annotations from abc import ABC from typing import Any, Generic, Literal, TypeVar, Union, cast import pystac from pystac.extensions import item_assets from pystac.extensions.base import ExtensionManagementMixin, PropertiesExtension from pystac.extensions.hooks import ExtensionHooks from pystac.utils import StringEnum, get_required, map_opt T = TypeVar( "T", pystac.Collection, pystac.Item, pystac.Asset, item_assets.AssetDefinition ) SCHEMA_URI = "https://stac-extensions.github.io/datacube/v2.2.0/schema.json" PREFIX: str = "cube:" DIMENSIONS_PROP = PREFIX + "dimensions" VARIABLES_PROP = PREFIX + "variables" # Dimension properties DIM_TYPE_PROP = "type" DIM_DESC_PROP = "description" DIM_AXIS_PROP = "axis" DIM_EXTENT_PROP = "extent" DIM_VALUES_PROP = "values" DIM_STEP_PROP = "step" DIM_REF_SYS_PROP = "reference_system" DIM_UNIT_PROP = "unit" # Variable properties VAR_TYPE_PROP = "type" VAR_DESC_PROP = "description" VAR_EXTENT_PROP = "extent" VAR_VALUES_PROP = "values" VAR_DIMENSIONS_PROP = "dimensions" VAR_UNIT_PROP = "unit" class DimensionType(StringEnum): """Dimension object types for spatial and temporal Dimension Objects.""" SPATIAL = "spatial" GEOMETRIES = "geometries" TEMPORAL = "temporal" class HorizontalSpatialDimensionAxis(StringEnum): """Allowed values for ``axis`` field of :class:`HorizontalSpatialDimension` object.""" X = "x" Y = "y" class VerticalSpatialDimensionAxis(StringEnum): """Allowed values for ``axis`` field of :class:`VerticalSpatialDimension` object.""" Z = "z" class Dimension(ABC): """Object representing a dimension of the datacube. The fields contained in Dimension Object vary by ``type``. See the :stac-ext:`Datacube Dimension Object ` docs for details. """ properties: dict[str, Any] def __init__(self, properties: dict[str, Any]) -> None: self.properties = properties @property def dim_type(self) -> DimensionType | str: """The type of the dimension. Must be ``"spatial"`` for :stac-ext:`Horizontal Spatial Dimension Objects ` or :stac-ext:`Vertical Spatial Dimension Objects `, ``geometries`` for :stac-ext:`Spatial Vector Dimension Objects ` ``"temporal"`` for :stac-ext:`Temporal Dimension Objects `. May be an arbitrary string for :stac-ext:`Additional Dimension Objects `.""" return get_required( self.properties.get(DIM_TYPE_PROP), "cube:dimension", DIM_TYPE_PROP ) @dim_type.setter def dim_type(self, v: DimensionType | str) -> None: self.properties[DIM_TYPE_PROP] = v @property def description(self) -> str | None: """Detailed multi-line description to explain the dimension. `CommonMark 0.29 `__ syntax MAY be used for rich text representation.""" return self.properties.get(DIM_DESC_PROP) @description.setter def description(self, v: str | None) -> None: if v is None: self.properties.pop(DIM_DESC_PROP, None) else: self.properties[DIM_DESC_PROP] = v def to_dict(self) -> dict[str, Any]: return self.properties @staticmethod def from_dict(d: dict[str, Any]) -> Dimension: dim_type: str = get_required( d.get(DIM_TYPE_PROP), "cube_dimension", DIM_TYPE_PROP ) if dim_type == DimensionType.SPATIAL: axis: str = get_required( d.get(DIM_AXIS_PROP), "cube_dimension", DIM_AXIS_PROP ) if axis == "z": return VerticalSpatialDimension(d) else: return HorizontalSpatialDimension(d) elif dim_type == DimensionType.GEOMETRIES: return VectorSpatialDimension(d) elif dim_type == DimensionType.TEMPORAL: # The v1.0.0 spec says that AdditionalDimensions can have # type 'temporal', but it is unclear how to differentiate that # from a temporal dimension. Just key off of type for now. # See https://github.com/stac-extensions/datacube/issues/5 return TemporalDimension(d) else: return AdditionalDimension(d) class SpatialDimension(Dimension): @property def extent(self) -> list[float]: """Extent (lower and upper bounds) of the dimension as two-dimensional array. Open intervals with ``None`` are not allowed.""" return get_required( self.properties.get(DIM_EXTENT_PROP), "cube:dimension", DIM_EXTENT_PROP ) @extent.setter def extent(self, v: list[float]) -> None: self.properties[DIM_EXTENT_PROP] = v @property def values(self) -> list[float] | None: """Optional set of all potential values.""" return self.properties.get(DIM_VALUES_PROP) @values.setter def values(self, v: list[float] | None) -> None: if v is None: self.properties.pop(DIM_VALUES_PROP, None) else: self.properties[DIM_VALUES_PROP] = v @property def step(self) -> float | None: """The space between the values. Use ``None`` for irregularly spaced steps.""" return self.properties.get(DIM_STEP_PROP) @step.setter def step(self, v: float | None) -> None: self.properties[DIM_STEP_PROP] = v def clear_step(self) -> None: """Setting step to None sets it to the null value, which means irregularly spaced steps. Use clear_step to remove it from the properties.""" self.properties.pop(DIM_STEP_PROP, None) @property def reference_system(self) -> str | float | dict[str, Any] | None: """The spatial reference system for the data, specified as `numerical EPSG code `__, `WKT2 (ISO 19162) string `__ or `PROJJSON object `__. Defaults to EPSG code 4326.""" return self.properties.get(DIM_REF_SYS_PROP) @reference_system.setter def reference_system(self, v: str | float | dict[str, Any] | None) -> None: if v is None: self.properties.pop(DIM_REF_SYS_PROP, None) else: self.properties[DIM_REF_SYS_PROP] = v class HorizontalSpatialDimension(SpatialDimension): @property def axis(self) -> HorizontalSpatialDimensionAxis: """Axis of the spatial dimension. Must be one of ``"x"`` or ``"y"``.""" return get_required( self.properties.get(DIM_AXIS_PROP), "cube:dimension", DIM_AXIS_PROP ) @axis.setter def axis(self, v: HorizontalSpatialDimensionAxis) -> None: self.properties[DIM_AXIS_PROP] = v class VerticalSpatialDimension(SpatialDimension): @property def axis(self) -> VerticalSpatialDimensionAxis: """Axis of the spatial dimension. Must be ``"z"``.""" return get_required( self.properties.get(DIM_AXIS_PROP), "cube:dimension", DIM_AXIS_PROP ) @axis.setter def axis(self, v: VerticalSpatialDimensionAxis) -> None: self.properties[DIM_AXIS_PROP] = v @property def unit(self) -> str | None: """The unit of measurement for the data, preferably compliant to `UDUNITS-2 `__ units (singular).""" return self.properties.get(DIM_UNIT_PROP) @unit.setter def unit(self, v: str | None) -> None: if v is None: self.properties.pop(DIM_UNIT_PROP, None) else: self.properties[DIM_UNIT_PROP] = v class VectorSpatialDimension(Dimension): @property def axes(self) -> list[str] | None: """Axes of the vector dimension as an ordered set of `x`, `y` and `z`.""" return self.properties.get("axes") @axes.setter def axes(self, v: list[str]) -> None: if v is None: self.properties.pop("axes", None) else: self.properties["axes"] = v @property def bbox(self) -> list[float]: """A single bounding box of the geometries as defined for STAC Collections but not nested.""" return get_required(self.properties.get("bbox"), "cube:bbox", "bbox") @bbox.setter def bbox(self, v: list[float]) -> None: self.properties["bbox"] = v @property def values(self) -> list[str] | None: """Optionally, a representation of the geometries. This could be a list of WKT strings or other identifiers.""" return self.properties.get(DIM_VALUES_PROP) @values.setter def values(self, v: list[str] | None) -> None: if v is None: self.properties.pop(DIM_VALUES_PROP, None) else: self.properties[DIM_VALUES_PROP] = v @property def geometry_types(self) -> list[str] | None: """A set of geometry types. If not present, mixed geometry types must be assumed.""" return self.properties.get("geometry_types") @geometry_types.setter def geometry_types(self, v: list[str] | None) -> None: if v is None: self.properties.pop("geometry_types", None) else: self.properties["geometry_types"] = v @property def reference_system(self) -> str | float | dict[str, Any] | None: """The reference system for the data.""" return self.properties.get(DIM_REF_SYS_PROP) @reference_system.setter def reference_system(self, v: str | float | dict[str, Any] | None) -> None: if v is None: self.properties.pop(DIM_REF_SYS_PROP, None) else: self.properties[DIM_REF_SYS_PROP] = v class TemporalDimension(Dimension): @property def extent(self) -> list[str | None] | None: """Extent (lower and upper bounds) of the dimension as two-dimensional array. The dates and/or times must be strings compliant to `ISO 8601 `__. ``None`` is allowed for open date ranges.""" return self.properties.get(DIM_EXTENT_PROP) @extent.setter def extent(self, v: list[str | None] | None) -> None: if v is None: self.properties.pop(DIM_EXTENT_PROP, None) else: self.properties[DIM_EXTENT_PROP] = v @property def values(self) -> list[str] | None: """If the dimension consists of set of specific values they can be listed here. The dates and/or times must be strings compliant to `ISO 8601 `__.""" return self.properties.get(DIM_VALUES_PROP) @values.setter def values(self, v: list[str] | None) -> None: if v is None: self.properties.pop(DIM_VALUES_PROP, None) else: self.properties[DIM_VALUES_PROP] = v @property def step(self) -> str | None: """The space between the temporal instances as `ISO 8601 duration `__, e.g. P1D. Use null for irregularly spaced steps.""" return self.properties.get(DIM_STEP_PROP) @step.setter def step(self, v: str | None) -> None: self.properties[DIM_STEP_PROP] = v def clear_step(self) -> None: """Setting step to None sets it to the null value, which means irregularly spaced steps. Use clear_step to remove it from the properties.""" self.properties.pop(DIM_STEP_PROP, None) class AdditionalDimension(Dimension): @property def extent(self) -> list[float | None] | None: """If the dimension consists of `ordinal `__ values, the extent (lower and upper bounds) of the values as two-dimensional array. Use null for open intervals.""" return self.properties.get(DIM_EXTENT_PROP) @extent.setter def extent(self, v: list[float | None] | None) -> None: if v is None: self.properties.pop(DIM_EXTENT_PROP, None) else: self.properties[DIM_EXTENT_PROP] = v @property def values(self) -> list[str] | list[float] | None: """A set of all potential values, especially useful for `nominal `__ values.""" return self.properties.get(DIM_VALUES_PROP) @values.setter def values(self, v: list[str] | list[float] | None) -> None: if v is None: self.properties.pop(DIM_VALUES_PROP, None) else: self.properties[DIM_VALUES_PROP] = v @property def step(self) -> float | None: """If the dimension consists of `interval `__ values, the space between the values. Use null for irregularly spaced steps.""" return self.properties.get(DIM_STEP_PROP) @step.setter def step(self, v: float | None) -> None: self.properties[DIM_STEP_PROP] = v def clear_step(self) -> None: """Setting step to None sets it to the null value, which means irregularly spaced steps. Use clear_step to remove it from the properties.""" self.properties.pop(DIM_STEP_PROP, None) @property def unit(self) -> str | None: """The unit of measurement for the data, preferably compliant to `UDUNITS-2 units `__ (singular).""" return self.properties.get(DIM_UNIT_PROP) @unit.setter def unit(self, v: str | None) -> None: if v is None: self.properties.pop(DIM_UNIT_PROP, None) else: self.properties[DIM_UNIT_PROP] = v @property def reference_system(self) -> str | float | dict[str, Any] | None: """The reference system for the data.""" return self.properties.get(DIM_REF_SYS_PROP) @reference_system.setter def reference_system(self, v: str | float | dict[str, Any] | None) -> None: if v is None: self.properties.pop(DIM_REF_SYS_PROP, None) else: self.properties[DIM_REF_SYS_PROP] = v class VariableType(StringEnum): """Variable object types""" DATA = "data" AUXILIARY = "auxiliary" class Variable: """Object representing a variable in the datacube. The dimensions field lists zero or more :stac-ext:`Datacube Dimension Object ` instances. See the :stac-ext:`Datacube Variable Object ` docs for details. """ properties: dict[str, Any] def __init__(self, properties: dict[str, Any]) -> None: self.properties = properties @property def dimensions(self) -> list[str]: """The dimensions of the variable. Should refer to keys in the ``cube:dimensions`` object or be an empty list if the variable has no dimensions """ return get_required( self.properties.get(VAR_DIMENSIONS_PROP), "cube:variable", VAR_DIMENSIONS_PROP, ) @dimensions.setter def dimensions(self, v: list[str]) -> None: self.properties[VAR_DIMENSIONS_PROP] = v @property def var_type(self) -> VariableType | str: """Type of the variable, either ``data`` or ``auxiliary``""" return get_required( self.properties.get(VAR_TYPE_PROP), "cube:variable", VAR_TYPE_PROP ) @var_type.setter def var_type(self, v: VariableType | str) -> None: self.properties[VAR_TYPE_PROP] = v @property def description(self) -> str | None: """Detailed multi-line description to explain the variable. `CommonMark 0.29 `__ syntax MAY be used for rich text representation.""" return self.properties.get(VAR_DESC_PROP) @description.setter def description(self, v: str | None) -> None: if v is None: self.properties.pop(VAR_DESC_PROP, None) else: self.properties[VAR_DESC_PROP] = v @property def extent(self) -> list[float | str | None]: """If the variable consists of `ordinal values `, the extent (lower and upper bounds) of the values as two-dimensional array. Use ``None`` for open intervals""" return get_required( self.properties.get(VAR_EXTENT_PROP), "cube:variable", VAR_EXTENT_PROP ) @extent.setter def extent(self, v: list[float | str | None]) -> None: self.properties[VAR_EXTENT_PROP] = v @property def values(self) -> list[float | str] | None: """A set of all potential values, especially useful for `nominal values `.""" return self.properties.get(VAR_VALUES_PROP) @values.setter def values(self, v: list[float | str] | None) -> None: if v is None: self.properties.pop(VAR_VALUES_PROP) else: self.properties[VAR_VALUES_PROP] = v @property def unit(self) -> str | None: """The unit of measurement for the data, preferably compliant to `UDUNITS-2 ` units (singular)""" return self.properties.get(VAR_UNIT_PROP) @unit.setter def unit(self, v: str | None) -> None: if v is None: self.properties.pop(VAR_UNIT_PROP) else: self.properties[VAR_UNIT_PROP] = v @staticmethod def from_dict(d: dict[str, Any]) -> Variable: return Variable(d) def to_dict(self) -> dict[str, Any]: return self.properties class DatacubeExtension( Generic[T], PropertiesExtension, ExtensionManagementMixin[Union[pystac.Collection, pystac.Item]], ): """An abstract class that can be used to extend the properties of a :class:`~pystac.Collection`, :class:`~pystac.Item`, or :class:`~pystac.Asset` with properties from the :stac-ext:`Datacube Extension `. This class is generic over the type of STAC Object to be extended (e.g. :class:`~pystac.Item`, :class:`~pystac.Asset`). To create a concrete instance of :class:`DatacubeExtension`, use the :meth:`DatacubeExtension.ext` method. For example: .. code-block:: python >>> item: pystac.Item = ... >>> dc_ext = DatacubeExtension.ext(item) """ name: Literal["cube"] = "cube" def apply( self, dimensions: dict[str, Dimension], variables: dict[str, Variable] | None = None, ) -> None: """Applies Datacube Extension properties to the extended :class:`~pystac.Collection`, :class:`~pystac.Item` or :class:`~pystac.Asset`. Args: dimensions : Dictionary mapping dimension name to :class:`Dimension` objects. variables : Dictionary mapping variable name to a :class:`Variable` object. """ self.dimensions = dimensions self.variables = variables @property def dimensions(self) -> dict[str, Dimension]: """A dictionary where each key is the name of a dimension and each value is a :class:`~Dimension` object. """ result = get_required( self._get_property(DIMENSIONS_PROP, dict[str, Any]), self, DIMENSIONS_PROP ) return {k: Dimension.from_dict(v) for k, v in result.items()} @dimensions.setter def dimensions(self, v: dict[str, Dimension]) -> None: self._set_property(DIMENSIONS_PROP, {k: dim.to_dict() for k, dim in v.items()}) @property def variables(self) -> dict[str, Variable] | None: """A dictionary where each key is the name of a variable and each value is a :class:`~Variable` object. """ result = self._get_property(VARIABLES_PROP, dict[str, Any]) if result is None: return None return {k: Variable.from_dict(v) for k, v in result.items()} @variables.setter def variables(self, v: dict[str, Variable] | None) -> None: self._set_property( VARIABLES_PROP, map_opt( lambda variables: {k: var.to_dict() for k, var in variables.items()}, v ), ) @classmethod def get_schema_uri(cls) -> str: return SCHEMA_URI @classmethod def ext(cls, obj: T, add_if_missing: bool = False) -> DatacubeExtension[T]: """Extends the given STAC Object with properties from the :stac-ext:`Datacube Extension `. This extension can be applied to instances of :class:`~pystac.Collection`, :class:`~pystac.Item` or :class:`~pystac.Asset`. Raises: pystac.ExtensionTypeError : If an invalid object type is passed. """ if isinstance(obj, pystac.Collection): cls.ensure_has_extension(obj, add_if_missing) return cast(DatacubeExtension[T], CollectionDatacubeExtension(obj)) if isinstance(obj, pystac.Item): cls.ensure_has_extension(obj, add_if_missing) return cast(DatacubeExtension[T], ItemDatacubeExtension(obj)) elif isinstance(obj, pystac.Asset): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(DatacubeExtension[T], AssetDatacubeExtension(obj)) elif isinstance(obj, item_assets.AssetDefinition): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(DatacubeExtension[T], ItemAssetsDatacubeExtension(obj)) else: raise pystac.ExtensionTypeError(cls._ext_error_message(obj)) class CollectionDatacubeExtension(DatacubeExtension[pystac.Collection]): """A concrete implementation of :class:`DatacubeExtension` on an :class:`~pystac.Collection` that extends the properties of the Item to include properties defined in the :stac-ext:`Datacube Extension `. This class should generally not be instantiated directly. Instead, call :meth:`DatacubeExtension.ext` on an :class:`~pystac.Collection` to extend it. """ collection: pystac.Collection properties: dict[str, Any] def __init__(self, collection: pystac.Collection): self.collection = collection self.properties = collection.extra_fields def __repr__(self) -> str: return f"" class ItemDatacubeExtension(DatacubeExtension[pystac.Item]): """A concrete implementation of :class:`DatacubeExtension` on an :class:`~pystac.Item` that extends the properties of the Item to include properties defined in the :stac-ext:`Datacube Extension `. This class should generally not be instantiated directly. Instead, call :meth:`DatacubeExtension.ext` on an :class:`~pystac.Item` to extend it. """ item: pystac.Item properties: dict[str, Any] def __init__(self, item: pystac.Item): self.item = item self.properties = item.properties def __repr__(self) -> str: return f"" class AssetDatacubeExtension(DatacubeExtension[pystac.Asset]): """A concrete implementation of :class:`DatacubeExtension` on an :class:`~pystac.Asset` that extends the Asset fields to include properties defined in the :stac-ext:`Datacube Extension `. This class should generally not be instantiated directly. Instead, call :meth:`EOExtension.ext` on an :class:`~pystac.Asset` to extend it. """ asset_href: str properties: dict[str, Any] additional_read_properties: list[dict[str, Any]] | None def __init__(self, asset: pystac.Asset): self.asset_href = asset.href self.properties = asset.extra_fields if asset.owner and isinstance(asset.owner, pystac.Item): self.additional_read_properties = [asset.owner.properties] else: self.additional_read_properties = None def __repr__(self) -> str: return f"" class ItemAssetsDatacubeExtension(DatacubeExtension[item_assets.AssetDefinition]): properties: dict[str, Any] asset_defn: item_assets.AssetDefinition def __init__(self, item_asset: item_assets.AssetDefinition): self.asset_defn = item_asset self.properties = item_asset.properties class DatacubeExtensionHooks(ExtensionHooks): schema_uri: str = SCHEMA_URI prev_extension_ids = { "datacube", "https://stac-extensions.github.io/datacube/v1.0.0/schema.json", "https://stac-extensions.github.io/datacube/v2.0.0/schema.json", "https://stac-extensions.github.io/datacube/v2.1.0/schema.json", } stac_object_types = { pystac.STACObjectType.COLLECTION, pystac.STACObjectType.ITEM, } DATACUBE_EXTENSION_HOOKS: ExtensionHooks = DatacubeExtensionHooks() pystac-1.9.0/pystac/extensions/eo.py000066400000000000000000000610731451576074700175320ustar00rootroot00000000000000"""Implements the :stac-ext:`Electro-Optical Extension `.""" from __future__ import annotations import warnings from collections.abc import Iterable from typing import ( Any, Generic, Literal, TypeVar, Union, cast, ) import pystac from pystac.extensions import item_assets, projection, view from pystac.extensions.base import ( ExtensionManagementMixin, PropertiesExtension, SummariesExtension, ) from pystac.extensions.hooks import ExtensionHooks from pystac.serialization.identify import STACJSONDescription, STACVersionID from pystac.summaries import RangeSummary from pystac.utils import get_required, map_opt T = TypeVar("T", pystac.Item, pystac.Asset, item_assets.AssetDefinition) SCHEMA_URI: str = "https://stac-extensions.github.io/eo/v1.1.0/schema.json" SCHEMA_URIS: list[str] = [ "https://stac-extensions.github.io/eo/v1.0.0/schema.json", SCHEMA_URI, ] PREFIX: str = "eo:" # Field names BANDS_PROP: str = PREFIX + "bands" CLOUD_COVER_PROP: str = PREFIX + "cloud_cover" SNOW_COVER_PROP: str = PREFIX + "snow_cover" def validated_percentage(v: float | None) -> float | None: if v is not None and not isinstance(v, (float, int)) or isinstance(v, bool): raise ValueError(f"Invalid percentage: {v} must be number") if v is not None and not 0 <= v <= 100: raise ValueError(f"Invalid percentage: {v} must be between 0 and 100") return v class Band: """Represents Band information attached to an Item that implements the eo extension. Use :meth:`Band.create` to create a new Band. """ properties: dict[str, Any] def __init__(self, properties: dict[str, Any]) -> None: self.properties = properties def apply( self, name: str, common_name: str | None = None, description: str | None = None, center_wavelength: float | None = None, full_width_half_max: float | None = None, solar_illumination: float | None = None, ) -> None: """ Sets the properties for this Band. Args: name : The name of the band (e.g., "B01", "B02", "B1", "B5", "QA"). common_name : The name commonly used to refer to the band to make it easier to search for bands across instruments. See the :stac-ext:`list of accepted common names `. description : Description to fully explain the band. center_wavelength : The center wavelength of the band, in micrometers (μm). full_width_half_max : Full width at half maximum (FWHM). The width of the band, as measured at half the maximum transmission, in micrometers (μm). solar_illumination: The solar illumination of the band, as measured at half the maximum transmission, in W/m2/micrometers. """ self.name = name self.common_name = common_name self.description = description self.center_wavelength = center_wavelength self.full_width_half_max = full_width_half_max self.solar_illumination = solar_illumination @classmethod def create( cls, name: str, common_name: str | None = None, description: str | None = None, center_wavelength: float | None = None, full_width_half_max: float | None = None, solar_illumination: float | None = None, ) -> Band: """ Creates a new band. Args: name : The name of the band (e.g., "B01", "B02", "B1", "B5", "QA"). common_name : The name commonly used to refer to the band to make it easier to search for bands across instruments. See the :stac-ext:`list of accepted common names `. description : Description to fully explain the band. center_wavelength : The center wavelength of the band, in micrometers (μm). full_width_half_max : Full width at half maximum (FWHM). The width of the band, as measured at half the maximum transmission, in micrometers (μm). solar_illumination: The solar illumination of the band, as measured at half the maximum transmission, in W/m2/micrometers. """ b = cls({}) b.apply( name=name, common_name=common_name, description=description, center_wavelength=center_wavelength, full_width_half_max=full_width_half_max, solar_illumination=solar_illumination, ) return b @property def name(self) -> str: """Get or sets the name of the band (e.g., "B01", "B02", "B1", "B5", "QA"). Returns: str """ return get_required(self.properties.get("name"), self, "name") @name.setter def name(self, v: str) -> None: self.properties["name"] = v @property def common_name(self) -> str | None: """Get or sets the name commonly used to refer to the band to make it easier to search for bands across instruments. See the :stac-ext:`list of accepted common names `. Returns: Optional[str] """ return self.properties.get("common_name") @common_name.setter def common_name(self, v: str | None) -> None: if v is not None: self.properties["common_name"] = v else: self.properties.pop("common_name", None) @property def description(self) -> str | None: """Get or sets the description to fully explain the band. CommonMark 0.29 syntax MAY be used for rich text representation. Returns: str """ return self.properties.get("description") @description.setter def description(self, v: str | None) -> None: if v is not None: self.properties["description"] = v else: self.properties.pop("description", None) @property def center_wavelength(self) -> float | None: """Get or sets the center wavelength of the band, in micrometers (μm). Returns: float """ return self.properties.get("center_wavelength") @center_wavelength.setter def center_wavelength(self, v: float | None) -> None: if v is not None: self.properties["center_wavelength"] = v else: self.properties.pop("center_wavelength", None) @property def full_width_half_max(self) -> float | None: """Get or sets the full width at half maximum (FWHM). The width of the band, as measured at half the maximum transmission, in micrometers (μm). Returns: [float] """ return self.properties.get("full_width_half_max") @full_width_half_max.setter def full_width_half_max(self, v: float | None) -> None: if v is not None: self.properties["full_width_half_max"] = v else: self.properties.pop("full_width_half_max", None) @property def solar_illumination(self) -> float | None: """Get or sets the solar illumination of the band, as measured at half the maximum transmission, in W/m2/micrometers. Returns: [float] """ return self.properties.get("solar_illumination") @solar_illumination.setter def solar_illumination(self, v: float | None) -> None: if v is not None: self.properties["solar_illumination"] = v else: self.properties.pop("solar_illumination", None) def __repr__(self) -> str: return f"" def to_dict(self) -> dict[str, Any]: """Returns this band as a dictionary. Returns: dict: The serialization of this Band. """ return self.properties @staticmethod def band_range(common_name: str) -> tuple[float, float] | None: """Gets the band range for a common band name. Args: common_name : The common band name. Must be one of the :stac-ext:`list of accepted common names `. Returns: Tuple[float, float] or None: The band range for this name as (min, max), or None if this is not a recognized common name. """ name_to_range = { "coastal": (0.40, 0.45), "blue": (0.45, 0.50), "green": (0.50, 0.60), "red": (0.60, 0.70), "yellow": (0.58, 0.62), "pan": (0.50, 0.70), "rededge": (0.70, 0.75), "nir": (0.75, 1.00), "nir08": (0.75, 0.90), "nir09": (0.85, 1.05), "cirrus": (1.35, 1.40), "swir16": (1.55, 1.75), "swir22": (2.10, 2.30), "lwir": (10.5, 12.5), "lwir11": (10.5, 11.5), "lwir12": (11.5, 12.5), } return name_to_range.get(common_name) @staticmethod def band_description(common_name: str) -> str | None: """Returns a description of the band for one with a common name. Args: common_name : The common band name. Must be one of the :stac-ext:`list of accepted common names `. Returns: str or None: If a recognized common name, returns a description including the band range. Otherwise returns None. """ r = Band.band_range(common_name) if r is not None: return f"Common name: {common_name}, Range: {r[0]} to {r[1]}" return None class EOExtension( Generic[T], PropertiesExtension, ExtensionManagementMixin[Union[pystac.Item, pystac.Collection]], ): """An abstract class that can be used to extend the properties of an :class:`~pystac.Item` or :class:`~pystac.Asset` with properties from the :stac-ext:`Electro-Optical Extension `. This class is generic over the type of STAC Object to be extended (e.g. :class:`~pystac.Item`, :class:`~pystac.Asset`). To create a concrete instance of :class:`EOExtension`, use the :meth:`EOExtension.ext` method. For example: .. code-block:: python >>> item: pystac.Item = ... >>> eo_ext = EOExtension.ext(item) """ name: Literal["eo"] = "eo" def apply( self, bands: list[Band] | None = None, cloud_cover: float | None = None, snow_cover: float | None = None, ) -> None: """Applies Electro-Optical Extension properties to the extended :class:`~pystac.Item` or :class:`~pystac.Asset`. Args: bands : A list of available bands where each item is a :class:`~Band` object. If given, requires at least one band. cloud_cover : The estimate of cloud cover as a percentage (0-100) of the entire scene. If not available the field should not be provided. snow_cover : The estimate of snow cover as a percentage (0-100) of the entire scene. If not available the field should not be provided. """ self.bands = bands self.cloud_cover = validated_percentage(cloud_cover) self.snow_cover = validated_percentage(snow_cover) @property def bands(self) -> list[Band] | None: """Gets or sets a list of available bands where each item is a :class:`~Band` object (or ``None`` if no bands have been set). If not available the field should not be provided. """ return self._get_bands() @bands.setter def bands(self, v: list[Band] | None) -> None: self._set_property( BANDS_PROP, map_opt(lambda bands: [b.to_dict() for b in bands], v) ) def _get_bands(self) -> list[Band] | None: return map_opt( lambda bands: [Band(b) for b in bands], self._get_property(BANDS_PROP, list[dict[str, Any]]), ) @property def cloud_cover(self) -> float | None: """Get or sets the estimate of cloud cover as a percentage (0-100) of the entire scene. If not available the field should not be provided. Returns: float or None """ return self._get_property(CLOUD_COVER_PROP, float) @cloud_cover.setter def cloud_cover(self, v: float | None) -> None: self._set_property(CLOUD_COVER_PROP, validated_percentage(v), pop_if_none=True) @property def snow_cover(self) -> float | None: """Get or sets the estimate of snow cover as a percentage (0-100) of the entire scene. If not available the field should not be provided. Returns: float or None """ return self._get_property(SNOW_COVER_PROP, float) @snow_cover.setter def snow_cover(self, v: float | None) -> None: self._set_property(SNOW_COVER_PROP, validated_percentage(v), pop_if_none=True) @classmethod def get_schema_uri(cls) -> str: return SCHEMA_URI @classmethod def get_schema_uris(cls) -> list[str]: warnings.warn( "get_schema_uris is deprecated and will be removed in v2", DeprecationWarning, ) return SCHEMA_URIS @classmethod def ext(cls, obj: T, add_if_missing: bool = False) -> EOExtension[T]: """Extends the given STAC Object with properties from the :stac-ext:`Electro-Optical Extension `. This extension can be applied to instances of :class:`~pystac.Item` or :class:`~pystac.Asset`. Raises: pystac.ExtensionTypeError : If an invalid object type is passed. """ if isinstance(obj, pystac.Item): cls.ensure_has_extension(obj, add_if_missing) return cast(EOExtension[T], ItemEOExtension(obj)) elif isinstance(obj, pystac.Asset): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(EOExtension[T], AssetEOExtension(obj)) elif isinstance(obj, item_assets.AssetDefinition): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(EOExtension[T], ItemAssetsEOExtension(obj)) else: raise pystac.ExtensionTypeError(cls._ext_error_message(obj)) @classmethod def summaries( cls, obj: pystac.Collection, add_if_missing: bool = False ) -> SummariesEOExtension: """Returns the extended summaries object for the given collection.""" cls.ensure_has_extension(obj, add_if_missing) return SummariesEOExtension(obj) class ItemEOExtension(EOExtension[pystac.Item]): """A concrete implementation of :class:`EOExtension` on an :class:`~pystac.Item` that extends the properties of the Item to include properties defined in the :stac-ext:`Electro-Optical Extension `. This class should generally not be instantiated directly. Instead, call :meth:`EOExtension.ext` on an :class:`~pystac.Item` to extend it. """ item: pystac.Item """The :class:`~pystac.Item` being extended.""" properties: dict[str, Any] """The :class:`~pystac.Item` properties, including extension properties.""" def __init__(self, item: pystac.Item): self.item = item self.properties = item.properties def _get_bands(self) -> list[Band] | None: """Get or sets a list of :class:`~pystac.Band` objects that represent the available bands. """ bands = self._get_property(BANDS_PROP, list[dict[str, Any]]) # get assets with eo:bands even if not in item if bands is None: asset_bands: list[dict[str, Any]] = [] for _, value in self.item.get_assets().items(): if BANDS_PROP in value.extra_fields: asset_bands.extend( cast(list[dict[str, Any]], value.extra_fields.get(BANDS_PROP)) ) if any(asset_bands): bands = asset_bands if bands is not None: return [Band(b) for b in bands] return None def get_assets( self, name: str | None = None, common_name: str | None = None, ) -> dict[str, pystac.Asset]: """Get the item's assets where eo:bands are defined. Args: name: If set, filter the assets such that only those with a matching ``eo:band.name`` are returned. common_name: If set, filter the assets such that only those with a matching ``eo:band.common_name`` are returned. Returns: Dict[str, Asset]: A dictionary of assets that match ``name`` and/or ``common_name`` if set or else all of this item's assets were eo:bands are defined. """ kwargs = {"name": name, "common_name": common_name} return { key: asset for key, asset in self.item.get_assets().items() if BANDS_PROP in asset.extra_fields and all( v is None or any(v == b.get(k) for b in asset.extra_fields[BANDS_PROP]) for k, v in kwargs.items() ) } def __repr__(self) -> str: return f"" class AssetEOExtension(EOExtension[pystac.Asset]): """A concrete implementation of :class:`EOExtension` on an :class:`~pystac.Asset` that extends the Asset fields to include properties defined in the :stac-ext:`Electro-Optical Extension `. This class should generally not be instantiated directly. Instead, call :meth:`EOExtension.ext` on an :class:`~pystac.Asset` to extend it. """ asset_href: str """The ``href`` value of the :class:`~pystac.Asset` being extended.""" properties: dict[str, Any] """The :class:`~pystac.Asset` fields, including extension properties.""" additional_read_properties: Iterable[dict[str, Any]] | None = None """If present, this will be a list containing 1 dictionary representing the properties of the owning :class:`~pystac.Item`.""" def _get_bands(self) -> list[Band] | None: if BANDS_PROP not in self.properties: return None return list( map( lambda band: Band(band), cast(list[dict[str, Any]], self.properties.get(BANDS_PROP)), ) ) def __init__(self, asset: pystac.Asset): self.asset_href = asset.href self.properties = asset.extra_fields if asset.owner and isinstance(asset.owner, pystac.Item): self.additional_read_properties = [asset.owner.properties] def __repr__(self) -> str: return f"" class ItemAssetsEOExtension(EOExtension[item_assets.AssetDefinition]): properties: dict[str, Any] asset_defn: item_assets.AssetDefinition def _get_bands(self) -> list[Band] | None: if BANDS_PROP not in self.properties: return None return list( map( lambda band: Band(band), cast(list[dict[str, Any]], self.properties.get(BANDS_PROP)), ) ) def __init__(self, item_asset: item_assets.AssetDefinition): self.asset_defn = item_asset self.properties = item_asset.properties class SummariesEOExtension(SummariesExtension): """A concrete implementation of :class:`~SummariesExtension` that extends the ``summaries`` field of a :class:`~pystac.Collection` to include properties defined in the :stac-ext:`Electro-Optical Extension `. """ @property def bands(self) -> list[Band] | None: """Get or sets the summary of :attr:`EOExtension.bands` values for this Collection. """ return map_opt( lambda bands: [Band(b) for b in bands], self.summaries.get_list(BANDS_PROP), ) @bands.setter def bands(self, v: list[Band] | None) -> None: self._set_summary(BANDS_PROP, map_opt(lambda x: [b.to_dict() for b in x], v)) @property def cloud_cover(self) -> RangeSummary[float] | None: """Get or sets the summary of :attr:`EOExtension.cloud_cover` values for this Collection. """ return self.summaries.get_range(CLOUD_COVER_PROP) @cloud_cover.setter def cloud_cover(self, v: RangeSummary[float] | None) -> None: self._set_summary(CLOUD_COVER_PROP, v) @property def snow_cover(self) -> RangeSummary[float] | None: """Get or sets the summary of :attr:`EOExtension.snow_cover` values for this Collection. """ return self.summaries.get_range(SNOW_COVER_PROP) @snow_cover.setter def snow_cover(self, v: RangeSummary[float] | None) -> None: self._set_summary(SNOW_COVER_PROP, v) class EOExtensionHooks(ExtensionHooks): schema_uri: str = SCHEMA_URI prev_extension_ids = { "eo", *[uri for uri in SCHEMA_URIS if uri != SCHEMA_URI], } stac_object_types = {pystac.STACObjectType.ITEM} def migrate( self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription ) -> None: if version < "0.9": # Some eo fields became common_metadata if ( "eo:platform" in obj["properties"] and "platform" not in obj["properties"] ): obj["properties"]["platform"] = obj["properties"]["eo:platform"] del obj["properties"]["eo:platform"] if ( "eo:instrument" in obj["properties"] and "instruments" not in obj["properties"] ): obj["properties"]["instruments"] = [obj["properties"]["eo:instrument"]] del obj["properties"]["eo:instrument"] if ( "eo:constellation" in obj["properties"] and "constellation" not in obj["properties"] ): obj["properties"]["constellation"] = obj["properties"][ "eo:constellation" ] del obj["properties"]["eo:constellation"] # Some eo fields became view extension fields eo_to_view_fields = [ "off_nadir", "azimuth", "incidence_angle", "sun_azimuth", "sun_elevation", ] for field in eo_to_view_fields: if f"eo:{field}" in obj["properties"]: if "stac_extensions" not in obj: obj["stac_extensions"] = [] if view.SCHEMA_URI not in obj["stac_extensions"]: obj["stac_extensions"].append(view.SCHEMA_URI) if f"view:{field}" not in obj["properties"]: obj["properties"][f"view:{field}"] = obj["properties"][ f"eo:{field}" ] del obj["properties"][f"eo:{field}"] # eo:epsg became proj:epsg eo_epsg = PREFIX + "epsg" proj_epsg = projection.PREFIX + "epsg" if eo_epsg in obj["properties"] and proj_epsg not in obj["properties"]: obj["properties"][proj_epsg] = obj["properties"].pop(eo_epsg) obj["stac_extensions"] = obj.get("stac_extensions", []) if ( projection.ProjectionExtension.get_schema_uri() not in obj["stac_extensions"] ): obj["stac_extensions"].append( projection.ProjectionExtension.get_schema_uri() ) if not any(prop.startswith(PREFIX) for prop in obj["properties"]): obj["stac_extensions"].remove(EOExtension.get_schema_uri()) if version < "1.0.0-beta.1" and info.object_type == pystac.STACObjectType.ITEM: # gsd moved from eo to common metadata if "eo:gsd" in obj["properties"]: obj["properties"]["gsd"] = obj["properties"]["eo:gsd"] del obj["properties"]["eo:gsd"] # The way bands were declared in assets changed. # In 1.0.0-beta.1 they are inlined into assets as # opposed to having indices back into a property-level array. if "eo:bands" in obj["properties"]: bands = obj["properties"]["eo:bands"] for asset in obj["assets"].values(): if "eo:bands" in asset: new_bands: list[dict[str, Any]] = [] for band_index in asset["eo:bands"]: new_bands.append(bands[band_index]) asset["eo:bands"] = new_bands super().migrate(obj, version, info) EO_EXTENSION_HOOKS: ExtensionHooks = EOExtensionHooks() pystac-1.9.0/pystac/extensions/ext.py000066400000000000000000000226071451576074700177270ustar00rootroot00000000000000from dataclasses import dataclass from typing import Any, Generic, Literal, TypeVar, cast from pystac import Asset, Catalog, Collection, Item, Link, STACError from pystac.extensions.classification import ClassificationExtension from pystac.extensions.datacube import DatacubeExtension from pystac.extensions.eo import EOExtension from pystac.extensions.file import FileExtension from pystac.extensions.grid import GridExtension from pystac.extensions.item_assets import AssetDefinition, ItemAssetsExtension from pystac.extensions.mgrs import MgrsExtension from pystac.extensions.pointcloud import PointcloudExtension from pystac.extensions.projection import ProjectionExtension from pystac.extensions.raster import RasterExtension from pystac.extensions.sar import SarExtension from pystac.extensions.sat import SatExtension from pystac.extensions.scientific import ScientificExtension from pystac.extensions.storage import StorageExtension from pystac.extensions.table import TableExtension from pystac.extensions.timestamps import TimestampsExtension from pystac.extensions.version import BaseVersionExtension, VersionExtension from pystac.extensions.view import ViewExtension from pystac.extensions.xarray_assets import XarrayAssetsExtension T = TypeVar("T", Asset, AssetDefinition, Link) U = TypeVar("U", Asset, AssetDefinition) EXTENSION_NAMES = Literal[ "classification", "cube", "eo", "file", "grid", "item_assets", "mgrs", "pc", "proj", "raster", "sar", "sat", "sci", "storage", "table", "timestamps", "version", "view", "xarray", ] EXTENSION_NAME_MAPPING: dict[EXTENSION_NAMES, Any] = { ClassificationExtension.name: ClassificationExtension, DatacubeExtension.name: DatacubeExtension, EOExtension.name: EOExtension, FileExtension.name: FileExtension, GridExtension.name: GridExtension, ItemAssetsExtension.name: ItemAssetsExtension, MgrsExtension.name: MgrsExtension, PointcloudExtension.name: PointcloudExtension, ProjectionExtension.name: ProjectionExtension, RasterExtension.name: RasterExtension, SarExtension.name: SarExtension, SatExtension.name: SatExtension, ScientificExtension.name: ScientificExtension, StorageExtension.name: StorageExtension, TableExtension.name: TableExtension, TimestampsExtension.name: TimestampsExtension, VersionExtension.name: VersionExtension, ViewExtension.name: ViewExtension, XarrayAssetsExtension.name: XarrayAssetsExtension, } def _get_class_by_name(name: str) -> Any: try: return EXTENSION_NAME_MAPPING[cast(EXTENSION_NAMES, name)] except KeyError as e: raise KeyError( f"Extension '{name}' is not a valid extension. " f"Options are {list(EXTENSION_NAME_MAPPING)}" ) from e @dataclass class CatalogExt: stac_object: Catalog def has(self, name: EXTENSION_NAMES) -> bool: return cast(bool, _get_class_by_name(name).has_extension(self.stac_object)) def add(self, name: EXTENSION_NAMES) -> None: _get_class_by_name(name).add_to(self.stac_object) def remove(self, name: EXTENSION_NAMES) -> None: _get_class_by_name(name).remove_from(self.stac_object) @property def version(self) -> VersionExtension[Catalog]: return VersionExtension.ext(self.stac_object) @dataclass class CollectionExt(CatalogExt): stac_object: Collection @property def cube(self) -> DatacubeExtension[Collection]: return DatacubeExtension.ext(self.stac_object) @property def item_assets(self) -> dict[str, AssetDefinition]: return ItemAssetsExtension.ext(self.stac_object).item_assets @property def sci(self) -> ScientificExtension[Collection]: return ScientificExtension.ext(self.stac_object) @property def table(self) -> TableExtension[Collection]: return TableExtension.ext(self.stac_object) @property def xarray(self) -> XarrayAssetsExtension[Collection]: return XarrayAssetsExtension.ext(self.stac_object) @dataclass class ItemExt: stac_object: Item def has(self, name: EXTENSION_NAMES) -> bool: return cast(bool, _get_class_by_name(name).has_extension(self.stac_object)) def add(self, name: EXTENSION_NAMES) -> None: _get_class_by_name(name).add_to(self.stac_object) def remove(self, name: EXTENSION_NAMES) -> None: _get_class_by_name(name).remove_from(self.stac_object) @property def classification(self) -> ClassificationExtension[Item]: return ClassificationExtension.ext(self.stac_object) @property def cube(self) -> DatacubeExtension[Item]: return DatacubeExtension.ext(self.stac_object) @property def eo(self) -> EOExtension[Item]: return EOExtension.ext(self.stac_object) @property def grid(self) -> GridExtension: return GridExtension.ext(self.stac_object) @property def mgrs(self) -> MgrsExtension: return MgrsExtension.ext(self.stac_object) @property def pc(self) -> PointcloudExtension[Item]: return PointcloudExtension.ext(self.stac_object) @property def proj(self) -> ProjectionExtension[Item]: return ProjectionExtension.ext(self.stac_object) @property def sar(self) -> SarExtension[Item]: return SarExtension.ext(self.stac_object) @property def sat(self) -> SatExtension[Item]: return SatExtension.ext(self.stac_object) @property def storage(self) -> StorageExtension[Item]: return StorageExtension.ext(self.stac_object) @property def table(self) -> TableExtension[Item]: return TableExtension.ext(self.stac_object) @property def timestamps(self) -> TimestampsExtension[Item]: return TimestampsExtension.ext(self.stac_object) @property def version(self) -> VersionExtension[Item]: return VersionExtension.ext(self.stac_object) @property def view(self) -> ViewExtension[Item]: return ViewExtension.ext(self.stac_object) @property def xarray(self) -> XarrayAssetsExtension[Item]: return XarrayAssetsExtension.ext(self.stac_object) class _AssetsExt(Generic[T]): stac_object: T def has(self, name: EXTENSION_NAMES) -> bool: if self.stac_object.owner is None: raise STACError( f"Attempted to add extension='{name}' for an object with no owner. " "Use `.set_owner` and then try to add the extension again." ) else: return cast( bool, _get_class_by_name(name).has_extension(self.stac_object.owner) ) def add(self, name: EXTENSION_NAMES) -> None: if self.stac_object.owner is None: raise STACError( f"Attempted to add extension='{name}' for an object with no owner. " "Use `.set_owner` and then try to add the extension again." ) else: _get_class_by_name(name).add_to(self.stac_object.owner) def remove(self, name: EXTENSION_NAMES) -> None: if self.stac_object.owner is None: raise STACError( f"Attempted to remove extension='{name}' for an object with no owner. " "Use `.set_owner` and then try to remove the extension again." ) else: _get_class_by_name(name).remove_from(self.stac_object.owner) class _AssetExt(_AssetsExt[U]): stac_object: U @property def classification(self) -> ClassificationExtension[U]: return ClassificationExtension.ext(self.stac_object) @property def cube(self) -> DatacubeExtension[U]: return DatacubeExtension.ext(self.stac_object) @property def eo(self) -> EOExtension[U]: return EOExtension.ext(self.stac_object) @property def pc(self) -> PointcloudExtension[U]: return PointcloudExtension.ext(self.stac_object) @property def proj(self) -> ProjectionExtension[U]: return ProjectionExtension.ext(self.stac_object) @property def raster(self) -> RasterExtension[U]: return RasterExtension.ext(self.stac_object) @property def sar(self) -> SarExtension[U]: return SarExtension.ext(self.stac_object) @property def sat(self) -> SatExtension[U]: return SatExtension.ext(self.stac_object) @property def storage(self) -> StorageExtension[U]: return StorageExtension.ext(self.stac_object) @property def table(self) -> TableExtension[U]: return TableExtension.ext(self.stac_object) @property def version(self) -> BaseVersionExtension[U]: return BaseVersionExtension.ext(self.stac_object) @property def view(self) -> ViewExtension[U]: return ViewExtension.ext(self.stac_object) @dataclass class AssetExt(_AssetExt[Asset]): stac_object: Asset @property def file(self) -> FileExtension[Asset]: return FileExtension.ext(self.stac_object) @property def timestamps(self) -> TimestampsExtension[Asset]: return TimestampsExtension.ext(self.stac_object) @property def xarray(self) -> XarrayAssetsExtension[Asset]: return XarrayAssetsExtension.ext(self.stac_object) @dataclass class ItemAssetExt(_AssetExt[AssetDefinition]): stac_object: AssetDefinition @dataclass class LinkExt(_AssetsExt[Link]): stac_object: Link @property def file(self) -> FileExtension[Link]: return FileExtension.ext(self.stac_object) pystac-1.9.0/pystac/extensions/file.py000066400000000000000000000330321451576074700200400ustar00rootroot00000000000000"""Implements the :stac-ext:`File Info Extension `.""" from __future__ import annotations import warnings from collections.abc import Iterable from typing import Any, Generic, Literal, TypeVar, Union, cast from pystac import ( Asset, Catalog, Collection, ExtensionTypeError, Item, Link, STACObjectType, ) from pystac.extensions.base import ExtensionManagementMixin, PropertiesExtension from pystac.extensions.hooks import ExtensionHooks from pystac.serialization.identify import ( OldExtensionShortIDs, STACJSONDescription, STACVersionID, ) from pystac.utils import StringEnum, get_required, map_opt T = TypeVar("T", Asset, Link) SCHEMA_URI = "https://stac-extensions.github.io/file/v2.1.0/schema.json" PREFIX = "file:" BYTE_ORDER_PROP = PREFIX + "byte_order" CHECKSUM_PROP = PREFIX + "checksum" HEADER_SIZE_PROP = PREFIX + "header_size" SIZE_PROP = PREFIX + "size" VALUES_PROP = PREFIX + "values" LOCAL_PATH_PROP = PREFIX + "local_path" class ByteOrder(StringEnum): """List of allows values for the ``"file:byte_order"`` field defined by the :stac-ext:`File Info Extension `.""" LITTLE_ENDIAN = "little-endian" BIG_ENDIAN = "big-endian" class MappingObject: """Represents a value map used by assets that are used as classification layers, and give details about the values in the asset and their meanings.""" properties: dict[str, Any] def __init__(self, properties: dict[str, Any]) -> None: self.properties = properties def apply(self, values: list[Any], summary: str) -> None: """Sets the properties for this :class:`~MappingObject` instance. Args: values : The value(s) in the file. At least one array element is required. summary : A short description of the value(s). """ self.values = values self.summary = summary @classmethod def create(cls, values: list[Any], summary: str) -> MappingObject: """Creates a new :class:`~MappingObject` instance. Args: values : The value(s) in the file. At least one array element is required. summary : A short description of the value(s). """ m = cls({}) m.apply(values=values, summary=summary) return m @property def values(self) -> list[Any]: """Gets or sets the list of value(s) in the file. At least one array element is required.""" return get_required(self.properties.get("values"), self, "values") @values.setter def values(self, v: list[Any]) -> None: self.properties["values"] = v @property def summary(self) -> str: """Gets or sets the short description of the value(s).""" return get_required(self.properties.get("summary"), self, "summary") @summary.setter def summary(self, v: str) -> None: self.properties["summary"] = v @classmethod def from_dict(cls, d: dict[str, Any]) -> MappingObject: return cls.create(**d) def to_dict(self) -> dict[str, Any]: return self.properties class FileExtension( Generic[T], PropertiesExtension, ExtensionManagementMixin[Union[Catalog, Collection, Item]], ): """A class that can be used to extend the properties of an :class:`~pystac.Asset` or :class:`~pystac.Link` with properties from the :stac-ext:`File Info Extension `. To create an instance of :class:`FileExtension`, use the :meth:`FileExtension.ext` method. For example: .. code-block:: python >>> asset: pystac.Asset = ... >>> file_ext = FileExtension.ext(asset) """ name: Literal["file"] = "file" def apply( self, byte_order: ByteOrder | None = None, checksum: str | None = None, header_size: int | None = None, size: int | None = None, values: list[MappingObject] | None = None, local_path: str | None = None, ) -> None: """Applies file extension properties to the extended Item. Args: byte_order : Optional byte order of integer values in the file. One of ``"big-endian"`` or ``"little-endian"``. checksum : Optional multihash for the corresponding file, encoded as hexadecimal (base 16) string with lowercase letters. header_size : Optional header size of the file, in bytes. size : Optional size of the file, in bytes. values : Optional list of :class:`~MappingObject` instances that lists the values that are in the file and describe their meaning. See the :stac-ext:`Mapping Object ` docs for an example. If given, at least one array element is required. """ self.byte_order = byte_order self.checksum = checksum self.header_size = header_size self.size = size self.values = values self.local_path = local_path @property def byte_order(self) -> ByteOrder | None: """Gets or sets the byte order of integer values in the file. One of big-endian or little-endian.""" return self._get_property(BYTE_ORDER_PROP, ByteOrder) @byte_order.setter def byte_order(self, v: ByteOrder | None) -> None: self._set_property(BYTE_ORDER_PROP, v) @property def checksum(self) -> str | None: """Get or sets the multihash for the corresponding file, encoded as hexadecimal (base 16) string with lowercase letters.""" return self._get_property(CHECKSUM_PROP, str) @checksum.setter def checksum(self, v: str | None) -> None: self._set_property(CHECKSUM_PROP, v) @property def header_size(self) -> int | None: """Get or sets the header size of the file, in bytes.""" return self._get_property(HEADER_SIZE_PROP, int) @header_size.setter def header_size(self, v: int | None) -> None: self._set_property(HEADER_SIZE_PROP, v) @property def local_path(self) -> str | None: """Get or sets a relative local path for the asset/link. The ``file:local_path`` field indicates a **relative** path that can be used by clients for different purposes to organize the files locally. For compatibility reasons the name-separator character in paths **must** be ``/`` and the Windows separator ``\\`` is **not** allowed. """ return self._get_property(LOCAL_PATH_PROP, str) @local_path.setter def local_path(self, v: str | None) -> None: self._set_property(LOCAL_PATH_PROP, v, pop_if_none=True) @property def size(self) -> int | None: """Get or sets the size of the file, in bytes.""" return self._get_property(SIZE_PROP, int) @size.setter def size(self, v: int | None) -> None: self._set_property(SIZE_PROP, v) @property def values(self) -> list[MappingObject] | None: """Get or sets the list of :class:`~MappingObject` instances that lists the values that are in the file and describe their meaning. See the :stac-ext:`Mapping Object ` docs for an example. If given, at least one array element is required.""" return map_opt( lambda values: [ MappingObject.from_dict(mapping_obj) for mapping_obj in values ], self._get_property(VALUES_PROP, list[dict[str, Any]]), ) @values.setter def values(self, v: list[MappingObject] | None) -> None: self._set_property( VALUES_PROP, map_opt( lambda values: [mapping_obj.to_dict() for mapping_obj in values], v ), ) @classmethod def get_schema_uri(cls) -> str: return SCHEMA_URI @classmethod def ext(cls, obj: Asset | Link, add_if_missing: bool = False) -> FileExtension[T]: """Extends the given STAC Object with properties from the :stac-ext:`File Info Extension `. This extension can be applied to instances of :class:`~pystac.Asset` or :class:`~pystac.Link` """ if isinstance(obj, Asset): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(FileExtension[T], AssetFileExtension(obj)) elif isinstance(obj, Link): cls.ensure_owner_has_extension(obj, add_if_missing) return cast(FileExtension[T], LinkFileExtension(obj)) else: raise ExtensionTypeError(cls._ext_error_message(obj)) class AssetFileExtension(FileExtension[Asset]): """A concrete implementation of :class:`FileExtension` on an :class:`~pystac.Asset` that extends the Asset fields to include properties defined in the :stac-ext:`File Info Extension `. This class should generally not be instantiated directly. Instead, call :meth:`FileExtension.ext` on an :class:`~pystac.Asset` to extend it. """ asset_href: str """The ``href`` value of the :class:`~pystac.Asset` being extended.""" properties: dict[str, Any] """The :class:`~pystac.Asset` fields, including extension properties.""" additional_read_properties: Iterable[dict[str, Any]] | None = None """If present, this will be a list containing 1 dictionary representing the properties of the owner.""" def __init__(self, asset: Asset): self.asset_href = asset.href self.properties = asset.extra_fields if asset.owner and hasattr(asset.owner, "properties"): self.additional_read_properties = [asset.owner.properties] def __repr__(self) -> str: return f"" class LinkFileExtension(FileExtension[Link]): """A concrete implementation of :class:`FileExtension` on an :class:`~pystac.Link` that extends the Link fields to include properties defined in the :stac-ext:`File Info Extension `. This class should generally not be instantiated directly. Instead, call :meth:`FileExtension.ext` on an :class:`~pystac.Link` to extend it. """ link_href: str """The ``href`` value of the :class:`~pystac.Link` being extended.""" properties: dict[str, Any] """The :class:`~pystac.Link` fields, including extension properties.""" additional_read_properties: Iterable[dict[str, Any]] | None = None """If present, this will be a list containing 1 dictionary representing the properties of the owner.""" def __init__(self, link: Link): self.link_href = link.href self.properties = link.extra_fields if link.owner and hasattr(link.owner, "properties"): self.additional_read_properties = [link.owner.properties] def __repr__(self) -> str: return f"" class FileExtensionHooks(ExtensionHooks): schema_uri: str = SCHEMA_URI prev_extension_ids = { "file", "https://stac-extensions.github.io/file/v1.0.0/schema.json", "https://stac-extensions.github.io/file/v2.0.0/schema.json", } stac_object_types = { STACObjectType.ITEM, STACObjectType.COLLECTION, STACObjectType.CATALOG, } removed_fields = { "file:bits_per_sample", "file:data_type", "file:nodata", "file:unit", } def migrate( self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription ) -> None: # The checksum field was previously it's own extension. old_checksum: dict[str, str] | None = None if info.version_range.latest_valid_version() < "v1.0.0-rc.2": if OldExtensionShortIDs.CHECKSUM.value in info.extensions: old_item_checksum = obj["properties"].get("checksum:multihash") if old_item_checksum is not None: if old_checksum is None: old_checksum = {} old_checksum["__item__"] = old_item_checksum for asset_key, asset in obj["assets"].items(): old_asset_checksum = asset.get("checksum:multihash") if old_asset_checksum is not None: if old_checksum is None: old_checksum = {} old_checksum[asset_key] = old_asset_checksum try: obj["stac_extensions"].remove(OldExtensionShortIDs.CHECKSUM.value) except ValueError: pass super().migrate(obj, version, info) if old_checksum is not None: if SCHEMA_URI not in obj["stac_extensions"]: obj["stac_extensions"].append(SCHEMA_URI) for key in old_checksum: if key == "__item__": obj["properties"][CHECKSUM_PROP] = old_checksum[key] else: obj["assets"][key][CHECKSUM_PROP] = old_checksum[key] found_fields = {} for asset_key, asset in obj.get("assets", {}).items(): if values := set(asset.keys()).intersection(self.removed_fields): found_fields[asset_key] = values if found_fields: warnings.warn( f"Assets {list(found_fields.keys())} contain fields: " f"{list(set.union(*found_fields.values()))} which " "were removed from the file extension spec in v2.0.0. Please " "consult the release notes " "(https://github.com/stac-extensions/file/releases/tag/v2.0.0) " "for instructions on how to migrate these fields.", UserWarning, ) FILE_EXTENSION_HOOKS: ExtensionHooks = FileExtensionHooks() pystac-1.9.0/pystac/extensions/grid.py000066400000000000000000000072501451576074700200510ustar00rootroot00000000000000"""Implements the :stac-ext:`Grid Extension `.""" from __future__ import annotations import re import warnings from re import Pattern from typing import Any, Literal, 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.1.0/schema.json" SCHEMA_URIS: list[str] = [ "https://stac-extensions.github.io/grid/v1.0.0/schema.json", SCHEMA_URI, ] PREFIX: str = "grid:" # Field names CODE_PROP: str = PREFIX + "code" # required CODE_REGEX: str = r"[A-Z0-9]+-[-_.A-Za-z0-9]+" CODE_PATTERN: Pattern[str] = re.compile(CODE_REGEX) def validated_code(v: str) -> str: if not isinstance(v, str): raise ValueError("Invalid Grid code: must be str") if not CODE_PATTERN.fullmatch(v): raise ValueError( f"Invalid Grid code: {v}" f" does not match the regex {CODE_REGEX}" ) return v class GridExtension( PropertiesExtension, ExtensionManagementMixin[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) """ name: Literal["grid"] = "grid" item: pystac.Item """The :class:`~pystac.Item` being extended.""" properties: dict[str, Any] """The :class:`~pystac.Item` properties, including extension properties.""" def __init__(self, item: pystac.Item): self.item = item self.properties = item.properties def __repr__(self) -> str: return f"" def apply(self, code: str) -> None: """Applies Grid extension properties to the extended Item. Args: code : REQUIRED. The code of the Item's grid location. """ self.code = validated_code(code) @property def code(self) -> str | None: """Get or sets the 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 get_schema_uris(cls) -> list[str]: warnings.warn( "get_schema_uris is deprecated and will be removed in v2", DeprecationWarning, ) return SCHEMA_URIS @classmethod def ext(cls, obj: pystac.Item, add_if_missing: bool = False) -> GridExtension: """Extends the given STAC Object with properties from the :stac-ext:`Grid Extension `. This extension can be applied to instances of :class:`~pystac.Item`. Raises: pystac.ExtensionTypeError : If an invalid object type is passed. """ if isinstance(obj, pystac.Item): cls.ensure_has_extension(obj, add_if_missing) return GridExtension(obj) else: raise pystac.ExtensionTypeError(cls._ext_error_message(obj)) class GridExtensionHooks(ExtensionHooks): schema_uri: str = SCHEMA_URI prev_extension_ids: set[str] = {*[uri for uri in SCHEMA_URIS if uri != SCHEMA_URI]} stac_object_types = {pystac.STACObjectType.ITEM} GRID_EXTENSION_HOOKS: ExtensionHooks = GridExtensionHooks() pystac-1.9.0/pystac/extensions/hooks.py000066400000000000000000000074221451576074700202500ustar00rootroot00000000000000from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Iterable from functools import lru_cache from typing import TYPE_CHECKING, Any import pystac from pystac.serialization.identify import STACJSONDescription, STACVersionID if TYPE_CHECKING: from pystac.stac_object import STACObject class ExtensionHooks(ABC): @property @abstractmethod def schema_uri(self) -> str: """The schema_uri for the current version of this extension""" raise NotImplementedError @property @abstractmethod def prev_extension_ids(self) -> set[str]: """A set of previous extension IDs (schema URIs or old short ids) that should be migrated to the latest schema URI in the 'stac_extensions' property. Override with a class attribute so that the set of previous IDs is only created once. """ raise NotImplementedError @property @abstractmethod def stac_object_types(self) -> set[pystac.STACObjectType]: """A set of STACObjectType for which migration logic will be applied.""" raise NotImplementedError @lru_cache def _get_stac_object_types(self) -> set[str]: """Translation of stac_object_types to strings, cached""" return {x.value for x in self.stac_object_types} def get_object_links(self, obj: STACObject) -> list[str | pystac.RelType] | None: return None def migrate( self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription ) -> None: """Migrate a STAC Object in dict format from a previous version. The base implementation will update the stac_extensions to the latest schema ID. This method will only be called for STAC objects that have been identified as a previous version of STAC. Implementations should directly manipulate the obj dict. Remember to call super() in order to change out the old 'stac_extension' entry with the latest schema URI. """ # Migrate schema versions for prev_id in self.prev_extension_ids: if prev_id in info.extensions: try: i = obj["stac_extensions"].index(prev_id) obj["stac_extensions"][i] = self.schema_uri except ValueError: obj["stac_extensions"].append(self.schema_uri) break class RegisteredExtensionHooks: hooks: dict[str, ExtensionHooks] def __init__(self, hooks: Iterable[ExtensionHooks]): self.hooks = {e.schema_uri: e for e in hooks} def add_extension_hooks(self, hooks: ExtensionHooks) -> None: e_id = hooks.schema_uri if e_id in self.hooks: raise pystac.ExtensionAlreadyExistsError( f"ExtensionDefinition with id '{e_id}' already exists." ) self.hooks[e_id] = hooks def remove_extension_hooks(self, extension_id: str) -> None: if extension_id in self.hooks: del self.hooks[extension_id] def get_extended_object_links(self, obj: STACObject) -> list[str | pystac.RelType]: result: list[str | pystac.RelType] | None = None for ext in obj.stac_extensions: if ext in self.hooks: ext_result = self.hooks[ext].get_object_links(obj) if ext_result is not None: if result is None: result = ext_result else: result.extend(ext_result) return result or [] def migrate( self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription ) -> None: for hooks in self.hooks.values(): if info.object_type in hooks._get_stac_object_types(): hooks.migrate(obj, version, info) pystac-1.9.0/pystac/extensions/item_assets.py000066400000000000000000000236021451576074700214430ustar00rootroot00000000000000"""Implements the :stac-ext:`Item Assets Definition Extension `.""" from __future__ import annotations from copy import deepcopy from typing import TYPE_CHECKING, Any, Literal import pystac from pystac.extensions.base import ExtensionManagementMixin from pystac.extensions.hooks import ExtensionHooks from pystac.serialization.identify import STACJSONDescription, STACVersionID from pystac.utils import get_required if TYPE_CHECKING: from pystac.extensions.ext import ItemAssetExt SCHEMA_URI = "https://stac-extensions.github.io/item-assets/v1.0.0/schema.json" ITEM_ASSETS_PROP = "item_assets" ASSET_TITLE_PROP = "title" ASSET_DESC_PROP = "description" ASSET_TYPE_PROP = "type" ASSET_ROLES_PROP = "roles" class AssetDefinition: """Object that contains details about the datafiles that will be included in member Items for this Collection. See the :stac-ext:`Asset Object ` for details. """ properties: dict[str, Any] owner: pystac.Collection | None def __init__( self, properties: dict[str, Any], owner: pystac.Collection | None = None ) -> None: self.properties = properties self.owner = owner def __eq__(self, o: object) -> bool: if not isinstance(o, AssetDefinition): return NotImplemented return self.to_dict() == o.to_dict() @classmethod def create( cls, title: str | None, description: str | None, media_type: str | None, roles: list[str] | None, extra_fields: dict[str, Any] | None = None, ) -> 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. extra_fields : Additional fields on the asset definition, e.g. from extensions. """ asset_defn = cls({}) asset_defn.apply( title=title, description=description, media_type=media_type, roles=roles, extra_fields=extra_fields, ) return asset_defn def apply( self, title: str | None, description: str | None, media_type: str | None, roles: list[str] | None, extra_fields: dict[str, Any] | None = None, ) -> 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. extra_fields : Additional fields on the asset definition, e.g. from extensions. """ if extra_fields: self.properties.update(extra_fields) self.title = title self.description = description self.media_type = media_type self.roles = roles self.owner = None def set_owner(self, obj: pystac.Collection) -> None: """Sets the owning item of this AssetDefinition. The owning item will be used to resolve relative HREFs of this asset. Args: obj: The Collection that owns this asset. """ self.owner = obj @property def title(self) -> str | None: """Gets or sets the displayed title for clients and users.""" return self.properties.get(ASSET_TITLE_PROP) @title.setter def title(self, v: str | None) -> None: if v is None: self.properties.pop(ASSET_TITLE_PROP, None) else: self.properties[ASSET_TITLE_PROP] = v @property def description(self) -> str | None: """Gets or sets a description of the Asset providing additional details, such as how it was processed or created. `CommonMark 0.29 `__ syntax MAY be used for rich text representation.""" return self.properties.get(ASSET_DESC_PROP) @description.setter def description(self, v: str | None) -> None: if v is None: self.properties.pop(ASSET_DESC_PROP, None) else: self.properties[ASSET_DESC_PROP] = v @property def media_type(self) -> str | None: """Gets or sets the `media type `__ of the asset.""" return self.properties.get(ASSET_TYPE_PROP) @media_type.setter def media_type(self, v: str | None) -> None: if v is None: self.properties.pop(ASSET_TYPE_PROP, None) else: self.properties[ASSET_TYPE_PROP] = v @property def roles(self) -> list[str] | None: """Gets or sets the `semantic roles `__ of the asset, similar to the use of rel in links.""" return self.properties.get(ASSET_ROLES_PROP) @roles.setter def roles(self, v: list[str] | None) -> None: if v is None: self.properties.pop(ASSET_ROLES_PROP, None) else: self.properties[ASSET_ROLES_PROP] = v def to_dict(self) -> dict[str, Any]: """Returns a dictionary representing this ``AssetDefinition``.""" return deepcopy(self.properties) def create_asset(self, href: str) -> pystac.Asset: """Creates a new :class:`~pystac.Asset` instance using the fields from this ``AssetDefinition`` and the given ``href``.""" return pystac.Asset( href=href, title=self.title, description=self.description, media_type=self.media_type, roles=self.roles, extra_fields={ k: v for k, v in self.properties.items() if k not in { ASSET_TITLE_PROP, ASSET_DESC_PROP, ASSET_TYPE_PROP, ASSET_ROLES_PROP, } }, ) @property def ext(self) -> ItemAssetExt: """Accessor for extension classes on this item_asset Example:: collection.ext.item_assets["data"].ext.proj.epsg = 4326 """ from pystac.extensions.ext import ItemAssetExt return ItemAssetExt(stac_object=self) class ItemAssetsExtension(ExtensionManagementMixin[pystac.Collection]): name: Literal["item_assets"] = "item_assets" collection: pystac.Collection def __init__(self, collection: pystac.Collection) -> None: self.collection = collection @property def item_assets(self) -> dict[str, AssetDefinition]: """Gets or sets a dictionary of assets that can be found in member Items. Maps the asset key to an :class:`AssetDefinition` instance.""" result: dict[str, Any] = get_required( self.collection.extra_fields.get(ITEM_ASSETS_PROP), self, ITEM_ASSETS_PROP ) return {k: AssetDefinition(v, self.collection) for k, v in result.items()} @item_assets.setter def item_assets(self, v: dict[str, AssetDefinition]) -> None: self.collection.extra_fields[ITEM_ASSETS_PROP] = { k: asset_def.properties for k, asset_def in v.items() } def __repr__(self) -> str: return f"" @classmethod def get_schema_uri(cls) -> str: return SCHEMA_URI @classmethod def ext( cls, obj: pystac.Collection, add_if_missing: bool = False ) -> ItemAssetsExtension: """Extends the given :class:`~pystac.Collection` with properties from the :stac-ext:`Item Assets Extension `. Raises: pystac.ExtensionTypeError : If an invalid object type is passed. """ if isinstance(obj, pystac.Collection): cls.ensure_has_extension(obj, add_if_missing) return cls(obj) else: raise pystac.ExtensionTypeError(cls._ext_error_message(obj)) class ItemAssetsExtensionHooks(ExtensionHooks): schema_uri: str = SCHEMA_URI prev_extension_ids = {"asset", "item-assets"} stac_object_types = {pystac.STACObjectType.COLLECTION} def migrate( self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription ) -> None: # Handle that the "item-assets" extension had the id of "assets", before # collection assets (since removed) took over the ID of "assets" if version < "1.0.0-beta.1" and "asset" in info.extensions: if "assets" in obj: obj["item_assets"] = obj["assets"] del obj["assets"] super().migrate(obj, version, info) ITEM_ASSETS_EXTENSION_HOOKS: ExtensionHooks = ItemAssetsExtensionHooks() pystac-1.9.0/pystac/extensions/label.py000066400000000000000000000706101451576074700202030ustar00rootroot00000000000000"""Implements the :stac-ext:`Label Extension