From 8bbec405591a155492d334cc4129bc13ac768376 Mon Sep 17 00:00:00 2001
From: Pete Gadomski
Date: Tue, 9 May 2023 12:28:36 -0600
Subject: [PATCH] Remove backend repos (#555)
* feat: remove backend repos
* docs: fix readme
* docs: more doc fixups
* fix, tests: add httpx to dev dependencies
* fix: remove wait-for-it script
* docs, fix: nav
---
.github/dependabot.yml | 12 -
.github/workflows/cicd.yaml | 116 +-
.github/workflows/deploy_mkdocs.yml | 1 -
.github/workflows/packages.yml | 45 -
.pre-commit-config.yaml | 109 +-
CHANGES.md | 6 +
docker/Dockerfile => Dockerfile | 4 +-
docker/Dockerfile.docs => Dockerfile.docs | 15 +-
Makefile | 83 +-
README.md | 123 +-
docker-compose.docs.yml | 2 +-
docker-compose.nginx.yml | 18 -
docker-compose.yml | 118 --
docker/Dockerfile.pgstac | 25 -
docker/Dockerfile.sqlalchemy | 27 -
docker/docker-compose.pgstac.yml | 42 -
docker/docker-compose.sqlalchemy.yml | 48 -
docs/tips-and-tricks.md | 22 +-
mkdocs.yml | 175 +-
nginx.conf | 29 -
pyproject.toml | 2 +-
scripts/ingest_joplin.py | 48 -
scripts/publish | 2 -
scripts/validate | 28 -
scripts/wait-for-it.sh | 186 --
stac_fastapi/api/setup.py | 1 +
stac_fastapi/pgstac/README.md | 66 -
stac_fastapi/pgstac/pytest.ini | 4 -
stac_fastapi/pgstac/setup.cfg | 2 -
stac_fastapi/pgstac/setup.py | 65 -
.../pgstac/stac_fastapi/pgstac/__init__.py | 1 -
.../pgstac/stac_fastapi/pgstac/app.py | 111 --
.../pgstac/stac_fastapi/pgstac/config.py | 56 -
.../pgstac/stac_fastapi/pgstac/core.py | 432 ----
stac_fastapi/pgstac/stac_fastapi/pgstac/db.py | 141 --
.../pgstac/extensions/__init__.py | 6 -
.../stac_fastapi/pgstac/extensions/filter.py | 41 -
.../stac_fastapi/pgstac/extensions/query.py | 48 -
.../stac_fastapi/pgstac/models/__init__.py | 1 -
.../stac_fastapi/pgstac/models/links.py | 250 ---
.../stac_fastapi/pgstac/transactions.py | 135 --
.../pgstac/types/base_item_cache.py | 55 -
.../stac_fastapi/pgstac/types/search.py | 26 -
.../pgstac/stac_fastapi/pgstac/utils.py | 115 --
.../pgstac/stac_fastapi/pgstac/version.py | 2 -
stac_fastapi/pgstac/tests/__init__.py | 0
stac_fastapi/pgstac/tests/api/__init__.py | 0
stac_fastapi/pgstac/tests/api/test_api.py | 624 ------
stac_fastapi/pgstac/tests/clients/__init__.py | 0
.../pgstac/tests/clients/test_postgres.py | 212 --
stac_fastapi/pgstac/tests/conftest.py | 240 ---
.../pgstac/tests/data/joplin/collection.json | 28 -
.../pgstac/tests/data/joplin/index.geojson | 1775 -----------------
.../pgstac/tests/data/test2_collection.json | 271 ---
.../pgstac/tests/data/test2_item.json | 258 ---
.../pgstac/tests/data/test_collection.json | 152 --
stac_fastapi/pgstac/tests/data/test_item.json | 510 -----
.../pgstac/tests/data/test_item2.json | 646 ------
.../pgstac/tests/resources/__init__.py | 0
.../pgstac/tests/resources/test_collection.py | 244 ---
.../tests/resources/test_conformance.py | 76 -
.../pgstac/tests/resources/test_item.py | 1511 --------------
.../pgstac/tests/resources/test_mgmt.py | 9 -
stac_fastapi/sqlalchemy/README.md | 3 -
stac_fastapi/sqlalchemy/alembic.ini | 85 -
stac_fastapi/sqlalchemy/alembic/README | 1 -
stac_fastapi/sqlalchemy/alembic/env.py | 90 -
.../sqlalchemy/alembic/script.py.mako | 24 -
.../versions/131aab4d9e49_create_tables.py | 76 -
.../407037cb1636_add_stac_1_0_0_fields.py | 27 -
...10f2e6_change_item_geometry_column_type.py | 34 -
...bf_make_item_geometry_and_bbox_nullable.py | 46 -
...f_use_timestamptz_rather_than_timestamp.py | 40 -
.../821aa04011e8_change_pri_key_for_item.py | 24 -
stac_fastapi/sqlalchemy/pytest.ini | 3 -
stac_fastapi/sqlalchemy/setup.cfg | 2 -
stac_fastapi/sqlalchemy/setup.py | 62 -
.../stac_fastapi/sqlalchemy/__init__.py | 1 -
.../sqlalchemy/stac_fastapi/sqlalchemy/app.py | 80 -
.../stac_fastapi/sqlalchemy/config.py | 40 -
.../stac_fastapi/sqlalchemy/core.py | 522 -----
.../sqlalchemy/extensions/__init__.py | 5 -
.../sqlalchemy/extensions/query.py | 133 --
.../sqlalchemy/models/__init__.py | 1 -
.../sqlalchemy/models/database.py | 99 -
.../stac_fastapi/sqlalchemy/models/search.py | 23 -
.../stac_fastapi/sqlalchemy/serializers.py | 177 --
.../stac_fastapi/sqlalchemy/session.py | 62 -
.../stac_fastapi/sqlalchemy/tokens.py | 55 -
.../stac_fastapi/sqlalchemy/transactions.py | 201 --
.../stac_fastapi/sqlalchemy/version.py | 2 -
stac_fastapi/sqlalchemy/tests/__init__.py | 0
stac_fastapi/sqlalchemy/tests/api/__init__.py | 0
stac_fastapi/sqlalchemy/tests/api/test_api.py | 529 -----
.../sqlalchemy/tests/clients/__init__.py | 0
.../sqlalchemy/tests/clients/test_postgres.py | 376 ----
stac_fastapi/sqlalchemy/tests/conftest.py | 155 --
.../tests/data/test_collection.json | 167 --
.../sqlalchemy/tests/data/test_item.json | 505 -----
.../tests/data/test_item_geometry_null.json | 169 --
.../tests/data/test_item_multipolygon.json | 454 -----
.../sqlalchemy/tests/features/__init__.py | 0
.../tests/features/test_custom_models.py | 75 -
.../sqlalchemy/tests/resources/__init__.py | 0
.../tests/resources/test_collection.py | 118 --
.../tests/resources/test_conformance.py | 68 -
.../sqlalchemy/tests/resources/test_item.py | 992 ---------
.../sqlalchemy/tests/resources/test_mgmt.py | 9 -
stac_fastapi/testdata/joplin/collection.json | 34 -
stac_fastapi/testdata/joplin/feature.geojson | 59 -
stac_fastapi/testdata/joplin/index.geojson | 1775 -----------------
111 files changed, 196 insertions(+), 16607 deletions(-)
delete mode 100644 .github/workflows/packages.yml
rename docker/Dockerfile => Dockerfile (75%)
rename docker/Dockerfile.docs => Dockerfile.docs (64%)
delete mode 100644 docker-compose.nginx.yml
delete mode 100644 docker-compose.yml
delete mode 100644 docker/Dockerfile.pgstac
delete mode 100644 docker/Dockerfile.sqlalchemy
delete mode 100644 docker/docker-compose.pgstac.yml
delete mode 100644 docker/docker-compose.sqlalchemy.yml
delete mode 100644 nginx.conf
delete mode 100644 scripts/ingest_joplin.py
delete mode 100755 scripts/validate
delete mode 100755 scripts/wait-for-it.sh
delete mode 100644 stac_fastapi/pgstac/README.md
delete mode 100644 stac_fastapi/pgstac/pytest.ini
delete mode 100644 stac_fastapi/pgstac/setup.cfg
delete mode 100644 stac_fastapi/pgstac/setup.py
delete mode 100644 stac_fastapi/pgstac/stac_fastapi/pgstac/__init__.py
delete mode 100644 stac_fastapi/pgstac/stac_fastapi/pgstac/app.py
delete mode 100644 stac_fastapi/pgstac/stac_fastapi/pgstac/config.py
delete mode 100644 stac_fastapi/pgstac/stac_fastapi/pgstac/core.py
delete mode 100644 stac_fastapi/pgstac/stac_fastapi/pgstac/db.py
delete mode 100644 stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/__init__.py
delete mode 100644 stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/filter.py
delete mode 100644 stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/query.py
delete mode 100644 stac_fastapi/pgstac/stac_fastapi/pgstac/models/__init__.py
delete mode 100644 stac_fastapi/pgstac/stac_fastapi/pgstac/models/links.py
delete mode 100644 stac_fastapi/pgstac/stac_fastapi/pgstac/transactions.py
delete mode 100644 stac_fastapi/pgstac/stac_fastapi/pgstac/types/base_item_cache.py
delete mode 100644 stac_fastapi/pgstac/stac_fastapi/pgstac/types/search.py
delete mode 100644 stac_fastapi/pgstac/stac_fastapi/pgstac/utils.py
delete mode 100644 stac_fastapi/pgstac/stac_fastapi/pgstac/version.py
delete mode 100644 stac_fastapi/pgstac/tests/__init__.py
delete mode 100644 stac_fastapi/pgstac/tests/api/__init__.py
delete mode 100644 stac_fastapi/pgstac/tests/api/test_api.py
delete mode 100644 stac_fastapi/pgstac/tests/clients/__init__.py
delete mode 100644 stac_fastapi/pgstac/tests/clients/test_postgres.py
delete mode 100644 stac_fastapi/pgstac/tests/conftest.py
delete mode 100644 stac_fastapi/pgstac/tests/data/joplin/collection.json
delete mode 100644 stac_fastapi/pgstac/tests/data/joplin/index.geojson
delete mode 100644 stac_fastapi/pgstac/tests/data/test2_collection.json
delete mode 100644 stac_fastapi/pgstac/tests/data/test2_item.json
delete mode 100644 stac_fastapi/pgstac/tests/data/test_collection.json
delete mode 100644 stac_fastapi/pgstac/tests/data/test_item.json
delete mode 100644 stac_fastapi/pgstac/tests/data/test_item2.json
delete mode 100644 stac_fastapi/pgstac/tests/resources/__init__.py
delete mode 100644 stac_fastapi/pgstac/tests/resources/test_collection.py
delete mode 100644 stac_fastapi/pgstac/tests/resources/test_conformance.py
delete mode 100644 stac_fastapi/pgstac/tests/resources/test_item.py
delete mode 100644 stac_fastapi/pgstac/tests/resources/test_mgmt.py
delete mode 100644 stac_fastapi/sqlalchemy/README.md
delete mode 100644 stac_fastapi/sqlalchemy/alembic.ini
delete mode 100644 stac_fastapi/sqlalchemy/alembic/README
delete mode 100644 stac_fastapi/sqlalchemy/alembic/env.py
delete mode 100644 stac_fastapi/sqlalchemy/alembic/script.py.mako
delete mode 100644 stac_fastapi/sqlalchemy/alembic/versions/131aab4d9e49_create_tables.py
delete mode 100644 stac_fastapi/sqlalchemy/alembic/versions/407037cb1636_add_stac_1_0_0_fields.py
delete mode 100644 stac_fastapi/sqlalchemy/alembic/versions/5909bd10f2e6_change_item_geometry_column_type.py
delete mode 100644 stac_fastapi/sqlalchemy/alembic/versions/7016c1bf3fbf_make_item_geometry_and_bbox_nullable.py
delete mode 100644 stac_fastapi/sqlalchemy/alembic/versions/77c019af60bf_use_timestamptz_rather_than_timestamp.py
delete mode 100644 stac_fastapi/sqlalchemy/alembic/versions/821aa04011e8_change_pri_key_for_item.py
delete mode 100644 stac_fastapi/sqlalchemy/pytest.ini
delete mode 100644 stac_fastapi/sqlalchemy/setup.cfg
delete mode 100644 stac_fastapi/sqlalchemy/setup.py
delete mode 100644 stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/__init__.py
delete mode 100644 stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/app.py
delete mode 100644 stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/config.py
delete mode 100644 stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/core.py
delete mode 100644 stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/extensions/__init__.py
delete mode 100644 stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/extensions/query.py
delete mode 100644 stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/__init__.py
delete mode 100644 stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/database.py
delete mode 100644 stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/search.py
delete mode 100644 stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/serializers.py
delete mode 100644 stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/session.py
delete mode 100644 stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/tokens.py
delete mode 100644 stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/transactions.py
delete mode 100644 stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/version.py
delete mode 100644 stac_fastapi/sqlalchemy/tests/__init__.py
delete mode 100644 stac_fastapi/sqlalchemy/tests/api/__init__.py
delete mode 100644 stac_fastapi/sqlalchemy/tests/api/test_api.py
delete mode 100644 stac_fastapi/sqlalchemy/tests/clients/__init__.py
delete mode 100644 stac_fastapi/sqlalchemy/tests/clients/test_postgres.py
delete mode 100644 stac_fastapi/sqlalchemy/tests/conftest.py
delete mode 100644 stac_fastapi/sqlalchemy/tests/data/test_collection.json
delete mode 100644 stac_fastapi/sqlalchemy/tests/data/test_item.json
delete mode 100644 stac_fastapi/sqlalchemy/tests/data/test_item_geometry_null.json
delete mode 100644 stac_fastapi/sqlalchemy/tests/data/test_item_multipolygon.json
delete mode 100644 stac_fastapi/sqlalchemy/tests/features/__init__.py
delete mode 100644 stac_fastapi/sqlalchemy/tests/features/test_custom_models.py
delete mode 100644 stac_fastapi/sqlalchemy/tests/resources/__init__.py
delete mode 100644 stac_fastapi/sqlalchemy/tests/resources/test_collection.py
delete mode 100644 stac_fastapi/sqlalchemy/tests/resources/test_conformance.py
delete mode 100644 stac_fastapi/sqlalchemy/tests/resources/test_item.py
delete mode 100644 stac_fastapi/sqlalchemy/tests/resources/test_mgmt.py
delete mode 100644 stac_fastapi/testdata/joplin/collection.json
delete mode 100644 stac_fastapi/testdata/joplin/feature.geojson
delete mode 100644 stac_fastapi/testdata/joplin/index.geojson
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 465c88f31..7ab0cc61d 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -12,10 +12,6 @@ updates:
directory: "/stac_fastapi/api"
schedule:
interval: weekly
- - package-ecosystem: pip
- directory: "/stac_fastapi/api"
- schedule:
- interval: weekly
- package-ecosystem: pip
directory: "/stac_fastapi/types"
schedule:
@@ -24,11 +20,3 @@ updates:
directory: "/stac_fastapi/extensions"
schedule:
interval: weekly
- - package-ecosystem: pip
- directory: "/stac_fastapi/pgstac"
- schedule:
- interval: weekly
- - package-ecosystem: pip
- directory: "/stac_fastapi/sqlalchemy"
- schedule:
- interval: weekly
diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml
index 6f4eaddc9..3c4942f20 100644
--- a/.github/workflows/cicd.yaml
+++ b/.github/workflows/cicd.yaml
@@ -53,10 +53,6 @@ jobs:
python -m pip install pre-commit
pre-commit run --all-files
- - name: Install pipenv
- run: |
- python -m pip install --upgrade pipenv wheel
-
- name: Install types
run: |
pip install ./stac_fastapi/types[dev]
@@ -69,118 +65,10 @@ jobs:
run: |
pip install ./stac_fastapi/extensions[dev]
- - name: Install sqlalchemy stac-fastapi
- run: |
- pip install ./stac_fastapi/sqlalchemy[dev,server]
-
- - name: Install pgstac stac-fastapi
- run: |
- pip install ./stac_fastapi/pgstac[dev,server]
-
- - name: Run migration
- run: |
- cd stac_fastapi/sqlalchemy && alembic upgrade head
- env:
- POSTGRES_USER: username
- POSTGRES_PASS: password
- POSTGRES_DBNAME: postgis
- POSTGRES_HOST: localhost
- POSTGRES_PORT: 5432
-
- - name: Run test suite
- run: |
- cd stac_fastapi/api && pipenv run pytest -svvv
- env:
- ENVIRONMENT: testing
-
- - name: Run test suite
- run: |
- cd stac_fastapi/types && pipenv run pytest -svvv
- env:
- ENVIRONMENT: testing
-
- - name: Run test suite
- run: |
- cd stac_fastapi/sqlalchemy && pipenv run pytest -svvv
- env:
- ENVIRONMENT: testing
- POSTGRES_USER: username
- POSTGRES_PASS: password
- POSTGRES_DBNAME: postgis
- POSTGRES_HOST_READER: localhost
- POSTGRES_HOST_WRITER: localhost
- POSTGRES_PORT: 5432
-
- - name: Run test suite
- run: |
- cd stac_fastapi/pgstac && pipenv run pytest -svvv
+ - name: Test
+ run: pytest -svvv
env:
ENVIRONMENT: testing
- POSTGRES_USER: username
- POSTGRES_PASS: password
- POSTGRES_DBNAME: postgis
- POSTGRES_HOST_READER: localhost
- POSTGRES_HOST_WRITER: localhost
- POSTGRES_PORT: 5432
-
- validate:
- runs-on: ubuntu-latest
- strategy:
- fail-fast: false
- matrix:
- backend: ["sqlalchemy", "pgstac"]
- services:
- pgstac:
- image: ghcr.io/stac-utils/pgstac:v0.7.1
- env:
- POSTGRES_USER: username
- POSTGRES_PASSWORD: password
- POSTGRES_DB: postgis
- PGUSER: username
- PGPASSWORD: password
- PGDATABASE: postgis
- options: >-
- --health-cmd pg_isready
- --health-interval 10s
- --health-timeout 5s
- --health-retries 5
- --log-driver none
- ports:
- - 5432:5432
- steps:
- - name: Check out repository code
- uses: actions/checkout@v3
- - name: Setup Python
- uses: actions/setup-python@v3
- with:
- python-version: "3.10"
- cache: pip
- cache-dependency-path: stac_fastapi/pgstac/setup.cfg
- - name: Install stac-fastapi and stac-api-validator
- run: pip install ./stac_fastapi/api ./stac_fastapi/types ./stac_fastapi/${{ matrix.backend }}[server] stac-api-validator==0.4.1
- - name: Run migration
- if: ${{ matrix.backend == 'sqlalchemy' }}
- run: cd stac_fastapi/sqlalchemy && alembic upgrade head
- env:
- POSTGRES_USER: username
- POSTGRES_PASS: password
- POSTGRES_DBNAME: postgis
- POSTGRES_HOST: localhost
- POSTGRES_PORT: 5432
- - name: Load data and validate
- run: python -m stac_fastapi.${{ matrix.backend }}.app & ./scripts/wait-for-it.sh localhost:8080 && python ./scripts/ingest_joplin.py http://localhost:8080 && ./scripts/validate http://localhost:8080
- env:
- POSTGRES_USER: username
- POSTGRES_PASS: password
- POSTGRES_DBNAME: postgis
- POSTGRES_HOST_READER: localhost
- POSTGRES_HOST_WRITER: localhost
- POSTGRES_PORT: 5432
- PGUSER: username
- PGPASSWORD: password
- PGDATABASE: postgis
- APP_HOST: 0.0.0.0
- APP_PORT: 8080
test-docs:
runs-on: ubuntu-latest
diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml
index 546718aab..e0a298137 100644
--- a/.github/workflows/deploy_mkdocs.yml
+++ b/.github/workflows/deploy_mkdocs.yml
@@ -32,7 +32,6 @@ jobs:
stac_fastapi/api \
stac_fastapi/types \
stac_fastapi/extensions \
- stac_fastapi/sqlalchemy
python -m pip install mkdocs mkdocs-material pdocs
- name: update API docs
diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml
deleted file mode 100644
index 2267b3077..000000000
--- a/.github/workflows/packages.yml
+++ /dev/null
@@ -1,45 +0,0 @@
-name: packages
-on:
- push:
- branches:
- - main
- tags:
- - "*"
-
-jobs:
- docker-build-push:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- packages: write
- strategy:
- fail-fast: true
- matrix:
- backend: ["sqlalchemy", "pgstac"]
- steps:
- - name: Checkout repository
- uses: actions/checkout@v3
- - name: Log in to the Container registry
- uses: docker/login-action@v2.1.0
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- - name: Extract metadata (tags, labels) for Docker
- id: meta
- uses: docker/metadata-action@v4.3.0
- with:
- images: ghcr.io/stac-utils/stac-fastapi
- tags: |
- type=schedule,suffix=-${{ matrix.backend }}
- type=ref,event=branch,suffix=-${{ matrix.backend }}
- type=ref,event=tag,suffix=-${{ matrix.backend }}
- type=ref,event=pr,suffix=-${{ matrix.backend }}
- - name: Build and push Docker image
- uses: docker/build-push-action@v4.0.0
- with:
- context: .
- file: docker/Dockerfile.${{ matrix.backend }}
- push: true
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index cba32d78e..8222a5dfe 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,57 +1,54 @@
repos:
- - repo: https://github.com/PyCQA/isort
- rev: 5.12.0
- hooks:
- - id: isort
- language_version: python3.8
- -
- repo: https://github.com/psf/black
- rev: 22.12.0
- hooks:
- - id: black
- args: ['--safe']
- language_version: python3.8
- -
- repo: https://github.com/pycqa/flake8
- rev: 6.0.0
- hooks:
- - id: flake8
- language_version: python3.8
- args: [
- # E501 let black handle all line length decisions
- # W503 black conflicts with "line break before operator" rule
- # E203 black conflicts with "whitespace before ':'" rule
- '--ignore=E501,W503,E203,C901']
- -
- repo: https://github.com/chewse/pre-commit-mirrors-pydocstyle
- # 2.1.1
- rev: v2.1.1
- hooks:
- - id: pydocstyle
- language_version: python3.8
- exclude: '.*(test|alembic|scripts).*'
- args: [
- # Check for docstring presence only
- '--select=D1',
-
- ]
- # Don't require docstrings for tests
- # '--match=(?!test).*\.py']
-# -
-# repo: https://github.com/pre-commit/mirrors-mypy
-# rev: v0.770
-# hooks:
-# - id: mypy
-# language_version: python3.8
-# args: [--no-strict-optional, --ignore-missing-imports]
- -
- repo: https://github.com/PyCQA/pydocstyle
- rev: 6.3.0
- hooks:
- - id: pydocstyle
- language_version: python3.8
- exclude: '.*(test|alembic|scripts).*'
- #args: [
- # Don't require docstrings for tests
- #'--match=(?!test|alembic|scripts).*\.py',
- #]
\ No newline at end of file
+ - repo: https://github.com/PyCQA/isort
+ rev: 5.12.0
+ hooks:
+ - id: isort
+ language_version: python3.8
+ - repo: https://github.com/psf/black
+ rev: 22.12.0
+ hooks:
+ - id: black
+ args: ["--safe"]
+ language_version: python3.8
+ - repo: https://github.com/pycqa/flake8
+ rev: 6.0.0
+ hooks:
+ - id: flake8
+ language_version: python3.8
+ args: [
+ # E501 let black handle all line length decisions
+ # W503 black conflicts with "line break before operator" rule
+ # E203 black conflicts with "whitespace before ':'" rule
+ "--ignore=E501,W503,E203,C901",
+ ]
+ - repo: https://github.com/chewse/pre-commit-mirrors-pydocstyle
+ # 2.1.1
+ rev: v2.1.1
+ hooks:
+ - id: pydocstyle
+ language_version: python3.8
+ exclude: ".*(test|alembic|scripts).*"
+ args:
+ [
+ # Check for docstring presence only
+ "--select=D1",
+ ]
+ # Don't require docstrings for tests
+ # '--match=(?!test).*\.py']
+ # -
+ # repo: https://github.com/pre-commit/mirrors-mypy
+ # rev: v0.770
+ # hooks:
+ # - id: mypy
+ # language_version: python3.8
+ # args: [--no-strict-optional, --ignore-missing-imports]
+ - repo: https://github.com/PyCQA/pydocstyle
+ rev: 6.3.0
+ hooks:
+ - id: pydocstyle
+ language_version: python3.8
+ exclude: ".*(test|alembic|scripts).*"
+ #args: [
+ # Don't require docstrings for tests
+ #'--match=(?!test|alembic|scripts).*\.py',
+ #]
diff --git a/CHANGES.md b/CHANGES.md
index fb8bc6f34..1edb883f9 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -8,6 +8,12 @@
* Default branch to **main** ([#544](https://github.com/stac-utils/stac-fastapi/pull/544))
+### Removed
+
+* Backends ([#555](https://github.com/stac-utils/stac-fastapi/pull/555))
+ * **stac-fastapi-pgstac**:
+ * **stac-fastapi-sqlalchemy**:
+
### Fixed
* Use `V()` instead of f-strings for pgstac queries ([#554](https://github.com/stac-utils/stac-fastapi/pull/554))
diff --git a/docker/Dockerfile b/Dockerfile
similarity index 75%
rename from docker/Dockerfile
rename to Dockerfile
index 5c218e279..2187ac53e 100644
--- a/docker/Dockerfile
+++ b/Dockerfile
@@ -18,6 +18,4 @@ COPY . /app
RUN pip install -e ./stac_fastapi/types[dev] && \
pip install -e ./stac_fastapi/api[dev] && \
- pip install -e ./stac_fastapi/extensions[dev] && \
- pip install -e ./stac_fastapi/sqlalchemy[dev,server] && \
- pip install -e ./stac_fastapi/pgstac[dev,server]
+ pip install -e ./stac_fastapi/extensions[dev]
diff --git a/docker/Dockerfile.docs b/Dockerfile.docs
similarity index 64%
rename from docker/Dockerfile.docs
rename to Dockerfile.docs
index f145b311a..e3c7447e5 100644
--- a/docker/Dockerfile.docs
+++ b/Dockerfile.docs
@@ -13,13 +13,12 @@ WORKDIR /opt/src
RUN python -m pip install \
stac_fastapi/api \
stac_fastapi/types \
- stac_fastapi/extensions \
- stac_fastapi/sqlalchemy
+ stac_fastapi/extensions
CMD ["pdocs", \
- "as_markdown", \
- "--output_dir", \
- "docs/api/", \
- "--exclude_source", \
- "--overwrite", \
- "stac_fastapi"]
\ No newline at end of file
+ "as_markdown", \
+ "--output_dir", \
+ "docs/api/", \
+ "--exclude_source", \
+ "--overwrite", \
+ "stac_fastapi"]
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 9c0c96efd..517b9b996 100644
--- a/Makefile
+++ b/Makefile
@@ -1,91 +1,14 @@
-#!make
-APP_HOST ?= 0.0.0.0
-APP_PORT ?= 8080
-EXTERNAL_APP_PORT ?= ${APP_PORT}
-run_sqlalchemy = docker-compose run --rm \
- -p ${EXTERNAL_APP_PORT}:${APP_PORT} \
- -e APP_HOST=${APP_HOST} \
- -e APP_PORT=${APP_PORT} \
- app-sqlalchemy
-
-run_pgstac = docker-compose run --rm \
- -p ${EXTERNAL_APP_PORT}:${APP_PORT} \
- -e APP_HOST=${APP_HOST} \
- -e APP_PORT=${APP_PORT} \
- app-pgstac
-
-LOG_LEVEL ?= warning
-
.PHONY: image
image:
- docker-compose build
-
-.PHONY: docker-run-all
-docker-run-all:
- docker-compose up
-
-.PHONY: docker-run-sqlalchemy
-docker-run-sqlalchemy: image
- $(run_sqlalchemy)
-
-.PHONY: docker-run-pgstac
-docker-run-pgstac: image
- $(run_pgstac)
-
-.PHONY: docker-run-nginx-proxy
-docker-run-nginx-proxy:
- docker-compose -f docker-compose.yml -f docker-compose.nginx.yml up
-
-.PHONY: docker-shell-sqlalchemy
-docker-shell-sqlalchemy:
- $(run_sqlalchemy) /bin/bash
-
-.PHONY: docker-shell-pgstac
-docker-shell-pgstac:
- $(run_pgstac) /bin/bash
+ docker build .
-.PHONY: test-sqlalchemy
-test-sqlalchemy: run-joplin-sqlalchemy
- $(run_sqlalchemy) /bin/bash -c 'export && ./scripts/wait-for-it.sh database:5432 && cd /app/stac_fastapi/sqlalchemy/tests/ && pytest -vvv --log-cli-level $(LOG_LEVEL)'
-
-.PHONY: test-pgstac
-test-pgstac:
- $(run_pgstac) /bin/bash -c 'export && ./scripts/wait-for-it.sh database:5432 && cd /app/stac_fastapi/pgstac/tests/ && pytest -vvv --log-cli-level $(LOG_LEVEL)'
-
-.PHONY: test-api
-test-api:
- $(run_sqlalchemy) /bin/bash -c 'cd /app/stac_fastapi/api && pytest -svvv --log-cli-level $(LOG_LEVEL)'
-
-.PHONY: run-database
-run-database:
- docker-compose run --rm database
-
-.PHONY: run-joplin-sqlalchemy
-run-joplin-sqlalchemy:
- docker-compose run --rm loadjoplin-sqlalchemy
-
-.PHONY: run-joplin-pgstac
-run-joplin-pgstac:
- docker-compose run --rm loadjoplin-pgstac
-
-.PHONY: test
-test: test-sqlalchemy test-pgstac
-
-.PHONY: pybase-install
-pybase-install:
+.PHONY: install
+install:
pip install wheel && \
pip install -e ./stac_fastapi/api[dev] && \
pip install -e ./stac_fastapi/types[dev] && \
pip install -e ./stac_fastapi/extensions[dev]
-.PHONY: pgstac-install
-pgstac-install: pybase-install
- pip install -e ./stac_fastapi/pgstac[dev,server]
-
-.PHONY: sqlalchemy-install
-sqlalchemy-install: pybase-install
- pip install -e ./stac_fastapi/sqlalchemy[dev,server]
-
.PHONY: docs-image
docs-image:
docker-compose -f docker-compose.docs.yml \
diff --git a/README.md b/README.md
index a05b50b1d..2db1d1f37 100644
--- a/README.md
+++ b/README.md
@@ -33,135 +33,40 @@ packages:
#### Backends
-- **stac_fastapi.sqlalchemy**: Postgres backend implementation with sqlalchemy.
-- **stac_fastapi.pgstac**: Postgres backend implementation with [PGStac](https://github.com/stac-utils/pgstac).
+Backends are hosted in their own repositories:
+
+- [stac-fastapi-pgstac](https://github.com/stac-utils/stac-fastapi-pgstac): Postgres backend implementation with [PgSTAC](https://github.com/stac-utils/pgstac).
+- [stac-fastapi-sqlalchemy](https://github.com/stac-utils/stac-fastapi-sqlalchemy) Postgres backend implementation with [sqlalchemy](https://www.sqlalchemy.org/).
`stac-fastapi` was initially developed by [arturo-ai](https://github.com/arturo-ai).
## Installation
```bash
-# Install from pypi.org
+# Install from PyPI
pip install stac-fastapi.api stac-fastapi.types stac-fastapi.extensions
# Install a backend of your choice
pip install stac-fastapi.sqlalchemy
# or
pip install stac-fastapi.pgstac
-
-#/////////////////////
-# Install from sources
-
-git clone https://github.com/stac-utils/stac-fastapi.git && cd stac-fastapi
-pip install \
- -e stac_fastapi/api \
- -e stac_fastapi/types \
- -e stac_fastapi/extensions
-
-# Install a backend of your choice
-pip install -e stac_fastapi/sqlalchemy
-# or
-pip install -e stac_fastapi/pgstac
-```
-
-### Pre-built Docker images
-
-Pre-built images are available from the [Github Container Registry](https://github.com/stac-utils/stac-fastapi/pkgs/container/stac-fastapi).
-The latest images are tagged with `main-pgstac` and `main-sqlalchemy`.
-To pull the image to your local system:
-
-```shell
-docker pull ghcr.io/stac-utils/stac-fastapi:main-pgstac # or main-sqlalchemy
-```
-
-This repository provides two example [Docker compose](https://docs.docker.com/compose/) files that demonstrate how you might link the pre-built images with a postgres/pgstac database:
-
-- [docker-compose.pgstac.yml](./docker/docker-compose.pgstac.yml)
-- [docker-compose.sqlalchemy.yml](./docker/docker-compose.sqlalchemy.yml)
-
-## Local Development
-
-Use docker-compose via make to start the application, migrate the database, and ingest some example data:
-
-```bash
-make image
-make docker-run-all
```
-- The SQLAlchemy backend app will be available on .
-- The PGStac backend app will be available on .
-
-You can also launch only one of the applications with either of these commands:
-
-```shell
-make docker-run-pgstac
-make docker-run-sqlalchemy
-```
-
-The application will be started on .
-
-By default, the apps are run with uvicorn hot-reloading enabled. This can be turned off by changing the value
-of the `RELOAD` env var in docker-compose.yml to `false`.
-
-### nginx proxy
-
-This repo includes an example nginx proxy service.
-To start:
+Other backends may be available from other sources, search [PyPI](https://pypi.org/) for more.
-```shell
-make docker-run-nginx-proxy
-```
-
-The proxy will be started on , with the pgstac app available at and the sqlalchemy app at .
-If you need to customize the proxy port, use the `STAC_FASTAPI_NGINX_PORT` environment variable:
-
-```shell
-STAC_FASTAPI_NGINX_PORT=7822 make docker-run-nginx-proxy
-```
-
-### Note to Docker for Windows users
-
-You'll need to enable experimental features on Docker for Windows in order to run the docker-compose,
-due to the "--platform" flag that is required to allow the project to run on some Apple architectures.
-To do this, open Docker Desktop, go to settings, select "Docker Engine", and modify the configuration
-JSON to have `"experimental": true`.
-
-### Testing
-
-Before running the tests, ensure the database and apps run with docker-compose are down:
-
-```shell
-docker-compose down
-```
-
-The database container provided by the docker-compose stack must be running. This can be started with:
-
-```shell
-make run-database
-```
-
-To run tests for both the pgstac and sqlalchemy backends, execute:
-
-```shell
-make test
-```
-
-To only run pgstac backend tests:
-
-```shell
-make test-pgstac
-```
+## Development
-To only run sqlalchemy backend tests:
+Install the packages in editable mode:
```shell
-make test-sqlalchemy
+pip install -e \
+ 'stac_fastapi/api[dev]' \
+ 'stac_fastapi/types[dev]' \
+ 'stac_fastapi/extensions[dev]'
```
-Run individual tests by running pytest within a docker container:
+To run the tests:
```shell
-make docker-shell-pgstac # or docker-shell-sqlalchemy
-$ pip install -e stac_fastapi/pgstac[dev]
-$ pytest -v stac_fastapi/pgstac/tests/api/test_api.py
+pytest
```
diff --git a/docker-compose.docs.yml b/docker-compose.docs.yml
index 5ed87782e..9c441f186 100644
--- a/docker-compose.docs.yml
+++ b/docker-compose.docs.yml
@@ -5,7 +5,7 @@ services:
container_name: stac-fastapi-docs-dev
build:
context: .
- dockerfile: docker/Dockerfile.docs
+ dockerfile: Dockerfile.docs
platform: linux/amd64
environment:
- POSTGRES_USER=username
diff --git a/docker-compose.nginx.yml b/docker-compose.nginx.yml
deleted file mode 100644
index b70bffe50..000000000
--- a/docker-compose.nginx.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-version: '3'
-services:
- nginx:
- image: nginx
- ports:
- - ${STAC_FASTAPI_NGINX_PORT:-80}:80
- volumes:
- - ./nginx.conf:/etc/nginx/nginx.conf
- depends_on:
- - app-pgstac
- - app-sqlalchemy
- command: [ "nginx-debug", "-g", "daemon off;" ]
- app-pgstac:
- environment:
- - UVICORN_ROOT_PATH=/api/v1/pgstac
- app-sqlalchemy:
- environment:
- - UVICORN_ROOT_PATH=/api/v1/sqlalchemy
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index 5637f7e0c..000000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,118 +0,0 @@
-version: '3'
-services:
- app-sqlalchemy:
- container_name: stac-fastapi-sqlalchemy
- image: stac-utils/stac-fastapi
- build:
- context: .
- dockerfile: docker/Dockerfile
- platform: linux/amd64
- environment:
- - APP_HOST=0.0.0.0
- - APP_PORT=8081
- - RELOAD=true
- - ENVIRONMENT=local
- - POSTGRES_USER=username
- - POSTGRES_PASS=password
- - POSTGRES_DBNAME=postgis
- - POSTGRES_HOST_READER=database
- - POSTGRES_HOST_WRITER=database
- - POSTGRES_PORT=5432
- - WEB_CONCURRENCY=10
- ports:
- - "8081:8081"
- volumes:
- - ./stac_fastapi:/app/stac_fastapi
- - ./scripts:/app/scripts
- depends_on:
- - database
- command: bash -c "./scripts/wait-for-it.sh database:5432 && python -m stac_fastapi.sqlalchemy.app"
-
- app-pgstac:
- container_name: stac-fastapi-pgstac
- image: stac-utils/stac-fastapi
- platform: linux/amd64
- environment:
- - APP_HOST=0.0.0.0
- - APP_PORT=8082
- - RELOAD=true
- - ENVIRONMENT=local
- - POSTGRES_USER=username
- - POSTGRES_PASS=password
- - POSTGRES_DBNAME=postgis
- - POSTGRES_HOST_READER=database
- - POSTGRES_HOST_WRITER=database
- - POSTGRES_PORT=5432
- - WEB_CONCURRENCY=10
- - VSI_CACHE=TRUE
- - GDAL_HTTP_MERGE_CONSECUTIVE_RANGES=YES
- - GDAL_DISABLE_READDIR_ON_OPEN=EMPTY_DIR
- - DB_MIN_CONN_SIZE=1
- - DB_MAX_CONN_SIZE=1
- - USE_API_HYDRATE=${USE_API_HYDRATE:-false}
- ports:
- - "8082:8082"
- volumes:
- - ./stac_fastapi:/app/stac_fastapi
- - ./scripts:/app/scripts
- depends_on:
- - database
- command: bash -c "./scripts/wait-for-it.sh database:5432 && python -m stac_fastapi.pgstac.app"
-
- database:
- container_name: stac-db
- image: ghcr.io/stac-utils/pgstac:v0.7.1
- environment:
- - POSTGRES_USER=username
- - POSTGRES_PASSWORD=password
- - POSTGRES_DB=postgis
- - PGUSER=username
- - PGPASSWORD=password
- - PGDATABASE=postgis
- ports:
- - "5439:5432"
- command: postgres -N 500
-
- # Load joplin demo dataset into the SQLAlchemy Application
- loadjoplin-sqlalchemy:
- image: stac-utils/stac-fastapi
- environment:
- - ENVIRONMENT=development
- - POSTGRES_USER=username
- - POSTGRES_PASS=password
- - POSTGRES_DBNAME=postgis
- - POSTGRES_HOST=database
- - POSTGRES_PORT=5432
- volumes:
- - ./stac_fastapi:/app/stac_fastapi
- - ./scripts:/app/scripts
- command: >
- bash -c "./scripts/wait-for-it.sh app-sqlalchemy:8081 -t 60 && cd stac_fastapi/sqlalchemy && alembic upgrade head && python /app/scripts/ingest_joplin.py http://app-sqlalchemy:8081"
- depends_on:
- - database
- - app-sqlalchemy
-
- # Load joplin demo dataset into the PGStac Application
- loadjoplin-pgstac:
- image: stac-utils/stac-fastapi
- environment:
- - ENVIRONMENT=development
- volumes:
- - ./stac_fastapi:/app/stac_fastapi
- - ./scripts:/app/scripts
- command:
- - "./scripts/wait-for-it.sh"
- - "-t"
- - "60"
- - "app-pgstac:8082"
- - "--"
- - "python"
- - "/app/scripts/ingest_joplin.py"
- - "http://app-pgstac:8082"
- depends_on:
- - database
- - app-pgstac
-
-networks:
- default:
- name: stac-fastapi-network
diff --git a/docker/Dockerfile.pgstac b/docker/Dockerfile.pgstac
deleted file mode 100644
index 1c1afe6f7..000000000
--- a/docker/Dockerfile.pgstac
+++ /dev/null
@@ -1,25 +0,0 @@
-FROM python:3.8-slim as builder
-
-RUN python -m venv /opt/venv
-
-ENV PATH="/opt/venv/bin:$PATH"
-
-WORKDIR /app
-
-COPY . /app
-
-RUN pip install ./stac_fastapi/types && \
- pip install ./stac_fastapi/api && \
- pip install ./stac_fastapi/extensions && \
- pip install ./stac_fastapi/pgstac[server]
-
-
-FROM python:3.8-slim as pgstac
-
-ENV CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
-
-COPY --from=builder /opt/venv /opt/venv
-
-ENV PATH="/opt/venv/bin:$PATH"
-
-CMD ["uvicorn", "stac_fastapi.pgstac.app:app", "--host", "0.0.0.0", "--port", "8080"]
diff --git a/docker/Dockerfile.sqlalchemy b/docker/Dockerfile.sqlalchemy
deleted file mode 100644
index 92b86b67e..000000000
--- a/docker/Dockerfile.sqlalchemy
+++ /dev/null
@@ -1,27 +0,0 @@
-FROM python:3.8-slim as builder
-
-RUN python -m venv /opt/venv
-
-ENV PATH="/opt/venv/bin:$PATH"
-
-WORKDIR /app
-
-COPY . /app
-
-RUN pip install ./stac_fastapi/types && \
- pip install ./stac_fastapi/api && \
- pip install ./stac_fastapi/extensions && \
- pip install ./stac_fastapi/sqlalchemy[server]
-
-
-FROM python:3.8-slim as sqlalchemy
-
-ENV CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
-
-COPY --from=builder /opt/venv /opt/venv
-COPY ./stac_fastapi/sqlalchemy/alembic /app/alembic
-COPY ./stac_fastapi/sqlalchemy/alembic.ini /app/alembic.ini
-
-ENV PATH="/opt/venv/bin:$PATH"
-
-CMD ["uvicorn", "stac_fastapi.sqlalchemy.app:app", "--host", "0.0.0.0", "--port", "8080"]
diff --git a/docker/docker-compose.pgstac.yml b/docker/docker-compose.pgstac.yml
deleted file mode 100644
index e5710e51a..000000000
--- a/docker/docker-compose.pgstac.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-version: '3'
-services:
- stac-fastapi-pgstac:
- image: ghcr.io/stac-utils/stac-fastapi:main-pgstac
- platform: linux/amd64
- environment:
- - APP_HOST=0.0.0.0
- - ENVIRONMENT=local
- - POSTGRES_USER=username
- - POSTGRES_PASS=password
- - POSTGRES_DBNAME=postgis
- - POSTGRES_HOST_READER=pgstac
- - POSTGRES_HOST_WRITER=pgstac
- - POSTGRES_PORT=5432
- - WEB_CONCURRENCY=10
- - VSI_CACHE=TRUE
- - GDAL_HTTP_MERGE_CONSECUTIVE_RANGES=YES
- - GDAL_DISABLE_READDIR_ON_OPEN=EMPTY_DIR
- - DB_MIN_CONN_SIZE=1
- - DB_MAX_CONN_SIZE=1
- - USE_API_HYDRATE=${USE_API_HYDRATE:-false}
- ports:
- - "8080:8080"
- depends_on:
- - pgstac
-
- pgstac:
- image: ghcr.io/stac-utils/pgstac:v0.7.1
- environment:
- - POSTGRES_USER=username
- - POSTGRES_PASSWORD=password
- - POSTGRES_DB=postgis
- - PGUSER=username
- - PGPASSWORD=password
- - PGDATABASE=postgis
- ports:
- - "5439:5432"
- command: postgres -N 500
-
-networks:
- default:
- name: stac-fastapi-network
diff --git a/docker/docker-compose.sqlalchemy.yml b/docker/docker-compose.sqlalchemy.yml
deleted file mode 100644
index 8c43418bf..000000000
--- a/docker/docker-compose.sqlalchemy.yml
+++ /dev/null
@@ -1,48 +0,0 @@
-version: '3'
-services:
- stac-fastapi-sqlalchemy:
- image: ghcr.io/stac-utils/stac-fastapi:main-sqlalchemy
- platform: linux/amd64
- environment:
- - APP_HOST=0.0.0.0
- - APP_PORT=8080
- - POSTGRES_USER=username
- - POSTGRES_PASS=password
- - POSTGRES_DBNAME=postgis
- - POSTGRES_HOST_READER=pgstac
- - POSTGRES_HOST_WRITER=pgstac
- - POSTGRES_PORT=5432
- - WEB_CONCURRENCY=10
- ports:
- - "8080:8080"
- depends_on:
- - pgstac
-
- pgstac:
- image: ghcr.io/stac-utils/pgstac:v0.7.1
- environment:
- - POSTGRES_USER=username
- - POSTGRES_PASSWORD=password
- - POSTGRES_DB=postgis
- - PGUSER=username
- - PGPASSWORD=password
- - PGDATABASE=postgis
- ports:
- - "5439:5432"
- command: postgres -N 500
-
- migrate:
- image: ghcr.io/stac-utils/stac-fastapi:main-sqlalchemy
- command: bash -c "cd /app && alembic upgrade head"
- environment:
- - POSTGRES_USER=username
- - POSTGRES_PASS=password
- - POSTGRES_DBNAME=postgis
- - POSTGRES_HOST=pgstac
- - POSTGRES_PORT=5432
- depends_on:
- - stac-fastapi-sqlalchemy
-
-networks:
- default:
- name: stac-fastapi-network
diff --git a/docs/tips-and-tricks.md b/docs/tips-and-tricks.md
index 3d4c9ac0f..ca5463c59 100644
--- a/docs/tips-and-tricks.md
+++ b/docs/tips-and-tricks.md
@@ -1,29 +1,33 @@
# Tips and Tricks
-This page contains a few 'tips and tricks' for getting stac-fastapi working in various situations.
+
+This page contains a few 'tips and tricks' for getting **stac-fastapi** working in various situations.
## Get stac-fastapi working with CORS
-CORS (Cross-Origin Resource Sharing) support may be required to use stac-fastapi in certain situations. For example, if you are running
-[stac-browser](https://github.com/radiantearth/stac-browser) to browse the STAC catalog created by stac-fastapi, then you will need to enable CORS support.
-To do this, edit `stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/app.py` (or the equivalent in the `pgstac` folder) and add the following import:
+CORS (Cross-Origin Resource Sharing) support may be required to use stac-fastapi in certain situations.
+For example, if you are running [stac-browser](https://github.com/radiantearth/stac-browser) to browse the STAC catalog created by **stac-fastapi**, then you will need to enable CORS support.
+To do this, edit your backend's `app.py` and add the following import:
-```
+```python
from fastapi.middleware.cors import CORSMiddleware
```
and then edit the `api = StacApi(...` call to add the following parameter:
-```
+```python
middlewares=[lambda app: CORSMiddleware(app, allow_origins=["*"])]
```
If needed, you can edit the `allow_origins` parameter to only allow CORS requests from specific origins.
## Enable the Context extension
-The Context STAC extension provides information on the number of items matched and returned from a STAC search. This is required by various other STAC-related tools, such as the pystac command-line client. To enable the extension, edit `stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/app.py` (or the equivalent in the `pgstac` folder) and add the following import:
-```
+The Context STAC extension provides information on the number of items matched and returned from a STAC search.
+This is required by various other STAC-related tools, such as the pystac command-line client.
+To enable the extension, edit your backend's `app.py` and add the following import:
+
+```python
from stac_fastapi.extensions.core.context import ContextExtension
```
-and then edit the `api = StacApi(...` call to add `ContextExtension()` to the list given as the `extensions` parameter.
\ No newline at end of file
+and then edit the `api = StacApi(...` call to add `ContextExtension()` to the list given as the `extensions` parameter.
diff --git a/mkdocs.yml b/mkdocs.yml
index d0e1b021d..60374dc3b 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -2,64 +2,77 @@ site_name: stac-fastapi
site_description: STAC FastAPI.
# Repository
-repo_name: 'stac-utils/stac-fastapi'
-repo_url: 'https://github.com/stac-utils/stac-fastapi'
-edit_uri: 'blob/master/docs/src/'
-
+repo_name: "stac-utils/stac-fastapi"
+repo_url: "https://github.com/stac-utils/stac-fastapi"
+edit_uri: "blob/master/docs/src/"
# Social links
extra:
social:
- - icon: 'fontawesome/brands/github'
- link: 'https://github.com/stac-utils'
+ - icon: "fontawesome/brands/github"
+ link: "https://github.com/stac-utils"
# Layout
nav:
- - Home: 'index.md'
+ - Home: "index.md"
- API:
- - stac_fastapi.api:
- - app: api/stac_fastapi/api/app.md
- - config: api/stac_fastapi/api/config.md
- - errors: api/stac_fastapi/api/errors.md
- - middleware: api/stac_fastapi/api/middleware.md
- - models: api/stac_fastapi/api/models.md
- - openapi: api/stac_fastapi/api/openapi.md
- - routes: api/stac_fastapi/api/routes.md
- - stac_fastapi.extensions:
- - core:
- - context: api/stac_fastapi/extensions/core/context.md
- - filter: api/stac_fastapi/extensions/core/filter/filter.md
- - fields: api/stac_fastapi/extensions/core/fields/fields.md
- - query: api/stac_fastapi/extensions/core/query/query.md
- - sort: api/stac_fastapi/extensions/core/sort/sort.md
- - transaction: api/stac_fastapi/extensions/core/transaction.md
- - pagination: api/stac_fastapi/extensions/core/pagination/pagination.md
- - third_party:
- - bulk_transactions: api/stac_fastapi/extensions/third_party/bulk_transactions.md
- - stac_fastapi.server:
- - app: api/stac_fastapi/server/app.md
- - stac_fastapi.sqlalchemy:
- - models:
- - database: api/stac_fastapi/sqlalchemy/models/database.md
- - decompose: api/stac_fastapi/sqlalchemy/models/decompose.md
- - links: api/stac_fastapi/sqlalchemy/models/links.md
- - schemas: api/stac_fastapi/sqlalchemy/models/schemas.md
- - config: api/stac_fastapi/sqlalchemy/config.md
- - core: api/stac_fastapi/sqlalchemy/core.md
- - session: api/stac_fastapi/sqlalchemy/session.md
- - tokens: api/stac_fastapi/sqlalchemy/tokens.md
- - transactions: api/stac_fastapi/sqlalchemy/transactions.md
- - version: api/stac_fastapi/sqlalchemy/version.md
- - stac_fastapi.types:
- - core: api/stac_fastapi/types/core.md
- - config: api/stac_fastapi/types/config.md
- - errors: api/stac_fastapi/types/errors.md
- - extension: api/stac_fastapi/types/extension.md
- - index: api/stac_fastapi/types/index.md
- - search: api/stac_fastapi/types/search.md
- - version: api/stac_fastapi/types/version.md
- - Development - Contributing: 'contributing.md'
- - Release Notes: 'release-notes.md'
+ - packages: api/stac_fastapi/index.md
+ - stac_fastapi.api:
+ - module: api/stac_fastapi/api/index.md
+ - app: api/stac_fastapi/api/app.md
+ - config: api/stac_fastapi/api/config.md
+ - errors: api/stac_fastapi/api/errors.md
+ - middleware: api/stac_fastapi/api/middleware.md
+ - models: api/stac_fastapi/api/models.md
+ - openapi: api/stac_fastapi/api/openapi.md
+ - routes: api/stac_fastapi/api/routes.md
+ - version: api/stac_fastapi/api/version.md
+ - stac_fastapi.extensions:
+ - module: api/stac_fastapi/extensions/index.md
+ - core:
+ - module: api/stac_fastapi/extensions/core/index.md
+ - context: api/stac_fastapi/extensions/core/context.md
+ - filter:
+ - module: api/stac_fastapi/extensions/core/filter/index.md
+ - filter: api/stac_fastapi/extensions/core/filter/filter.md
+ - request: api/stac_fastapi/extensions/core/filter/request.md
+ - fields:
+ - module: api/stac_fastapi/extensions/core/fields/index.md
+ - fields: api/stac_fastapi/extensions/core/fields/fields.md
+ - request: api/stac_fastapi/extensions/core/fields/request.md
+ - query:
+ - module: api/stac_fastapi/extensions/core/query/index.md
+ - query: api/stac_fastapi/extensions/core/query/query.md
+ - request: api/stac_fastapi/extensions/core/query/request.md
+ - sort:
+ - module: api/stac_fastapi/extensions/core/sort/index.md
+ - request: api/stac_fastapi/extensions/core/sort/request.md
+ - sort: api/stac_fastapi/extensions/core/sort/sort.md
+ - transaction: api/stac_fastapi/extensions/core/transaction.md
+ - pagination:
+ - module: api/stac_fastapi/extensions/core/pagination/index.md
+ - pagination: api/stac_fastapi/extensions/core/pagination/pagination.md
+ - token_pagination: api/stac_fastapi/extensions/core/pagination/token_pagination.md
+ - version: api/stac_fastapi/extensions/version.md
+ - third_party:
+ - bulk_transactions: api/stac_fastapi/extensions/third_party/bulk_transactions.md
+ - index: api/stac_fastapi/extensions/third_party/index.md
+ - stac_fastapi.types:
+ - module: api/stac_fastapi/types/index.md
+ - config: api/stac_fastapi/types/config.md
+ - conformance: api/stac_fastapi/types/conformance.md
+ - core: api/stac_fastapi/types/core.md
+ - errors: api/stac_fastapi/types/errors.md
+ - extension: api/stac_fastapi/types/extension.md
+ - links: api/stac_fastapi/types/links.md
+ - requests: api/stac_fastapi/types/requests.md
+ - rfc3339: api/stac_fastapi/types/rfc3339.md
+ - search: api/stac_fastapi/types/search.md
+ - stac: api/stac_fastapi/types/stac.md
+ - version: api/stac_fastapi/types/version.md
+ - Development - Contributing: "contributing.md"
+ - Release Notes: "release-notes.md"
+ - Tips and Tricks: tips-and-tricks.md
plugins:
- search
@@ -67,13 +80,13 @@ plugins:
# Theme
theme:
icon:
- logo: 'material/home'
- repo: 'fontawesome/brands/github'
- name: 'material'
- language: 'en'
+ logo: "material/home"
+ repo: "fontawesome/brands/github"
+ name: "material"
+ language: "en"
font:
- text: 'Nunito Sans'
- code: 'Fira Code'
+ text: "Nunito Sans"
+ code: "Fira Code"
extra_css:
- stylesheets/extra.css
@@ -82,28 +95,28 @@ extra_css:
# This way, I can write in Pandoc's Markdown and have it be supported here.
# https://pandoc.org/MANUAL.html
markdown_extensions:
- - admonition
- - attr_list
- - codehilite:
- guess_lang: false
- - def_list
- - footnotes
- - pymdownx.arithmatex
- - pymdownx.betterem
- - pymdownx.caret:
- insert: false
- - pymdownx.details
- - pymdownx.emoji
- - pymdownx.escapeall:
- hardbreak: true
- nbsp: true
- - pymdownx.magiclink:
- hide_protocol: true
- repo_url_shortener: true
- - pymdownx.smartsymbols
- - pymdownx.superfences
- - pymdownx.tasklist:
- custom_checkbox: true
- - pymdownx.tilde
- - toc:
- permalink: true
+ - admonition
+ - attr_list
+ - codehilite:
+ guess_lang: false
+ - def_list
+ - footnotes
+ - pymdownx.arithmatex
+ - pymdownx.betterem
+ - pymdownx.caret:
+ insert: false
+ - pymdownx.details
+ - pymdownx.emoji
+ - pymdownx.escapeall:
+ hardbreak: true
+ nbsp: true
+ - pymdownx.magiclink:
+ hide_protocol: true
+ repo_url_shortener: true
+ - pymdownx.smartsymbols
+ - pymdownx.superfences
+ - pymdownx.tasklist:
+ custom_checkbox: true
+ - pymdownx.tilde
+ - toc:
+ permalink: true
diff --git a/nginx.conf b/nginx.conf
deleted file mode 100644
index 0084e149e..000000000
--- a/nginx.conf
+++ /dev/null
@@ -1,29 +0,0 @@
-events {}
-
-http {
- server {
- listen 80;
-
- location /api/v1/pgstac {
- rewrite ^/api/v1/pgstac(.*)$ $1 break;
- proxy_pass http://app-pgstac:8082;
- proxy_set_header HOST $host;
- proxy_set_header Referer $http_referer;
- proxy_set_header X-Forwarded-For $remote_addr;
- proxy_set_header X-Forwarded-Proto $scheme;
- }
-
- location /api/v1/sqlalchemy {
- rewrite ^/api/v1/sqlalchemy(.*)$ $1 break;
- proxy_pass http://app-sqlalchemy:8081;
- proxy_set_header HOST $host;
- proxy_set_header Referer $http_referer;
- proxy_set_header X-Forwarded-For $remote_addr;
- proxy_set_header X-Forwarded-Proto $scheme;
- }
-
- location / {
- proxy_redirect off;
- }
- }
-}
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 4f04d8a5d..68f2f7731 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,5 +7,5 @@ max-line-length = 90
[tool.isort]
profile = "black"
known_first_party = "stac_fastapi"
-known_third_party = ["rasterio", "stac-pydantic", "sqlalchemy", "geoalchemy2", "fastapi"]
+known_third_party = ["stac-pydantic", "fastapi"]
sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]
diff --git a/scripts/ingest_joplin.py b/scripts/ingest_joplin.py
deleted file mode 100644
index 76320fa53..000000000
--- a/scripts/ingest_joplin.py
+++ /dev/null
@@ -1,48 +0,0 @@
-"""Ingest sample data during docker-compose"""
-import json
-import sys
-from pathlib import Path
-from urllib.parse import urljoin
-
-import requests
-
-workingdir = Path(__file__).parent.absolute()
-joplindata = workingdir.parent / "stac_fastapi" / "testdata" / "joplin"
-
-app_host = sys.argv[1]
-
-if not app_host:
- raise Exception("You must include full path/port to stac instance")
-
-
-def post_or_put(url: str, data: dict):
- """Post or put data to url."""
- r = requests.post(url, json=data)
- if r.status_code == 409:
- new_url = url if data["type"] == "Collection" else url + f"/{data['id']}"
- # Exists, so update
- r = requests.put(new_url, json=data)
- # Unchanged may throw a 404
- if not r.status_code == 404:
- r.raise_for_status()
- else:
- r.raise_for_status()
-
-
-def ingest_joplin_data(app_host: str = app_host, data_dir: Path = joplindata):
- """ingest data."""
-
- with open(data_dir / "collection.json") as f:
- collection = json.load(f)
-
- post_or_put(urljoin(app_host, "/collections"), collection)
-
- with open(data_dir / "index.geojson") as f:
- index = json.load(f)
-
- for feat in index["features"]:
- post_or_put(urljoin(app_host, f"collections/{collection['id']}/items"), feat)
-
-
-if __name__ == "__main__":
- ingest_joplin_data()
diff --git a/scripts/publish b/scripts/publish
index 083b13055..1cc7b5253 100755
--- a/scripts/publish
+++ b/scripts/publish
@@ -11,8 +11,6 @@ SUBPACKAGE_DIRS=(
"stac_fastapi/types"
"stac_fastapi/extensions"
"stac_fastapi/api"
- "stac_fastapi/sqlalchemy"
- "stac_fastapi/pgstac"
)
function usage() {
diff --git a/scripts/validate b/scripts/validate
deleted file mode 100755
index 3431ac36f..000000000
--- a/scripts/validate
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/env sh
-#
-# Validate a STAC server using [stac-api-validator](https://github.com/stac-utils/stac-api-validator).
-#
-# Assumptions:
-#
-# - You have stac-api-validator installed, e.g. via `pip install stac-api-validator`
-# - You've loaded the joplin data, probably using `python ./scripts/ingest_joplin.py http://localhost:8080``
-#
-# Currently, item-search is not checked, because it crashes stac-api-validator (probably a problem on our side).
-
-set -e
-
-if [ $# -eq 0 ]; then
- root_url=http://localhost:8080
-else
- root_url="$1"
-fi
-geometry='{"type":"Polygon","coordinates":[[[-94.6884155,37.0595608],[-94.6884155,37.0332547],[-94.6554565,37.0332547],[-94.6554565,37.0595608],[-94.6884155,37.0595608]]]}'
-
-stac-api-validator --root-url "$root_url" \
- --conformance core \
- --conformance collections \
- --conformance features \
- --conformance filter \
- --collection joplin \
- --geometry "$geometry"
- # --conformance item-search # currently breaks stac-api-validator
diff --git a/scripts/wait-for-it.sh b/scripts/wait-for-it.sh
deleted file mode 100755
index 7593a22b2..000000000
--- a/scripts/wait-for-it.sh
+++ /dev/null
@@ -1,186 +0,0 @@
-#!/usr/bin/env bash
-# Use this script to test if a given TCP host/port are available
-
-######################################################
-# Copied from https://github.com/vishnubob/wait-for-it
-######################################################
-
-WAITFORIT_cmdname=${0##*/}
-
-echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
-
-usage()
-{
- cat << USAGE >&2
-Usage:
- $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
- -h HOST | --host=HOST Host or IP under test
- -p PORT | --port=PORT TCP port under test
- Alternatively, you specify the host and port as host:port
- -s | --strict Only execute subcommand if the test succeeds
- -q | --quiet Don't output any status messages
- -t TIMEOUT | --timeout=TIMEOUT
- Timeout in seconds, zero for no timeout
- -- COMMAND ARGS Execute command with args after the test finishes
-USAGE
- exit 1
-}
-
-wait_for()
-{
- if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
- echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
- else
- echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
- fi
- WAITFORIT_start_ts=$(date +%s)
- while :
- do
- if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
- nc -z $WAITFORIT_HOST $WAITFORIT_PORT
- WAITFORIT_result=$?
- else
- (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
- WAITFORIT_result=$?
- fi
- if [[ $WAITFORIT_result -eq 0 ]]; then
- WAITFORIT_end_ts=$(date +%s)
- echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
- break
- fi
- sleep 1
- done
- return $WAITFORIT_result
-}
-
-wait_for_wrapper()
-{
- # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
- if [[ $WAITFORIT_QUIET -eq 1 ]]; then
- timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
- else
- timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
- fi
- WAITFORIT_PID=$!
- trap "kill -INT -$WAITFORIT_PID" INT
- wait $WAITFORIT_PID
- WAITFORIT_RESULT=$?
- if [[ $WAITFORIT_RESULT -ne 0 ]]; then
- echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
- fi
- return $WAITFORIT_RESULT
-}
-
-# process arguments
-while [[ $# -gt 0 ]]
-do
- case "$1" in
- *:* )
- WAITFORIT_hostport=(${1//:/ })
- WAITFORIT_HOST=${WAITFORIT_hostport[0]}
- WAITFORIT_PORT=${WAITFORIT_hostport[1]}
- shift 1
- ;;
- --child)
- WAITFORIT_CHILD=1
- shift 1
- ;;
- -q | --quiet)
- WAITFORIT_QUIET=1
- shift 1
- ;;
- -s | --strict)
- WAITFORIT_STRICT=1
- shift 1
- ;;
- -h)
- WAITFORIT_HOST="$2"
- if [[ $WAITFORIT_HOST == "" ]]; then break; fi
- shift 2
- ;;
- --host=*)
- WAITFORIT_HOST="${1#*=}"
- shift 1
- ;;
- -p)
- WAITFORIT_PORT="$2"
- if [[ $WAITFORIT_PORT == "" ]]; then break; fi
- shift 2
- ;;
- --port=*)
- WAITFORIT_PORT="${1#*=}"
- shift 1
- ;;
- -t)
- WAITFORIT_TIMEOUT="$2"
- if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
- shift 2
- ;;
- --timeout=*)
- WAITFORIT_TIMEOUT="${1#*=}"
- shift 1
- ;;
- --)
- shift
- WAITFORIT_CLI=("$@")
- break
- ;;
- --help)
- usage
- ;;
- *)
- echoerr "Unknown argument: $1"
- usage
- ;;
- esac
-done
-
-if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
- echoerr "Error: you need to provide a host and port to test."
- usage
-fi
-
-WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
-WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
-WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
-WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
-
-# Check to see if timeout is from busybox?
-WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
-WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
-
-WAITFORIT_BUSYTIMEFLAG=""
-if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
- WAITFORIT_ISBUSY=1
- # Check if busybox timeout uses -t flag
- # (recent Alpine versions don't support -t anymore)
- if timeout &>/dev/stdout | grep -q -e '-t '; then
- WAITFORIT_BUSYTIMEFLAG="-t"
- fi
-else
- WAITFORIT_ISBUSY=0
-fi
-
-if [[ $WAITFORIT_CHILD -gt 0 ]]; then
- wait_for
- WAITFORIT_RESULT=$?
- exit $WAITFORIT_RESULT
-else
- if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
- wait_for_wrapper
- WAITFORIT_RESULT=$?
- else
- wait_for
- WAITFORIT_RESULT=$?
- fi
-fi
-
-if [[ $WAITFORIT_CLI != "" ]]; then
- if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
- echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
- exit $WAITFORIT_RESULT
- fi
- exec "${WAITFORIT_CLI[@]}"
-else
- exit $WAITFORIT_RESULT
-fi
\ No newline at end of file
diff --git a/stac_fastapi/api/setup.py b/stac_fastapi/api/setup.py
index 2b70d4292..d0af6f3b5 100644
--- a/stac_fastapi/api/setup.py
+++ b/stac_fastapi/api/setup.py
@@ -15,6 +15,7 @@
extra_reqs = {
"dev": [
+ "httpx",
"pytest",
"pytest-cov",
"pytest-asyncio",
diff --git a/stac_fastapi/pgstac/README.md b/stac_fastapi/pgstac/README.md
deleted file mode 100644
index 80d7fc89a..000000000
--- a/stac_fastapi/pgstac/README.md
+++ /dev/null
@@ -1,66 +0,0 @@
-
-
-
FastAPI implemention of the STAC API spec using PGStac
-
-
-
-
-
-
-
-
-
-
-
-
-
----
-
-**Documentation**: [https://stac-utils.github.io/stac-fastapi/](https://stac-utils.github.io/stac-fastapi/)
-
-**Source Code**: [https://github.com/stac-utils/stac-fastapi](https://github.com/stac-utils/stac-fastapi)
-
----
-
-Stac FastAPI using the [PGStac](https://github.com/stac-utils/pgstac) backend.
-
-[PGStac](https://github.com/stac-utils/pgstac) is a separately managed PostgreSQL database that is designed for enhanced performance to be able to scale Stac FastAPI to be able to efficiently handle hundreds of millions of records. [PGStac](https://github.com/stac-utils/pgstac) automatically includes indexes on Item id, Collection id, Item Geometry, Item Datetime, and an Index for equality checks on any key in Item Properties. Additional indexes may be added to Item Properties to speed up the use of order, <, <=, >, and >= queries.
-
-Stac FastAPI acts as the HTTP interface validating any requests and data that is sent to the [PGStac](https://github.com/stac-utils/pgstac) backend and adds in Link items on data return relative to the service host. All other processing and search is provided directly using PGStac procedural sql / plpgsql functions on the database.
-
-PGStac stores all collection and item records as jsonb fields exactly as they come in allowing for any custom fields to be stored and retrieved transparently.
-
-While the Stac Sort Extension is fully supported, [PGStac](https://github.com/stac-utils/pgstac) is particularly enhanced to be able to sort by datetime (either ascending or descending). Sorting by anything other than datetime (the default if no sort is specified) on very large Stac repositories without very specific query limits (ie selecting a single day date range) will not have the same performance. For more than millions of records it is recommended to either set a low connection timeout on PostgreSQL or to disable use of the Sort Extension.
-
-`stac-fastapi pgstac` was initially added to `stac-fastapi` by [developmentseed](https://github.com/developmentseed).
-
-## Installation
-
-```shell
-git clone https://github.com/stac-utils/stac-fastapi.git
-cd stac-fastapi
-pip install -e \
- stac_fastapi/api[dev] \
- stac_fastapi/types[dev] \
- stac_fastapi/extensions[dev] \
- stac_fastapi/pgstac[dev,server]
-```
-
-### Settings
-
-To configure PGStac stac-fastapi to [hydrate search result items in the API](https://github.com/stac-utils/pgstac#runtime-configurations), set the `USE_API_HYDRATE` environment variable to `true` or explicitly set the option in the PGStac Settings object.
-
-### Migrations
-
-PGStac is an external project and the may be used by multiple front ends.
-For Stac FastAPI development, a docker image (which is pulled as part of the docker-compose) is available at
-bitner/pgstac:[version] that has the full database already set up for PGStac.
-
-There is also a python utility as part of PGStac (pypgstac) that includes a migration utility. The pgstac
-version required by stac-fastapi/pgstac is pinned by using the pinned version of pypgstac in the [setup](setup.py) file.
-
-In order to migrate database versions you can use the migration utility:
-
-```shell
-pypgstac migrate
-```
diff --git a/stac_fastapi/pgstac/pytest.ini b/stac_fastapi/pgstac/pytest.ini
deleted file mode 100644
index 8ce7fc4f9..000000000
--- a/stac_fastapi/pgstac/pytest.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[pytest]
-testpaths = tests
-addopts = -sv
-asyncio_mode = auto
diff --git a/stac_fastapi/pgstac/setup.cfg b/stac_fastapi/pgstac/setup.cfg
deleted file mode 100644
index aea33939d..000000000
--- a/stac_fastapi/pgstac/setup.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[metadata]
-version = attr: stac_fastapi.pgstac.version.__version__
diff --git a/stac_fastapi/pgstac/setup.py b/stac_fastapi/pgstac/setup.py
deleted file mode 100644
index 6632ff0c4..000000000
--- a/stac_fastapi/pgstac/setup.py
+++ /dev/null
@@ -1,65 +0,0 @@
-"""stac_fastapi: pgstac module."""
-
-from setuptools import find_namespace_packages, setup
-
-with open("README.md") as f:
- desc = f.read()
-
-install_requires = [
- "attrs",
- "orjson",
- "pydantic[dotenv]",
- "stac_pydantic==2.0.*",
- "stac-fastapi.types",
- "stac-fastapi.api",
- "stac-fastapi.extensions",
- "asyncpg",
- "buildpg",
- "brotli_asgi",
- "pygeofilter>=0.2",
- "pypgstac==0.7.*",
-]
-
-extra_reqs = {
- "dev": [
- "pypgstac[psycopg]==0.7.*",
- "pytest",
- "pytest-cov",
- "pytest-asyncio>=0.17",
- "pre-commit",
- "requests",
- "httpx",
- ],
- "docs": ["mkdocs", "mkdocs-material", "pdocs"],
- "server": ["uvicorn[standard]==0.19.0"],
- "awslambda": ["mangum"],
-}
-
-
-setup(
- name="stac-fastapi.pgstac",
- description="An implementation of STAC API based on the FastAPI framework and using the pgstac backend.",
- long_description=desc,
- long_description_content_type="text/markdown",
- python_requires=">=3.8",
- classifiers=[
- "Intended Audience :: Developers",
- "Intended Audience :: Information Technology",
- "Intended Audience :: Science/Research",
- "Programming Language :: Python :: 3.8",
- "License :: OSI Approved :: MIT License",
- ],
- keywords="STAC FastAPI COG",
- author="David Bitner",
- author_email="david@developmentseed.org",
- url="https://github.com/stac-utils/stac-fastapi",
- license="MIT",
- packages=find_namespace_packages(exclude=["alembic", "tests", "scripts"]),
- zip_safe=False,
- install_requires=install_requires,
- tests_require=extra_reqs["dev"],
- extras_require=extra_reqs,
- entry_points={
- "console_scripts": ["stac-fastapi-pgstac=stac_fastapi.pgstac.app:run"]
- },
-)
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/__init__.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/__init__.py
deleted file mode 100644
index c2603210e..000000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""stac_fastapi.pgstac.models module."""
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/app.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/app.py
deleted file mode 100644
index 8a33424bb..000000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/app.py
+++ /dev/null
@@ -1,111 +0,0 @@
-"""FastAPI application using PGStac.
-
-Enables the extensions specified as a comma-delimited list in
-the ENABLED_EXTENSIONS environment variable (e.g. `transactions,sort,query`).
-If the variable is not set, enables all extensions.
-"""
-
-import os
-
-from fastapi.responses import ORJSONResponse
-
-from stac_fastapi.api.app import StacApi
-from stac_fastapi.api.models import create_get_request_model, create_post_request_model
-from stac_fastapi.extensions.core import (
- ContextExtension,
- FieldsExtension,
- FilterExtension,
- SortExtension,
- TokenPaginationExtension,
- TransactionExtension,
-)
-from stac_fastapi.extensions.third_party import BulkTransactionExtension
-from stac_fastapi.pgstac.config import Settings
-from stac_fastapi.pgstac.core import CoreCrudClient
-from stac_fastapi.pgstac.db import close_db_connection, connect_to_db
-from stac_fastapi.pgstac.extensions import QueryExtension
-from stac_fastapi.pgstac.extensions.filter import FiltersClient
-from stac_fastapi.pgstac.transactions import BulkTransactionsClient, TransactionsClient
-from stac_fastapi.pgstac.types.search import PgstacSearch
-
-settings = Settings()
-extensions_map = {
- "transaction": TransactionExtension(
- client=TransactionsClient(),
- settings=settings,
- response_class=ORJSONResponse,
- ),
- "query": QueryExtension(),
- "sort": SortExtension(),
- "fields": FieldsExtension(),
- "pagination": TokenPaginationExtension(),
- "context": ContextExtension(),
- "filter": FilterExtension(client=FiltersClient()),
- "bulk_transactions": BulkTransactionExtension(client=BulkTransactionsClient()),
-}
-
-if enabled_extensions := os.getenv("ENABLED_EXTENSIONS"):
- extensions = [
- extensions_map[extension_name]
- for extension_name in enabled_extensions.split(",")
- ]
-else:
- extensions = list(extensions_map.values())
-
-post_request_model = create_post_request_model(extensions, base_model=PgstacSearch)
-
-api = StacApi(
- settings=settings,
- extensions=extensions,
- client=CoreCrudClient(post_request_model=post_request_model),
- response_class=ORJSONResponse,
- search_get_request_model=create_get_request_model(extensions),
- search_post_request_model=post_request_model,
-)
-app = api.app
-
-
-@app.on_event("startup")
-async def startup_event():
- """Connect to database on startup."""
- await connect_to_db(app)
-
-
-@app.on_event("shutdown")
-async def shutdown_event():
- """Close database connection."""
- await close_db_connection(app)
-
-
-def run():
- """Run app from command line using uvicorn if available."""
- try:
- import uvicorn
-
- uvicorn.run(
- "stac_fastapi.pgstac.app:app",
- host=settings.app_host,
- port=settings.app_port,
- log_level="info",
- reload=settings.reload,
- root_path=os.getenv("UVICORN_ROOT_PATH", ""),
- )
- except ImportError:
- raise RuntimeError("Uvicorn must be installed in order to use command")
-
-
-if __name__ == "__main__":
- run()
-
-
-def create_handler(app):
- """Create a handler to use with AWS Lambda if mangum available."""
- try:
- from mangum import Mangum
-
- return Mangum(app)
- except ImportError:
- return None
-
-
-handler = create_handler(app)
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/config.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/config.py
deleted file mode 100644
index 58a1e17f7..000000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/config.py
+++ /dev/null
@@ -1,56 +0,0 @@
-"""Postgres API configuration."""
-
-from typing import Type
-from urllib.parse import quote
-
-from stac_fastapi.pgstac.types.base_item_cache import (
- BaseItemCache,
- DefaultBaseItemCache,
-)
-from stac_fastapi.types.config import ApiSettings
-
-
-class Settings(ApiSettings):
- """Postgres-specific API settings.
-
- Attributes:
- postgres_user: postgres username.
- postgres_pass: postgres password.
- postgres_host_reader: hostname for the reader connection.
- postgres_host_writer: hostname for the writer connection.
- postgres_port: database port.
- postgres_dbname: database name.
- use_api_hydrate: perform hydration of stac items within stac-fastapi.
- """
-
- postgres_user: str
- postgres_pass: str
- postgres_host_reader: str
- postgres_host_writer: str
- postgres_port: str
- postgres_dbname: str
-
- db_min_conn_size: int = 10
- db_max_conn_size: int = 10
- db_max_queries: int = 50000
- db_max_inactive_conn_lifetime: float = 300
-
- use_api_hydrate: bool = False
- base_item_cache: Type[BaseItemCache] = DefaultBaseItemCache
-
- testing: bool = False
-
- @property
- def reader_connection_string(self):
- """Create reader psql connection string."""
- return f"postgresql://{self.postgres_user}:{quote(self.postgres_pass)}@{self.postgres_host_reader}:{self.postgres_port}/{self.postgres_dbname}"
-
- @property
- def writer_connection_string(self):
- """Create writer psql connection string."""
- return f"postgresql://{self.postgres_user}:{quote(self.postgres_pass)}@{self.postgres_host_writer}:{self.postgres_port}/{self.postgres_dbname}"
-
- @property
- def testing_connection_string(self):
- """Create testing psql connection string."""
- return f"postgresql://{self.postgres_user}:{quote(self.postgres_pass)}@{self.postgres_host_writer}:{self.postgres_port}/pgstactestdb"
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/core.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/core.py
deleted file mode 100644
index 568cf0995..000000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/core.py
+++ /dev/null
@@ -1,432 +0,0 @@
-"""Item crud client."""
-import re
-from datetime import datetime
-from typing import Any, Dict, List, Optional, Union
-from urllib.parse import unquote_plus, urljoin
-
-import attr
-import orjson
-from asyncpg.exceptions import InvalidDatetimeFormatError
-from buildpg import render
-from fastapi import HTTPException, Request
-from pydantic import ValidationError
-from pygeofilter.backends.cql2_json import to_cql2
-from pygeofilter.parsers.cql2_text import parse as parse_cql2_text
-from pypgstac.hydration import hydrate
-from stac_pydantic.links import Relations
-from stac_pydantic.shared import MimeTypes
-
-from stac_fastapi.pgstac.config import Settings
-from stac_fastapi.pgstac.models.links import (
- CollectionLinks,
- ItemCollectionLinks,
- ItemLinks,
- PagingLinks,
-)
-from stac_fastapi.pgstac.types.search import PgstacSearch
-from stac_fastapi.pgstac.utils import filter_fields
-from stac_fastapi.types.core import AsyncBaseCoreClient
-from stac_fastapi.types.errors import InvalidQueryParameter, NotFoundError
-from stac_fastapi.types.requests import get_base_url
-from stac_fastapi.types.stac import Collection, Collections, Item, ItemCollection
-
-NumType = Union[float, int]
-
-
-@attr.s
-class CoreCrudClient(AsyncBaseCoreClient):
- """Client for core endpoints defined by stac."""
-
- async def all_collections(self, request: Request, **kwargs) -> Collections:
- """Read all collections from the database."""
- base_url = get_base_url(request)
-
- async with request.app.state.get_connection(request, "r") as conn:
- collections = await conn.fetchval(
- """
- SELECT * FROM all_collections();
- """
- )
- linked_collections: List[Collection] = []
- if collections is not None and len(collections) > 0:
- for c in collections:
- coll = Collection(**c)
- coll["links"] = await CollectionLinks(
- collection_id=coll["id"], request=request
- ).get_links(extra_links=coll.get("links"))
-
- linked_collections.append(coll)
-
- links = [
- {
- "rel": Relations.root.value,
- "type": MimeTypes.json,
- "href": base_url,
- },
- {
- "rel": Relations.parent.value,
- "type": MimeTypes.json,
- "href": base_url,
- },
- {
- "rel": Relations.self.value,
- "type": MimeTypes.json,
- "href": urljoin(base_url, "collections"),
- },
- ]
- collection_list = Collections(collections=linked_collections or [], links=links)
- return collection_list
-
- async def get_collection(
- self, collection_id: str, request: Request, **kwargs
- ) -> Collection:
- """Get collection by id.
-
- Called with `GET /collections/{collection_id}`.
-
- Args:
- collection_id: ID of the collection.
-
- Returns:
- Collection.
- """
- collection: Optional[Dict[str, Any]]
-
- async with request.app.state.get_connection(request, "r") as conn:
- q, p = render(
- """
- SELECT * FROM get_collection(:id::text);
- """,
- id=collection_id,
- )
- collection = await conn.fetchval(q, *p)
- if collection is None:
- raise NotFoundError(f"Collection {collection_id} does not exist.")
-
- collection["links"] = await CollectionLinks(
- collection_id=collection_id, request=request
- ).get_links(extra_links=collection.get("links"))
-
- return Collection(**collection)
-
- async def _get_base_item(
- self, collection_id: str, request: Request
- ) -> Dict[str, Any]:
- """Get the base item of a collection for use in rehydrating full item collection properties.
-
- Args:
- collection: ID of the collection.
-
- Returns:
- Item.
- """
- item: Optional[Dict[str, Any]]
-
- async with request.app.state.get_connection(request, "r") as conn:
- q, p = render(
- """
- SELECT * FROM collection_base_item(:collection_id::text);
- """,
- collection_id=collection_id,
- )
- item = await conn.fetchval(q, *p)
-
- if item is None:
- raise NotFoundError(f"A base item for {collection_id} does not exist.")
-
- return item
-
- async def _search_base(
- self,
- search_request: PgstacSearch,
- request: Request,
- ) -> ItemCollection:
- """Cross catalog search (POST).
-
- Called with `POST /search`.
-
- Args:
- search_request: search request parameters.
-
- Returns:
- ItemCollection containing items which match the search criteria.
- """
- items: Dict[str, Any]
-
- settings: Settings = request.app.state.settings
-
- search_request.conf = search_request.conf or {}
- search_request.conf["nohydrate"] = settings.use_api_hydrate
- search_request_json = search_request.json(exclude_none=True, by_alias=True)
-
- try:
- async with request.app.state.get_connection(request, "r") as conn:
- q, p = render(
- """
- SELECT * FROM search(:req::text::jsonb);
- """,
- req=search_request_json,
- )
- items = await conn.fetchval(q, *p)
- except InvalidDatetimeFormatError:
- raise InvalidQueryParameter(
- f"Datetime parameter {search_request.datetime} is invalid."
- )
-
- next: Optional[str] = items.pop("next", None)
- prev: Optional[str] = items.pop("prev", None)
- collection = ItemCollection(**items)
-
- exclude = search_request.fields.exclude
- if exclude and len(exclude) == 0:
- exclude = None
- include = search_request.fields.include
- if include and len(include) == 0:
- include = None
-
- async def _add_item_links(
- feature: Item,
- collection_id: Optional[str] = None,
- item_id: Optional[str] = None,
- ) -> None:
- """Add ItemLinks to the Item.
-
- If the fields extension is excluding links, then don't add them.
- Also skip links if the item doesn't provide collection and item ids.
- """
- collection_id = feature.get("collection") or collection_id
- item_id = feature.get("id") or item_id
-
- if (
- search_request.fields.exclude is None
- or "links" not in search_request.fields.exclude
- and all([collection_id, item_id])
- ):
- feature["links"] = await ItemLinks(
- collection_id=collection_id,
- item_id=item_id,
- request=request,
- ).get_links(extra_links=feature.get("links"))
-
- cleaned_features: List[Item] = []
-
- if settings.use_api_hydrate:
-
- async def _get_base_item(collection_id: str) -> Dict[str, Any]:
- return await self._get_base_item(collection_id, request)
-
- base_item_cache = settings.base_item_cache(
- fetch_base_item=_get_base_item, request=request
- )
-
- for feature in collection.get("features") or []:
- base_item = await base_item_cache.get(feature.get("collection"))
- feature = hydrate(base_item, feature)
-
- # Grab ids needed for links that may be removed by the fields extension.
- collection_id = feature.get("collection")
- item_id = feature.get("id")
-
- feature = filter_fields(feature, include, exclude)
- await _add_item_links(feature, collection_id, item_id)
-
- cleaned_features.append(feature)
- else:
- for feature in collection.get("features") or []:
- await _add_item_links(feature)
- cleaned_features.append(feature)
-
- collection["features"] = cleaned_features
- collection["links"] = await PagingLinks(
- request=request,
- next=next,
- prev=prev,
- ).get_links()
- return collection
-
- async def item_collection(
- self,
- collection_id: str,
- request: Request,
- bbox: Optional[List[NumType]] = None,
- datetime: Optional[Union[str, datetime]] = None,
- limit: Optional[int] = None,
- token: str = None,
- **kwargs,
- ) -> ItemCollection:
- """Get all items from a specific collection.
-
- Called with `GET /collections/{collection_id}/items`
-
- Args:
- collection_id: id of the collection.
- limit: number of items to return.
- token: pagination token.
-
- Returns:
- An ItemCollection.
- """
- # If collection does not exist, NotFoundError wil be raised
- await self.get_collection(collection_id, request)
-
- base_args = {
- "collections": [collection_id],
- "bbox": bbox,
- "datetime": datetime,
- "limit": limit,
- "token": token,
- }
-
- clean = {}
- for k, v in base_args.items():
- if v is not None and v != []:
- clean[k] = v
-
- search_request = self.post_request_model(
- **clean,
- )
- item_collection = await self._search_base(search_request, request)
- links = await ItemCollectionLinks(
- collection_id=collection_id, request=request
- ).get_links(extra_links=item_collection["links"])
- item_collection["links"] = links
- return item_collection
-
- async def get_item(
- self, item_id: str, collection_id: str, request: Request, **kwargs
- ) -> Item:
- """Get item by id.
-
- Called with `GET /collections/{collection_id}/items/{item_id}`.
-
- Args:
- item_id: ID of the item.
- collection_id: ID of the collection the item is in.
-
- Returns:
- Item.
- """
- # If collection does not exist, NotFoundError wil be raised
- await self.get_collection(collection_id, request)
-
- search_request = self.post_request_model(
- ids=[item_id], collections=[collection_id], limit=1
- )
- item_collection = await self._search_base(search_request, request)
- if not item_collection["features"]:
- raise NotFoundError(
- f"Item {item_id} in Collection {collection_id} does not exist."
- )
-
- return Item(**item_collection["features"][0])
-
- async def post_search(
- self, search_request: PgstacSearch, request: Request, **kwargs
- ) -> ItemCollection:
- """Cross catalog search (POST).
-
- Called with `POST /search`.
-
- Args:
- search_request: search request parameters.
-
- Returns:
- ItemCollection containing items which match the search criteria.
- """
- item_collection = await self._search_base(search_request, request)
- return ItemCollection(**item_collection)
-
- async def get_search(
- self,
- request: Request,
- collections: Optional[List[str]] = None,
- ids: Optional[List[str]] = None,
- bbox: Optional[List[NumType]] = None,
- datetime: Optional[Union[str, datetime]] = None,
- limit: Optional[int] = None,
- query: Optional[str] = None,
- token: Optional[str] = None,
- fields: Optional[List[str]] = None,
- sortby: Optional[str] = None,
- filter: Optional[str] = None,
- filter_lang: Optional[str] = None,
- intersects: Optional[str] = None,
- **kwargs,
- ) -> ItemCollection:
- """Cross catalog search (GET).
-
- Called with `GET /search`.
-
- Returns:
- ItemCollection containing items which match the search criteria.
- """
- query_params = str(request.query_params)
-
- # Kludgy fix because using factory does not allow alias for filter-lang
- if filter_lang is None:
- match = re.search(r"filter-lang=([a-z0-9-]+)", query_params, re.IGNORECASE)
- if match:
- filter_lang = match.group(1)
-
- # Parse request parameters
- base_args = {
- "collections": collections,
- "ids": ids,
- "bbox": bbox,
- "limit": limit,
- "token": token,
- "query": orjson.loads(unquote_plus(query)) if query else query,
- }
-
- if filter:
- if filter_lang == "cql2-text":
- ast = parse_cql2_text(filter)
- base_args["filter"] = orjson.loads(to_cql2(ast))
- base_args["filter-lang"] = "cql2-json"
-
- if datetime:
- base_args["datetime"] = datetime
-
- if intersects:
- base_args["intersects"] = orjson.loads(unquote_plus(intersects))
-
- if sortby:
- # https://github.com/radiantearth/stac-spec/tree/master/api-spec/extensions/sort#http-get-or-post-form
- sort_param = []
- for sort in sortby:
- sortparts = re.match(r"^([+-]?)(.*)$", sort)
- if sortparts:
- sort_param.append(
- {
- "field": sortparts.group(2).strip(),
- "direction": "desc" if sortparts.group(1) == "-" else "asc",
- }
- )
- base_args["sortby"] = sort_param
-
- if fields:
- includes = set()
- excludes = set()
- for field in fields:
- if field[0] == "-":
- excludes.add(field[1:])
- elif field[0] == "+":
- includes.add(field[1:])
- else:
- includes.add(field)
- base_args["fields"] = {"include": includes, "exclude": excludes}
-
- # Remove None values from dict
- clean = {}
- for k, v in base_args.items():
- if v is not None and v != []:
- clean[k] = v
-
- # Do the request
- try:
- search_request = self.post_request_model(**clean)
- except ValidationError as e:
- raise HTTPException(
- status_code=400, detail=f"Invalid parameters provided {e}"
- )
- return await self.post_search(search_request, request=request)
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/db.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/db.py
deleted file mode 100644
index afa15abe7..000000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/db.py
+++ /dev/null
@@ -1,141 +0,0 @@
-"""Database connection handling."""
-
-import json
-from contextlib import asynccontextmanager, contextmanager
-from typing import AsyncIterator, Callable, Dict, Generator, Literal, Union
-
-import attr
-import orjson
-from asyncpg import Connection, exceptions
-from buildpg import V, asyncpg, render
-from fastapi import FastAPI, Request
-
-from stac_fastapi.types.errors import (
- ConflictError,
- DatabaseError,
- ForeignKeyError,
- NotFoundError,
-)
-
-
-async def con_init(conn):
- """Use orjson for json returns."""
- await conn.set_type_codec(
- "json",
- encoder=orjson.dumps,
- decoder=orjson.loads,
- schema="pg_catalog",
- )
- await conn.set_type_codec(
- "jsonb",
- encoder=orjson.dumps,
- decoder=orjson.loads,
- schema="pg_catalog",
- )
-
-
-ConnectionGetter = Callable[[Request, Literal["r", "w"]], AsyncIterator[Connection]]
-
-
-async def connect_to_db(app: FastAPI, get_conn: ConnectionGetter = None) -> None:
- """Create connection pools & connection retriever on application."""
- settings = app.state.settings
- if app.state.settings.testing:
- readpool = writepool = settings.testing_connection_string
- else:
- readpool = settings.reader_connection_string
- writepool = settings.writer_connection_string
- db = DB()
- app.state.readpool = await db.create_pool(readpool, settings)
- app.state.writepool = await db.create_pool(writepool, settings)
- app.state.get_connection = get_conn if get_conn else get_connection
-
-
-async def close_db_connection(app: FastAPI) -> None:
- """Close connection."""
- await app.state.readpool.close()
- await app.state.writepool.close()
-
-
-@asynccontextmanager
-async def get_connection(
- request: Request,
- readwrite: Literal["r", "w"] = "r",
-) -> AsyncIterator[Connection]:
- """Retrieve connection from database conection pool."""
- pool = (
- request.app.state.writepool if readwrite == "w" else request.app.state.readpool
- )
- with translate_pgstac_errors():
- async with pool.acquire() as conn:
- yield conn
-
-
-async def dbfunc(conn: Connection, func: str, arg: Union[str, Dict]):
- """Wrap PLPGSQL Functions.
-
- Keyword arguments:
- pool -- the asyncpg pool to use to connect to the database
- func -- the name of the PostgreSQL function to call
- arg -- the argument to the PostgreSQL function as either a string
- or a dict that will be converted into jsonb
- """
- with translate_pgstac_errors():
- if isinstance(arg, str):
- q, p = render(
- """
- SELECT * FROM :func(:item::text);
- """,
- func=V(func),
- item=arg,
- )
- return await conn.fetchval(q, *p)
- else:
- q, p = render(
- """
- SELECT * FROM :func(:item::text::jsonb);
- """,
- func=V(func),
- item=json.dumps(arg),
- )
- return await conn.fetchval(q, *p)
-
-
-@contextmanager
-def translate_pgstac_errors() -> Generator[None, None, None]:
- """Context manager that translates pgstac errors into FastAPI errors."""
- try:
- yield
- except exceptions.UniqueViolationError as e:
- raise ConflictError from e
- except exceptions.NoDataFoundError as e:
- raise NotFoundError from e
- except exceptions.NotNullViolationError as e:
- raise DatabaseError from e
- except exceptions.ForeignKeyViolationError as e:
- raise ForeignKeyError from e
-
-
-@attr.s
-class DB:
- """DB class that can be used with context manager."""
-
- connection_string = attr.ib(default=None)
- _pool = attr.ib(default=None)
- _connection = attr.ib(default=None)
-
- async def create_pool(self, connection_string: str, settings):
- """Create a connection pool."""
- pool = await asyncpg.create_pool(
- connection_string,
- min_size=settings.db_min_conn_size,
- max_size=settings.db_max_conn_size,
- max_queries=settings.db_max_queries,
- max_inactive_connection_lifetime=settings.db_max_inactive_conn_lifetime,
- init=con_init,
- server_settings={
- "search_path": "pgstac,public",
- "application_name": "pgstac",
- },
- )
- return pool
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/__init__.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/__init__.py
deleted file mode 100644
index 005441794..000000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""pgstac extension customisations."""
-
-from .filter import FiltersClient
-from .query import QueryExtension
-
-__all__ = ["QueryExtension", "FiltersClient"]
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/filter.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/filter.py
deleted file mode 100644
index 38ca8625e..000000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/filter.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""Get Queryables."""
-from typing import Any, Optional
-
-from buildpg import render
-from fastapi import Request
-from fastapi.responses import JSONResponse
-
-from stac_fastapi.types.core import AsyncBaseFiltersClient
-from stac_fastapi.types.errors import NotFoundError
-
-
-class FiltersClient(AsyncBaseFiltersClient):
- """Defines a pattern for implementing the STAC filter extension."""
-
- async def get_queryables(
- self, request: Request, collection_id: Optional[str] = None, **kwargs: Any
- ) -> JSONResponse:
- """Get the queryables available for the given collection_id.
-
- If collection_id is None, returns the intersection of all
- queryables over all collections.
- This base implementation returns a blank queryable schema. This is not allowed
- under OGC CQL but it is allowed by the STAC API Filter Extension
- https://github.com/radiantearth/stac-api-spec/tree/master/fragments/filter#queryables
- """
- pool = request.app.state.readpool
-
- async with pool.acquire() as conn:
- q, p = render(
- """
- SELECT * FROM get_queryables(:collection::text);
- """,
- collection=collection_id,
- )
- queryables = await conn.fetchval(q, *p)
- if not queryables:
- raise NotFoundError(f"Collection {collection_id} not found")
-
- queryables["$id"] = str(request.url)
- headers = {"Content-Type": "application/schema+json"}
- return JSONResponse(queryables, headers=headers)
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/query.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/query.py
deleted file mode 100644
index 91df8539d..000000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/query.py
+++ /dev/null
@@ -1,48 +0,0 @@
-"""Pgstac query customisation."""
-
-import operator
-from enum import auto
-from types import DynamicClassAttribute
-from typing import Any, Callable, Dict, Optional
-
-from pydantic import BaseModel
-from stac_pydantic.utils import AutoValueEnum
-
-from stac_fastapi.extensions.core.query import QueryExtension as QueryExtensionBase
-
-
-class Operator(str, AutoValueEnum):
- """Defines the set of operators supported by the API."""
-
- eq = auto()
- ne = auto()
- lt = auto()
- lte = auto()
- gt = auto()
- gte = auto()
- # TODO: These are defined in the spec but aren't currently implemented by the api
- # startsWith = auto()
- # endsWith = auto()
- # contains = auto()
- # in = auto()
-
- @DynamicClassAttribute
- def operator(self) -> Callable[[Any, Any], bool]:
- """Return python operator."""
- return getattr(operator, self._value_)
-
-
-class QueryExtensionPostRequest(BaseModel):
- """Query Extension POST request model."""
-
- query: Optional[Dict[str, Dict[Operator, Any]]]
-
-
-class QueryExtension(QueryExtensionBase):
- """Query Extension.
-
- Override the POST request model to add validation against
- supported fields
- """
-
- POST = QueryExtensionPostRequest
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/models/__init__.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/models/__init__.py
deleted file mode 100644
index c2603210e..000000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/models/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""stac_fastapi.pgstac.models module."""
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/models/links.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/models/links.py
deleted file mode 100644
index c59891876..000000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/models/links.py
+++ /dev/null
@@ -1,250 +0,0 @@
-"""link helpers."""
-
-from typing import Any, Dict, List, Optional
-from urllib.parse import ParseResult, parse_qs, unquote, urlencode, urljoin, urlparse
-
-import attr
-from stac_pydantic.links import Relations
-from stac_pydantic.shared import MimeTypes
-from starlette.requests import Request
-
-from stac_fastapi.types.requests import get_base_url
-
-# These can be inferred from the item/collection so they aren't included in the database
-# Instead they are dynamically generated when querying the database using the classes defined below
-INFERRED_LINK_RELS = ["self", "item", "parent", "collection", "root"]
-
-
-def filter_links(links: List[Dict]) -> List[Dict]:
- """Remove inferred links."""
- return [link for link in links if link["rel"] not in INFERRED_LINK_RELS]
-
-
-def merge_params(url: str, newparams: Dict) -> str:
- """Merge url parameters."""
- u = urlparse(url)
- params = parse_qs(u.query)
- params.update(newparams)
- param_string = unquote(urlencode(params, True))
-
- href = ParseResult(
- scheme=u.scheme,
- netloc=u.netloc,
- path=u.path,
- params=u.params,
- query=param_string,
- fragment=u.fragment,
- ).geturl()
- return href
-
-
-@attr.s
-class BaseLinks:
- """Create inferred links common to collections and items."""
-
- request: Request = attr.ib()
-
- @property
- def base_url(self):
- """Get the base url."""
- return get_base_url(self.request)
-
- @property
- def url(self):
- """Get the current request url."""
- return str(self.request.url)
-
- def resolve(self, url):
- """Resolve url to the current request url."""
- return urljoin(str(self.base_url), str(url))
-
- def link_self(self) -> Dict:
- """Return the self link."""
- return dict(rel=Relations.self.value, type=MimeTypes.json.value, href=self.url)
-
- def link_root(self) -> Dict:
- """Return the catalog root."""
- return dict(
- rel=Relations.root.value, type=MimeTypes.json.value, href=self.base_url
- )
-
- def create_links(self) -> List[Dict[str, Any]]:
- """Return all inferred links."""
- links = []
- for name in dir(self):
- if name.startswith("link_") and callable(getattr(self, name)):
- link = getattr(self, name)()
- if link is not None:
- links.append(link)
- return links
-
- async def get_links(
- self, extra_links: Optional[List[Dict[str, Any]]] = None
- ) -> List[Dict[str, Any]]:
- """
- Generate all the links.
-
- Get the links object for a stac resource by iterating through
- available methods on this class that start with link_.
- """
- # TODO: Pass request.json() into function so this doesn't need to be coroutine
- if self.request.method == "POST":
- self.request.postbody = await self.request.json()
- # join passed in links with generated links
- # and update relative paths
- links = self.create_links()
-
- if extra_links:
- # For extra links passed in,
- # add links modified with a resolved href.
- # Drop any links that are dynamically
- # determined by the server (e.g. self, parent, etc.)
- # Resolving the href allows for relative paths
- # to be stored in pgstac and for the hrefs in the
- # links of response STAC objects to be resolved
- # to the request url.
- links += [
- {**link, "href": self.resolve(link["href"])}
- for link in extra_links
- if link["rel"] not in INFERRED_LINK_RELS
- ]
-
- return links
-
-
-@attr.s
-class PagingLinks(BaseLinks):
- """Create links for paging."""
-
- next: Optional[str] = attr.ib(kw_only=True, default=None)
- prev: Optional[str] = attr.ib(kw_only=True, default=None)
-
- def link_next(self) -> Optional[Dict[str, Any]]:
- """Create link for next page."""
- if self.next is not None:
- method = self.request.method
- if method == "GET":
- href = merge_params(self.url, {"token": f"next:{self.next}"})
- link = dict(
- rel=Relations.next.value,
- type=MimeTypes.geojson.value,
- method=method,
- href=href,
- )
- return link
- if method == "POST":
- return {
- "rel": Relations.next,
- "type": MimeTypes.geojson,
- "method": method,
- "href": f"{self.request.url}",
- "body": {**self.request.postbody, "token": f"next:{self.next}"},
- }
-
- return None
-
- def link_prev(self) -> Optional[Dict[str, Any]]:
- """Create link for previous page."""
- if self.prev is not None:
- method = self.request.method
- if method == "GET":
- href = merge_params(self.url, {"token": f"prev:{self.prev}"})
- return dict(
- rel=Relations.previous.value,
- type=MimeTypes.geojson.value,
- method=method,
- href=href,
- )
- if method == "POST":
- return {
- "rel": Relations.previous,
- "type": MimeTypes.geojson,
- "method": method,
- "href": f"{self.request.url}",
- "body": {**self.request.postbody, "token": f"prev:{self.prev}"},
- }
- return None
-
-
-@attr.s
-class CollectionLinksBase(BaseLinks):
- """Create inferred links specific to collections."""
-
- collection_id: str = attr.ib()
-
- def collection_link(self, rel: str = Relations.collection.value) -> Dict:
- """Create a link to a collection."""
- return dict(
- rel=rel,
- type=MimeTypes.json.value,
- href=self.resolve(f"collections/{self.collection_id}"),
- )
-
-
-@attr.s
-class CollectionLinks(CollectionLinksBase):
- """Create inferred links specific to collections."""
-
- def link_self(self) -> Dict:
- """Return the self link."""
- return self.collection_link(rel=Relations.self.value)
-
- def link_parent(self) -> Dict:
- """Create the `parent` link."""
- return dict(
- rel=Relations.parent.value,
- type=MimeTypes.json.value,
- href=self.base_url,
- )
-
- def link_items(self) -> Dict:
- """Create the `item` link."""
- return dict(
- rel="items",
- type=MimeTypes.geojson.value,
- href=self.resolve(f"collections/{self.collection_id}/items"),
- )
-
-
-@attr.s
-class ItemCollectionLinks(CollectionLinksBase):
- """Create inferred links specific to collections."""
-
- def link_self(self) -> Dict:
- """Return the self link."""
- return dict(
- rel=Relations.self.value,
- type=MimeTypes.geojson.value,
- href=self.resolve(f"collections/{self.collection_id}/items"),
- )
-
- def link_parent(self) -> Dict:
- """Create the `parent` link."""
- return self.collection_link(rel=Relations.parent.value)
-
- def link_collection(self) -> Dict:
- """Create the `collection` link."""
- return self.collection_link()
-
-
-@attr.s
-class ItemLinks(CollectionLinksBase):
- """Create inferred links specific to items."""
-
- item_id: str = attr.ib()
-
- def link_self(self) -> Dict:
- """Create the self link."""
- return dict(
- rel=Relations.self.value,
- type=MimeTypes.geojson.value,
- href=self.resolve(f"collections/{self.collection_id}/items/{self.item_id}"),
- )
-
- def link_parent(self) -> Dict:
- """Create the `parent` link."""
- return self.collection_link(rel=Relations.parent.value)
-
- def link_collection(self) -> Dict:
- """Create the `collection` link."""
- return self.collection_link()
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/transactions.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/transactions.py
deleted file mode 100644
index 91cb1fee2..000000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/transactions.py
+++ /dev/null
@@ -1,135 +0,0 @@
-"""transactions extension client."""
-
-import logging
-from typing import Optional, Union
-
-import attr
-from buildpg import render
-from fastapi import HTTPException, Request
-from starlette.responses import JSONResponse, Response
-
-from stac_fastapi.extensions.third_party.bulk_transactions import (
- AsyncBaseBulkTransactionsClient,
- Items,
-)
-from stac_fastapi.pgstac.db import dbfunc
-from stac_fastapi.pgstac.models.links import CollectionLinks, ItemLinks
-from stac_fastapi.types import stac as stac_types
-from stac_fastapi.types.core import AsyncBaseTransactionsClient
-
-logger = logging.getLogger("uvicorn")
-logger.setLevel(logging.INFO)
-
-
-@attr.s
-class TransactionsClient(AsyncBaseTransactionsClient):
- """Transactions extension specific CRUD operations."""
-
- async def create_item(
- self, collection_id: str, item: stac_types.Item, request: Request, **kwargs
- ) -> Optional[Union[stac_types.Item, Response]]:
- """Create item."""
- body_collection_id = item.get("collection")
- if body_collection_id is not None and collection_id != body_collection_id:
- raise HTTPException(
- status_code=400,
- detail=f"Collection ID from path parameter ({collection_id}) does not match Collection ID from Item ({body_collection_id})",
- )
- item["collection"] = collection_id
- async with request.app.state.get_connection(request, "w") as conn:
- await dbfunc(conn, "create_item", item)
- item["links"] = await ItemLinks(
- collection_id=collection_id,
- item_id=item["id"],
- request=request,
- ).get_links(extra_links=item.get("links"))
- return stac_types.Item(**item)
-
- async def update_item(
- self,
- request: Request,
- collection_id: str,
- item_id: str,
- item: stac_types.Item,
- **kwargs,
- ) -> Optional[Union[stac_types.Item, Response]]:
- """Update item."""
- body_collection_id = item.get("collection")
- if body_collection_id is not None and collection_id != body_collection_id:
- raise HTTPException(
- status_code=400,
- detail=f"Collection ID from path parameter ({collection_id}) does not match Collection ID from Item ({body_collection_id})",
- )
- item["collection"] = collection_id
- body_item_id = item["id"]
- if body_item_id != item_id:
- raise HTTPException(
- status_code=400,
- detail=f"Item ID from path parameter ({item_id}) does not match Item ID from Item ({body_item_id})",
- )
- async with request.app.state.get_connection(request, "w") as conn:
- await dbfunc(conn, "update_item", item)
- item["links"] = await ItemLinks(
- collection_id=collection_id,
- item_id=item["id"],
- request=request,
- ).get_links(extra_links=item.get("links"))
- return stac_types.Item(**item)
-
- async def create_collection(
- self, collection: stac_types.Collection, request: Request, **kwargs
- ) -> Optional[Union[stac_types.Collection, Response]]:
- """Create collection."""
- async with request.app.state.get_connection(request, "w") as conn:
- await dbfunc(conn, "create_collection", collection)
- collection["links"] = await CollectionLinks(
- collection_id=collection["id"], request=request
- ).get_links(extra_links=collection.get("links"))
-
- return stac_types.Collection(**collection)
-
- async def update_collection(
- self, collection: stac_types.Collection, request: Request, **kwargs
- ) -> Optional[Union[stac_types.Collection, Response]]:
- """Update collection."""
- async with request.app.state.get_connection(request, "w") as conn:
- await dbfunc(conn, "update_collection", collection)
- collection["links"] = await CollectionLinks(
- collection_id=collection["id"], request=request
- ).get_links(extra_links=collection.get("links"))
- return stac_types.Collection(**collection)
-
- async def delete_item(
- self, item_id: str, collection_id: str, request: Request, **kwargs
- ) -> Optional[Union[stac_types.Item, Response]]:
- """Delete item."""
- q, p = render(
- "SELECT * FROM delete_item(:item::text, :collection::text);",
- item=item_id,
- collection=collection_id,
- )
- async with request.app.state.get_connection(request, "w") as conn:
- await conn.fetchval(q, *p)
- return JSONResponse({"deleted item": item_id})
-
- async def delete_collection(
- self, collection_id: str, request: Request, **kwargs
- ) -> Optional[Union[stac_types.Collection, Response]]:
- """Delete collection."""
- async with request.app.state.get_connection(request, "w") as conn:
- await dbfunc(conn, "delete_collection", collection_id)
- return JSONResponse({"deleted collection": collection_id})
-
-
-@attr.s
-class BulkTransactionsClient(AsyncBaseBulkTransactionsClient):
- """Postgres bulk transactions."""
-
- async def bulk_item_insert(self, items: Items, request: Request, **kwargs) -> str:
- """Bulk item insertion using pgstac."""
- items = list(items.items.values())
- async with request.app.state.get_connection(request, "w") as conn:
- await dbfunc(conn, "create_items", items)
-
- return_msg = f"Successfully added {len(items)} items."
- return return_msg
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/types/base_item_cache.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/types/base_item_cache.py
deleted file mode 100644
index 9b92e759d..000000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/types/base_item_cache.py
+++ /dev/null
@@ -1,55 +0,0 @@
-"""base_item_cache classes for pgstac fastapi."""
-import abc
-from typing import Any, Callable, Coroutine, Dict
-
-from starlette.requests import Request
-
-
-class BaseItemCache(abc.ABC):
- """
- A cache that returns a base item for a collection.
-
- If no base item is found in the cache, use the fetch_base_item function
- to fetch the base item from pgstac.
- """
-
- def __init__(
- self,
- fetch_base_item: Callable[[str], Coroutine[Any, Any, Dict[str, Any]]],
- request: Request,
- ):
- """
- Initialize the base item cache.
-
- Args:
- fetch_base_item: A function that fetches the base item for a collection.
- request: The request object containing app state that may be used by caches.
- """
- self._fetch_base_item = fetch_base_item
- self._request = request
-
- @abc.abstractmethod
- async def get(self, collection_id: str) -> Dict[str, Any]:
- """Return the base item for the collection and cache by collection id."""
- pass
-
-
-class DefaultBaseItemCache(BaseItemCache):
- """Implementation of the BaseItemCache that holds base items in a dict."""
-
- def __init__(
- self,
- fetch_base_item: Callable[[str], Coroutine[Any, Any, Dict[str, Any]]],
- request: Request,
- ):
- """Initialize the base item cache."""
- self._base_items = {}
- super().__init__(fetch_base_item, request)
-
- async def get(self, collection_id: str):
- """Return the base item for the collection and cache by collection id."""
- if collection_id not in self._base_items:
- self._base_items[collection_id] = await self._fetch_base_item(
- collection_id,
- )
- return self._base_items[collection_id]
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/types/search.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/types/search.py
deleted file mode 100644
index 2b8abb9da..000000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/types/search.py
+++ /dev/null
@@ -1,26 +0,0 @@
-"""stac_fastapi.types.search module."""
-
-from typing import Dict, Optional
-
-from pydantic import validator
-
-from stac_fastapi.types.search import BaseSearchPostRequest
-
-
-class PgstacSearch(BaseSearchPostRequest):
- """Search model.
-
- Overrides the validation for datetime from the base request model.
- """
-
- conf: Optional[Dict] = None
-
- @validator("filter_lang", pre=False, check_fields=False, always=True)
- def validate_query_uses_cql(cls, v, values):
- """Use of Query Extension is not allowed with cql2."""
- if values.get("query", None) is not None and v != "cql-json":
- raise ValueError(
- "Query extension is not available when using pgstac with cql2"
- )
-
- return v
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/utils.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/utils.py
deleted file mode 100644
index 4a0ce4c72..000000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/utils.py
+++ /dev/null
@@ -1,115 +0,0 @@
-"""stac-fastapi utility methods."""
-from typing import Any, Dict, Optional, Set, Union
-
-from stac_fastapi.types.stac import Item
-
-
-def filter_fields(
- item: Union[Item, Dict[str, Any]],
- include: Optional[Set[str]] = None,
- exclude: Optional[Set[str]] = None,
-) -> Item:
- """Preserve and remove fields as indicated by the fields extension include/exclude sets.
-
- Returns a shallow copy of the Item with the fields filtered.
-
- This will not perform a deep copy; values of the original item will be referenced
- in the return item.
- """
- if not include and not exclude:
- return item
-
- # Build a shallow copy of included fields on an item, or a sub-tree of an item
- def include_fields(
- source: Dict[str, Any], fields: Optional[Set[str]]
- ) -> Dict[str, Any]:
- if not fields:
- return source
-
- clean_item: Dict[str, Any] = {}
- for key_path in fields or []:
- key_path_parts = key_path.split(".")
- key_root = key_path_parts[0]
- if key_root in source:
- if isinstance(source[key_root], dict) and len(key_path_parts) > 1:
- # The root of this key path on the item is a dict, and the
- # key path indicates a sub-key to be included. Walk the dict
- # from the root key and get the full nested value to include.
- value = include_fields(
- source[key_root], fields=set([".".join(key_path_parts[1:])])
- )
-
- if isinstance(clean_item.get(key_root), dict):
- # A previously specified key and sub-keys may have been included
- # already, so do a deep merge update if the root key already exists.
- dict_deep_update(clean_item[key_root], value)
- else:
- # The root key does not exist, so add it. Fields
- # extension only allows nested referencing on dicts, so
- # this won't overwrite anything.
- clean_item[key_root] = value
- else:
- # The item value to include is not a dict, or, it is a dict but the
- # key path is for the whole value, not a sub-key. Include the entire
- # value in the cleaned item.
- clean_item[key_root] = source[key_root]
- else:
- # The key, or root key of a multi-part key, is not present in the item,
- # so it is ignored
- pass
- return clean_item
-
- # For an item built up for included fields, remove excluded fields. This
- # modifies `source` in place.
- def exclude_fields(source: Dict[str, Any], fields: Optional[Set[str]]) -> None:
- for key_path in fields or []:
- key_path_part = key_path.split(".")
- key_root = key_path_part[0]
- if key_root in source:
- if isinstance(source[key_root], dict) and len(key_path_part) > 1:
- # Walk the nested path of this key to remove the leaf-key
- exclude_fields(
- source[key_root], fields=set([".".join(key_path_part[1:])])
- )
- # If, after removing the leaf-key, the root is now an empty
- # dict, remove it entirely
- if not source[key_root]:
- del source[key_root]
- else:
- # The key's value is not a dict, or there is no sub-key to remove. The
- # entire key can be removed from the source.
- source.pop(key_root, None)
- else:
- # The key to remove does not exist on the source, so it is ignored
- pass
-
- # Coalesce incoming type to a dict
- item = dict(item)
-
- clean_item = include_fields(item, include)
-
- # If, after including all the specified fields, there are no included properties,
- # return just id and collection.
- if not clean_item:
- return Item({"id": item.get(id), "collection": item.get("collection")})
-
- exclude_fields(clean_item, exclude)
-
- return Item(**clean_item)
-
-
-def dict_deep_update(merge_to: Dict[str, Any], merge_from: Dict[str, Any]) -> None:
- """Perform a deep update of two dicts.
-
- merge_to is updated in-place with the values from merge_from.
- merge_from values take precedence over existing values in merge_to.
- """
- for k, v in merge_from.items():
- if (
- k in merge_to
- and isinstance(merge_to[k], dict)
- and isinstance(merge_from[k], dict)
- ):
- dict_deep_update(merge_to[k], merge_from[k])
- else:
- merge_to[k] = v
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/version.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/version.py
deleted file mode 100644
index cdb2b2835..000000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/version.py
+++ /dev/null
@@ -1,2 +0,0 @@
-"""library version."""
-__version__ = "2.4.5"
diff --git a/stac_fastapi/pgstac/tests/__init__.py b/stac_fastapi/pgstac/tests/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/stac_fastapi/pgstac/tests/api/__init__.py b/stac_fastapi/pgstac/tests/api/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/stac_fastapi/pgstac/tests/api/test_api.py b/stac_fastapi/pgstac/tests/api/test_api.py
deleted file mode 100644
index ae766eb92..000000000
--- a/stac_fastapi/pgstac/tests/api/test_api.py
+++ /dev/null
@@ -1,624 +0,0 @@
-from datetime import datetime, timedelta
-from typing import Any, Dict, List
-from urllib.parse import quote_plus
-
-import orjson
-import pytest
-from pystac import Collection, Extent, Item, SpatialExtent, TemporalExtent
-
-STAC_CORE_ROUTES = [
- "GET /",
- "GET /collections",
- "GET /collections/{collection_id}",
- "GET /collections/{collection_id}/items",
- "GET /collections/{collection_id}/items/{item_id}",
- "GET /conformance",
- "GET /search",
- "POST /search",
-]
-
-STAC_TRANSACTION_ROUTES = [
- "DELETE /collections/{collection_id}",
- "DELETE /collections/{collection_id}/items/{item_id}",
- "POST /collections",
- "POST /collections/{collection_id}/items",
- "PUT /collections",
- "PUT /collections/{collection_id}/items/{item_id}",
-]
-
-GLOBAL_BBOX = [-180.0, -90.0, 180.0, 90.0]
-GLOBAL_GEOMETRY = {
- "type": "Polygon",
- "coordinates": (
- (
- (180.0, -90.0),
- (180.0, 90.0),
- (-180.0, 90.0),
- (-180.0, -90.0),
- (180.0, -90.0),
- ),
- ),
-}
-DEFAULT_EXTENT = Extent(
- SpatialExtent(GLOBAL_BBOX),
- TemporalExtent([[datetime.now(), None]]),
-)
-
-
-async def test_post_search_content_type(app_client):
- params = {"limit": 1}
- resp = await app_client.post("search", json=params)
- assert resp.headers["content-type"] == "application/geo+json"
-
-
-async def test_get_search_content_type(app_client):
- resp = await app_client.get("search")
- assert resp.headers["content-type"] == "application/geo+json"
-
-
-async def test_get_queryables_content_type(app_client, load_test_collection):
- resp = await app_client.get("queryables")
- assert resp.headers["content-type"] == "application/schema+json"
-
- coll = load_test_collection
- resp = await app_client.get(f"collections/{coll.id}/queryables")
- assert resp.headers["content-type"] == "application/schema+json"
-
-
-async def test_get_features_content_type(app_client, load_test_collection):
- coll = load_test_collection
- resp = await app_client.get(f"collections/{coll.id}/items")
- assert resp.headers["content-type"] == "application/geo+json"
-
-
-async def test_get_features_self_link(app_client, load_test_collection):
- # https://github.com/stac-utils/stac-fastapi/issues/483
- resp = await app_client.get(f"collections/{load_test_collection.id}/items")
- assert resp.status_code == 200
- resp_json = resp.json()
- self_link = next(
- (link for link in resp_json["links"] if link["rel"] == "self"), None
- )
- assert self_link is not None
- assert self_link["href"].endswith("/items")
-
-
-async def test_get_feature_content_type(
- app_client, load_test_collection, load_test_item
-):
- resp = await app_client.get(
- f"collections/{load_test_collection.id}/items/{load_test_item.id}"
- )
- assert resp.headers["content-type"] == "application/geo+json"
-
-
-async def test_api_headers(app_client):
- resp = await app_client.get("/api")
- assert (
- resp.headers["content-type"] == "application/vnd.oai.openapi+json;version=3.0"
- )
- assert resp.status_code == 200
-
-
-async def test_core_router(api_client, app):
- core_routes = set()
- for core_route in STAC_CORE_ROUTES:
- method, path = core_route.split(" ")
- core_routes.add("{} {}".format(method, app.state.router_prefix + path))
-
- api_routes = set(
- [f"{list(route.methods)[0]} {route.path}" for route in api_client.app.routes]
- )
- assert not core_routes - api_routes
-
-
-async def test_landing_page_stac_extensions(app_client):
- resp = await app_client.get("/")
- assert resp.status_code == 200
- resp_json = resp.json()
- assert not resp_json["stac_extensions"]
-
-
-async def test_transactions_router(api_client, app):
- transaction_routes = set()
- for transaction_route in STAC_TRANSACTION_ROUTES:
- method, path = transaction_route.split(" ")
- transaction_routes.add("{} {}".format(method, app.state.router_prefix + path))
-
- api_routes = set(
- [f"{list(route.methods)[0]} {route.path}" for route in api_client.app.routes]
- )
- assert not transaction_routes - api_routes
-
-
-async def test_app_transaction_extension(
- app_client, load_test_data, load_test_collection
-):
- coll = load_test_collection
- item = load_test_data("test_item.json")
- resp = await app_client.post(f"/collections/{coll.id}/items", json=item)
- assert resp.status_code == 200
-
-
-async def test_app_query_extension(load_test_data, app_client, load_test_collection):
- coll = load_test_collection
- item = load_test_data("test_item.json")
- resp = await app_client.post(f"/collections/{coll.id}/items", json=item)
- assert resp.status_code == 200
-
- params = {"query": {"proj:epsg": {"eq": item["properties"]["proj:epsg"]}}}
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
- params["query"] = quote_plus(orjson.dumps(params["query"]))
- resp = await app_client.get("/search", params=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
-
-async def test_app_query_extension_limit_1(
- load_test_data, app_client, load_test_collection
-):
- coll = load_test_collection
- item = load_test_data("test_item.json")
- resp = await app_client.post(f"/collections/{coll.id}/items", json=item)
- assert resp.status_code == 200
-
- params = {"limit": 1}
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
-
-async def test_app_query_extension_limit_eq0(app_client):
- params = {"limit": 0}
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 400
-
-
-async def test_app_query_extension_limit_lt0(
- load_test_data, app_client, load_test_collection
-):
- coll = load_test_collection
- item = load_test_data("test_item.json")
- resp = await app_client.post(f"/collections/{coll.id}/items", json=item)
- assert resp.status_code == 200
-
- params = {"limit": -1}
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 400
-
-
-async def test_app_query_extension_limit_gt10000(
- load_test_data, app_client, load_test_collection
-):
- coll = load_test_collection
- item = load_test_data("test_item.json")
- resp = await app_client.post(f"/collections/{coll.id}/items", json=item)
- assert resp.status_code == 200
-
- params = {"limit": 10001}
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
-
-
-async def test_app_query_extension_gt(load_test_data, app_client, load_test_collection):
- coll = load_test_collection
- item = load_test_data("test_item.json")
- resp = await app_client.post(f"/collections/{coll.id}/items", json=item)
- assert resp.status_code == 200
-
- params = {"query": {"proj:epsg": {"gt": item["properties"]["proj:epsg"]}}}
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 0
-
-
-async def test_app_query_extension_gte(
- load_test_data, app_client, load_test_collection
-):
- coll = load_test_collection
- item = load_test_data("test_item.json")
- resp = await app_client.post(f"/collections/{coll.id}/items", json=item)
- assert resp.status_code == 200
-
- params = {"query": {"proj:epsg": {"gte": item["properties"]["proj:epsg"]}}}
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
-
-async def test_app_sort_extension(load_test_data, app_client, load_test_collection):
- coll = load_test_collection
- first_item = load_test_data("test_item.json")
- item_date = datetime.strptime(
- first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%SZ"
- )
- resp = await app_client.post(f"/collections/{coll.id}/items", json=first_item)
- assert resp.status_code == 200
-
- second_item = load_test_data("test_item.json")
- second_item["id"] = "another-item"
- another_item_date = item_date - timedelta(days=1)
- second_item["properties"]["datetime"] = another_item_date.strftime(
- "%Y-%m-%dT%H:%M:%SZ"
- )
- resp = await app_client.post(f"/collections/{coll.id}/items", json=second_item)
- assert resp.status_code == 200
-
- params = {
- "collections": [coll.id],
- "sortby": [{"field": "datetime", "direction": "desc"}],
- }
-
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert resp_json["features"][0]["id"] == first_item["id"]
- assert resp_json["features"][1]["id"] == second_item["id"]
-
- params = {
- "collections": [coll.id],
- "sortby": [{"field": "datetime", "direction": "asc"}],
- }
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert resp_json["features"][1]["id"] == first_item["id"]
- assert resp_json["features"][0]["id"] == second_item["id"]
-
-
-async def test_search_invalid_date(load_test_data, app_client, load_test_collection):
- coll = load_test_collection
- first_item = load_test_data("test_item.json")
- resp = await app_client.post(f"/collections/{coll.id}/items", json=first_item)
- assert resp.status_code == 200
-
- params = {
- "datetime": "2020-XX-01/2020-10-30",
- "collections": [coll.id],
- }
-
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 400
-
-
-async def test_bbox_3d(load_test_data, app_client, load_test_collection):
- coll = load_test_collection
- first_item = load_test_data("test_item.json")
- resp = await app_client.post(f"/collections/{coll.id}/items", json=first_item)
- assert resp.status_code == 200
-
- australia_bbox = [106.343365, -47.199523, 0.1, 168.218365, -19.437288, 0.1]
- params = {
- "bbox": australia_bbox,
- "collections": [coll.id],
- }
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
-
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
-
-async def test_app_search_response(load_test_data, app_client, load_test_collection):
- coll = load_test_collection
- params = {
- "collections": [coll.id],
- }
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
-
- assert resp_json.get("type") == "FeatureCollection"
- # stac_version and stac_extensions were removed in v1.0.0-beta.3
- assert resp_json.get("stac_version") is None
- assert resp_json.get("stac_extensions") is None
-
-
-async def test_search_point_intersects(
- load_test_data, app_client, load_test_collection
-):
- coll = load_test_collection
- item = load_test_data("test_item.json")
- resp = await app_client.post(f"/collections/{coll.id}/items", json=item)
- assert resp.status_code == 200
-
- new_coordinates = list()
- for coordinate in item["geometry"]["coordinates"][0]:
- new_coordinates.append([coordinate[0] * -1, coordinate[1] * -1])
- item["id"] = "test-item-other-hemispheres"
- item["geometry"]["coordinates"] = [new_coordinates]
- item["bbox"] = list(value * -1 for value in item["bbox"])
- resp = await app_client.post(f"/collections/{coll.id}/items", json=item)
- assert resp.status_code == 200
-
- point = [150.04, -33.14]
- intersects = {"type": "Point", "coordinates": point}
-
- params = {
- "intersects": intersects,
- "collections": [item["collection"]],
- }
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
- params["intersects"] = orjson.dumps(params["intersects"]).decode("utf-8")
- resp = await app_client.get("/search", params=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
-
-async def test_search_line_string_intersects(
- load_test_data, app_client, load_test_collection
-):
- coll = load_test_collection
- item = load_test_data("test_item.json")
- resp = await app_client.post(f"/collections/{coll.id}/items", json=item)
- assert resp.status_code == 200
-
- line = [[150.04, -33.14], [150.22, -33.89]]
- intersects = {"type": "LineString", "coordinates": line}
-
- params = {
- "intersects": intersects,
- "collections": [item["collection"]],
- }
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
-
-@pytest.mark.asyncio
-async def test_landing_forwarded_header(
- load_test_data, app_client, load_test_collection
-):
- coll = load_test_collection
- item = load_test_data("test_item.json")
- await app_client.post(f"/collections/{coll.id}/items", json=item)
- response = (
- await app_client.get(
- "/",
- headers={
- "Forwarded": "proto=https;host=test:1234",
- "X-Forwarded-Proto": "http",
- "X-Forwarded-Port": "4321",
- },
- )
- ).json()
- for link in response["links"]:
- assert link["href"].startswith("https://test:1234/")
-
-
-@pytest.mark.asyncio
-async def test_search_forwarded_header(
- load_test_data, app_client, load_test_collection
-):
- coll = load_test_collection
- item = load_test_data("test_item.json")
- await app_client.post(f"/collections/{coll.id}/items", json=item)
- resp = await app_client.post(
- "/search",
- json={
- "collections": [item["collection"]],
- },
- headers={"Forwarded": "proto=https;host=test:1234"},
- )
- features = resp.json()["features"]
- assert len(features) > 0
- for feature in features:
- for link in feature["links"]:
- assert link["href"].startswith("https://test:1234/")
-
-
-@pytest.mark.asyncio
-async def test_search_x_forwarded_headers(
- load_test_data, app_client, load_test_collection
-):
- coll = load_test_collection
- item = load_test_data("test_item.json")
- await app_client.post(f"/collections/{coll.id}/items", json=item)
- resp = await app_client.post(
- "/search",
- json={
- "collections": [item["collection"]],
- },
- headers={
- "X-Forwarded-Proto": "https",
- "X-Forwarded-Port": "1234",
- },
- )
- features = resp.json()["features"]
- assert len(features) > 0
- for feature in features:
- for link in feature["links"]:
- assert link["href"].startswith("https://test:1234/")
-
-
-@pytest.mark.asyncio
-async def test_search_duplicate_forward_headers(
- load_test_data, app_client, load_test_collection
-):
- coll = load_test_collection
- item = load_test_data("test_item.json")
- await app_client.post(f"/collections/{coll.id}/items", json=item)
- resp = await app_client.post(
- "/search",
- json={
- "collections": [item["collection"]],
- },
- headers={
- "Forwarded": "proto=https;host=test:1234",
- "X-Forwarded-Proto": "http",
- "X-Forwarded-Port": "4321",
- },
- )
- features = resp.json()["features"]
- assert len(features) > 0
- for feature in features:
- for link in feature["links"]:
- assert link["href"].startswith("https://test:1234/")
-
-
-@pytest.mark.asyncio
-async def test_base_queryables(load_test_data, app_client, load_test_collection):
- resp = await app_client.get("/queryables")
- assert resp.headers["Content-Type"] == "application/schema+json"
- q = resp.json()
- assert q["$id"].endswith("/queryables")
- assert q["type"] == "object"
- assert "properties" in q
- assert "id" in q["properties"]
- assert "eo:cloud_cover" in q["properties"]
-
-
-@pytest.mark.asyncio
-async def test_collection_queryables(load_test_data, app_client, load_test_collection):
- resp = await app_client.get("/collections/test-collection/queryables")
- assert resp.headers["Content-Type"] == "application/schema+json"
- q = resp.json()
- assert q["$id"].endswith("/collections/test-collection/queryables")
- assert q["type"] == "object"
- assert "properties" in q
- assert "id" in q["properties"]
- assert "eo:cloud_cover" in q["properties"]
-
-
-@pytest.mark.asyncio
-async def test_item_collection_filter_bbox(
- load_test_data, app_client, load_test_collection
-):
- coll = load_test_collection
- first_item = load_test_data("test_item.json")
- resp = await app_client.post(f"/collections/{coll.id}/items", json=first_item)
- assert resp.status_code == 200
-
- bbox = "100,-50,170,-20"
- resp = await app_client.get(f"/collections/{coll.id}/items", params={"bbox": bbox})
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
- bbox = "1,2,3,4"
- resp = await app_client.get(f"/collections/{coll.id}/items", params={"bbox": bbox})
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 0
-
-
-@pytest.mark.asyncio
-async def test_item_collection_filter_datetime(
- load_test_data, app_client, load_test_collection
-):
- coll = load_test_collection
- first_item = load_test_data("test_item.json")
- resp = await app_client.post(f"/collections/{coll.id}/items", json=first_item)
- assert resp.status_code == 200
-
- datetime_range = "2020-01-01T00:00:00.00Z/.."
- resp = await app_client.get(
- f"/collections/{coll.id}/items", params={"datetime": datetime_range}
- )
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
- datetime_range = "2018-01-01T00:00:00.00Z/2019-01-01T00:00:00.00Z"
- resp = await app_client.get(
- f"/collections/{coll.id}/items", params={"datetime": datetime_range}
- )
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 0
-
-
-@pytest.mark.asyncio
-async def test_bad_collection_queryables(
- load_test_data, app_client, load_test_collection
-):
- resp = await app_client.get("/collections/bad-collection/queryables")
- assert resp.status_code == 404
-
-
-async def test_deleting_items_with_identical_ids(app_client):
- collection_a = Collection("collection-a", "The first collection", DEFAULT_EXTENT)
- collection_b = Collection("collection-b", "The second collection", DEFAULT_EXTENT)
- item = Item("the-item", GLOBAL_GEOMETRY, GLOBAL_BBOX, datetime.now(), {})
-
- for collection in (collection_a, collection_b):
- response = await app_client.post(
- "/collections", json=collection.to_dict(include_self_link=False)
- )
- assert response.status_code == 200
- item_as_dict = item.to_dict(include_self_link=False)
- item_as_dict["collection"] = collection.id
- response = await app_client.post(
- f"/collections/{collection.id}/items", json=item_as_dict
- )
- assert response.status_code == 200
- response = await app_client.get(f"/collections/{collection.id}/items")
- assert response.status_code == 200, response.json()
- assert len(response.json()["features"]) == 1
-
- for collection in (collection_a, collection_b):
- response = await app_client.delete(
- f"/collections/{collection.id}/items/{item.id}"
- )
- assert response.status_code == 200, response.json()
- response = await app_client.get(f"/collections/{collection.id}/items")
- assert response.status_code == 200, response.json()
- assert not response.json()["features"]
-
-
-@pytest.mark.parametrize("direction", ("asc", "desc"))
-async def test_sorting_and_paging(app_client, load_test_collection, direction: str):
- collection_id = load_test_collection.id
- for i in range(10):
- item = Item(
- id=f"item-{i}",
- geometry={"type": "Point", "coordinates": [-105.1019, 40.1672]},
- bbox=[-105.1019, 40.1672, -105.1019, 40.1672],
- datetime=datetime.now(),
- properties={
- "eo:cloud_cover": 42 + i if i % 3 != 0 else None,
- },
- )
- item.collection_id = collection_id
- response = await app_client.post(
- f"/collections/{collection_id}/items",
- json=item.to_dict(include_self_link=False, transform_hrefs=False),
- )
- assert response.status_code == 200
-
- async def search(query: Dict[str, Any]) -> List[Item]:
- items: List[Item] = list()
- while True:
- response = await app_client.post("/search", json=query)
- json = response.json()
- assert response.status_code == 200, json
- items.extend((Item.from_dict(d) for d in json["features"]))
- next_link = next(
- (link for link in json["links"] if link["rel"] == "next"), None
- )
- if next_link is None:
- return items
- else:
- query = next_link["body"]
-
- query = {
- "collections": [collection_id],
- "sortby": [{"field": "properties.eo:cloud_cover", "direction": direction}],
- "limit": 5,
- }
- items = await search(query)
- assert len(items) == 10, items
diff --git a/stac_fastapi/pgstac/tests/clients/__init__.py b/stac_fastapi/pgstac/tests/clients/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/stac_fastapi/pgstac/tests/clients/test_postgres.py b/stac_fastapi/pgstac/tests/clients/test_postgres.py
deleted file mode 100644
index 2d8fd6b52..000000000
--- a/stac_fastapi/pgstac/tests/clients/test_postgres.py
+++ /dev/null
@@ -1,212 +0,0 @@
-import logging
-import uuid
-from contextlib import asynccontextmanager
-from copy import deepcopy
-from typing import Callable, Literal
-
-import pytest
-from fastapi import Request
-from stac_pydantic import Collection, Item
-
-from stac_fastapi.pgstac.db import close_db_connection, connect_to_db, get_connection
-
-# from tests.conftest import MockStarletteRequest
-logger = logging.getLogger(__name__)
-
-
-async def test_create_collection(app_client, load_test_data: Callable):
- in_json = load_test_data("test_collection.json")
- in_coll = Collection.parse_obj(in_json)
- resp = await app_client.post(
- "/collections",
- json=in_json,
- )
- assert resp.status_code == 200
- post_coll = Collection.parse_obj(resp.json())
- assert in_coll.dict(exclude={"links"}) == post_coll.dict(exclude={"links"})
- resp = await app_client.get(f"/collections/{post_coll.id}")
- assert resp.status_code == 200
- get_coll = Collection.parse_obj(resp.json())
- assert post_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"})
-
-
-async def test_update_collection(app_client, load_test_collection):
- in_coll = load_test_collection
- in_coll.keywords.append("newkeyword")
-
- resp = await app_client.put("/collections", json=in_coll.dict())
- assert resp.status_code == 200
-
- resp = await app_client.get(f"/collections/{in_coll.id}")
- assert resp.status_code == 200
-
- get_coll = Collection.parse_obj(resp.json())
- assert in_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"})
- assert "newkeyword" in get_coll.keywords
-
-
-async def test_delete_collection(app_client, load_test_collection):
- in_coll = load_test_collection
-
- resp = await app_client.delete(f"/collections/{in_coll.id}")
- assert resp.status_code == 200
-
- resp = await app_client.get(f"/collections/{in_coll.id}")
- assert resp.status_code == 404
-
-
-async def test_create_item(app_client, load_test_data: Callable, load_test_collection):
- coll = load_test_collection
-
- in_json = load_test_data("test_item.json")
- in_item = Item.parse_obj(in_json)
- resp = await app_client.post(
- f"/collections/{coll.id}/items",
- json=in_json,
- )
- assert resp.status_code == 200
-
- post_item = Item.parse_obj(resp.json())
- assert in_item.dict(exclude={"links"}) == post_item.dict(exclude={"links"})
-
- resp = await app_client.get(f"/collections/{coll.id}/items/{post_item.id}")
-
- assert resp.status_code == 200
-
- get_item = Item.parse_obj(resp.json())
- assert in_item.dict(exclude={"links"}) == get_item.dict(exclude={"links"})
-
-
-async def test_update_item(app_client, load_test_collection, load_test_item):
- coll = load_test_collection
- item = load_test_item
-
- item.properties.description = "Update Test"
-
- resp = await app_client.put(
- f"/collections/{coll.id}/items/{item.id}", content=item.json()
- )
- assert resp.status_code == 200
-
- resp = await app_client.get(f"/collections/{coll.id}/items/{item.id}")
- assert resp.status_code == 200
- get_item = Item.parse_obj(resp.json())
- assert item.dict(exclude={"links"}) == get_item.dict(exclude={"links"})
- assert get_item.properties.description == "Update Test"
-
-
-async def test_delete_item(app_client, load_test_collection, load_test_item):
- coll = load_test_collection
- item = load_test_item
-
- resp = await app_client.delete(f"/collections/{coll.id}/items/{item.id}")
- assert resp.status_code == 200
-
- resp = await app_client.get(f"/collections/{coll.id}/items/{item.id}")
- assert resp.status_code == 404
-
-
-async def test_get_collection_items(app_client, load_test_collection, load_test_item):
- coll = load_test_collection
- item = load_test_item
-
- for _ in range(4):
- item.id = str(uuid.uuid4())
- resp = await app_client.post(
- f"/collections/{coll.id}/items",
- content=item.json(),
- )
- assert resp.status_code == 200
-
- resp = await app_client.get(
- f"/collections/{coll.id}/items",
- )
- assert resp.status_code == 200
- fc = resp.json()
- assert "features" in fc
- assert len(fc["features"]) == 5
-
-
-async def test_create_bulk_items(
- app_client, load_test_data: Callable, load_test_collection
-):
- coll = load_test_collection
- item = load_test_data("test_item.json")
-
- items = {}
- for _ in range(2):
- _item = deepcopy(item)
- _item["id"] = str(uuid.uuid4())
- items[_item["id"]] = _item
-
- payload = {"items": items}
-
- resp = await app_client.post(
- f"/collections/{coll.id}/bulk_items",
- json=payload,
- )
- assert resp.status_code == 200
- assert resp.text == '"Successfully added 2 items."'
-
- for item_id in items.keys():
- resp = await app_client.get(f"/collections/{coll.id}/items/{item_id}")
- assert resp.status_code == 200
-
-
-# TODO since right now puts implement upsert
-# test_create_collection_already_exists
-# test create_item_already_exists
-
-
-# def test_get_collection_items(
-# postgres_core: CoreCrudClient,
-# postgres_transactions: TransactionsClient,
-# load_test_data: Callable,
-# ):
-# coll = Collection.parse_obj(load_test_data("test_collection.json"))
-# postgres_transactions.create_collection(coll, request=MockStarletteRequest)
-
-# item = Item.parse_obj(load_test_data("test_item.json"))
-
-# for _ in range(5):
-# item.id = str(uuid.uuid4())
-# postgres_transactions.create_item(item, request=MockStarletteRequest)
-
-# fc = postgres_core.item_collection(coll.id, request=MockStarletteRequest)
-# assert len(fc.features) == 5
-
-# for item in fc.features:
-# assert item.collection == coll.id
-
-
-@asynccontextmanager
-async def custom_get_connection(
- request: Request,
- readwrite: Literal["r", "w"],
-):
- """An example of customizing the connection getter"""
- async with get_connection(request, readwrite) as conn:
- await conn.execute("SELECT set_config('api.test', 'added-config', false)")
- yield conn
-
-
-class TestDbConnect:
- @pytest.fixture
- async def app(self, api_client):
- """
- app fixture override to setup app with a customized db connection getter
- """
- logger.debug("Customizing app setup")
- await connect_to_db(api_client.app, custom_get_connection)
- yield api_client.app
- await close_db_connection(api_client.app)
-
- async def test_db_setup(self, api_client, app_client):
- @api_client.app.get(f"{api_client.router.prefix}/db-test")
- async def example_view(request: Request):
- async with request.app.state.get_connection(request, "r") as conn:
- return await conn.fetchval("SELECT current_setting('api.test', true)")
-
- response = await app_client.get("/db-test")
- assert response.status_code == 200
- assert response.json() == "added-config"
diff --git a/stac_fastapi/pgstac/tests/conftest.py b/stac_fastapi/pgstac/tests/conftest.py
deleted file mode 100644
index aa1ca6d7d..000000000
--- a/stac_fastapi/pgstac/tests/conftest.py
+++ /dev/null
@@ -1,240 +0,0 @@
-import asyncio
-import json
-import logging
-import os
-import time
-from typing import Callable, Dict
-from urllib.parse import urljoin
-
-import asyncpg
-import pytest
-from fastapi import APIRouter
-from fastapi.responses import ORJSONResponse
-from httpx import AsyncClient
-from pypgstac.db import PgstacDB
-from pypgstac.migrate import Migrate
-from stac_pydantic import Collection, Item
-
-from stac_fastapi.api.app import StacApi
-from stac_fastapi.api.models import create_get_request_model, create_post_request_model
-from stac_fastapi.extensions.core import (
- ContextExtension,
- FieldsExtension,
- FilterExtension,
- SortExtension,
- TokenPaginationExtension,
- TransactionExtension,
-)
-from stac_fastapi.extensions.third_party import BulkTransactionExtension
-from stac_fastapi.pgstac.config import Settings
-from stac_fastapi.pgstac.core import CoreCrudClient
-from stac_fastapi.pgstac.db import close_db_connection, connect_to_db
-from stac_fastapi.pgstac.extensions import QueryExtension
-from stac_fastapi.pgstac.extensions.filter import FiltersClient
-from stac_fastapi.pgstac.transactions import BulkTransactionsClient, TransactionsClient
-from stac_fastapi.pgstac.types.search import PgstacSearch
-
-logger = logging.getLogger(__name__)
-
-DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
-
-settings = Settings(testing=True)
-pgstac_api_hydrate_settings = Settings(testing=True, use_api_hydrate=True)
-
-
-@pytest.fixture(scope="session")
-def event_loop():
- return asyncio.get_event_loop()
-
-
-@pytest.fixture(scope="session")
-async def pg():
- logger.info(f"Connecting to write database {settings.writer_connection_string}")
- os.environ["orig_postgres_dbname"] = settings.postgres_dbname
- conn = await asyncpg.connect(dsn=settings.writer_connection_string)
- try:
- await conn.execute("CREATE DATABASE pgstactestdb;")
- await conn.execute(
- """
- ALTER DATABASE pgstactestdb SET search_path to pgstac, public;
- ALTER DATABASE pgstactestdb SET log_statement to 'all';
- """
- )
- except asyncpg.exceptions.DuplicateDatabaseError:
- await conn.execute("DROP DATABASE pgstactestdb;")
- await conn.execute("CREATE DATABASE pgstactestdb;")
- await conn.execute(
- "ALTER DATABASE pgstactestdb SET search_path to pgstac, public;"
- )
- await conn.close()
- logger.info("migrating...")
- os.environ["postgres_dbname"] = "pgstactestdb"
- conn = await asyncpg.connect(dsn=settings.testing_connection_string)
- await conn.execute("SELECT true")
- await conn.close()
- db = PgstacDB(dsn=settings.testing_connection_string)
- migrator = Migrate(db)
- version = migrator.run_migration()
- db.close()
- logger.info(f"PGStac Migrated to {version}")
-
- yield settings.testing_connection_string
-
- logger.info("Getting rid of test database")
- os.environ["postgres_dbname"] = os.environ["orig_postgres_dbname"]
- conn = await asyncpg.connect(dsn=settings.writer_connection_string)
- try:
- await conn.execute("DROP DATABASE pgstactestdb;")
- await conn.close()
- except Exception:
- try:
- await conn.execute("DROP DATABASE pgstactestdb WITH (force);")
- await conn.close()
- except Exception:
- pass
-
-
-@pytest.fixture(autouse=True)
-async def pgstac(pg):
- logger.info(f"{os.environ['postgres_dbname']}")
- yield
- logger.info("Truncating Data")
- conn = await asyncpg.connect(dsn=settings.testing_connection_string)
- await conn.execute(
- """
- DROP SCHEMA IF EXISTS pgstac CASCADE;
- """
- )
- await conn.close()
- with PgstacDB(dsn=settings.testing_connection_string) as db:
- migrator = Migrate(db)
- version = migrator.run_migration()
- logger.info(f"PGStac Migrated to {version}")
-
-
-# Run all the tests that use the api_client in both db hydrate and api hydrate mode
-@pytest.fixture(
- params=[
- (settings, ""),
- (settings, "/router_prefix"),
- (pgstac_api_hydrate_settings, ""),
- (pgstac_api_hydrate_settings, "/router_prefix"),
- ],
- scope="session",
-)
-def api_client(request, pg):
- api_settings, prefix = request.param
-
- api_settings.openapi_url = prefix + api_settings.openapi_url
- api_settings.docs_url = prefix + api_settings.docs_url
-
- logger.info(
- "creating client with settings, hydrate: {}, router prefix: '{}'".format(
- api_settings.use_api_hydrate, prefix
- )
- )
-
- extensions = [
- TransactionExtension(client=TransactionsClient(), settings=settings),
- QueryExtension(),
- SortExtension(),
- FieldsExtension(),
- TokenPaginationExtension(),
- ContextExtension(),
- FilterExtension(client=FiltersClient()),
- BulkTransactionExtension(client=BulkTransactionsClient()),
- ]
-
- post_request_model = create_post_request_model(extensions, base_model=PgstacSearch)
- api = StacApi(
- settings=api_settings,
- extensions=extensions,
- client=CoreCrudClient(post_request_model=post_request_model),
- search_get_request_model=create_get_request_model(extensions),
- search_post_request_model=post_request_model,
- response_class=ORJSONResponse,
- router=APIRouter(prefix=prefix),
- )
-
- return api
-
-
-@pytest.fixture(scope="function")
-async def app(api_client):
- logger.info("Creating app Fixture")
- time.time()
- app = api_client.app
- await connect_to_db(app)
-
- yield app
-
- await close_db_connection(app)
-
- logger.info("Closed Pools.")
-
-
-@pytest.fixture(scope="function")
-async def app_client(app):
- logger.info("creating app_client")
-
- base_url = "http://test"
- if app.state.router_prefix != "":
- base_url = urljoin(base_url, app.state.router_prefix)
-
- async with AsyncClient(app=app, base_url=base_url) as c:
- yield c
-
-
-@pytest.fixture
-def load_test_data() -> Callable[[str], Dict]:
- def load_file(filename: str) -> Dict:
- with open(os.path.join(DATA_DIR, filename)) as file:
- return json.load(file)
-
- return load_file
-
-
-@pytest.fixture
-async def load_test_collection(app_client, load_test_data):
- data = load_test_data("test_collection.json")
- resp = await app_client.post(
- "/collections",
- json=data,
- )
- assert resp.status_code == 200
- return Collection.parse_obj(resp.json())
-
-
-@pytest.fixture
-async def load_test_item(app_client, load_test_data, load_test_collection):
- coll = load_test_collection
- data = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{coll.id}/items",
- json=data,
- )
- assert resp.status_code == 200
- return Item.parse_obj(resp.json())
-
-
-@pytest.fixture
-async def load_test2_collection(app_client, load_test_data):
- data = load_test_data("test2_collection.json")
- resp = await app_client.post(
- "/collections",
- json=data,
- )
- assert resp.status_code == 200
- return Collection.parse_obj(resp.json())
-
-
-@pytest.fixture
-async def load_test2_item(app_client, load_test_data, load_test2_collection):
- coll = load_test2_collection
- data = load_test_data("test2_item.json")
- resp = await app_client.post(
- f"/collections/{coll.id}/items",
- json=data,
- )
- assert resp.status_code == 200
- return Item.parse_obj(resp.json())
diff --git a/stac_fastapi/pgstac/tests/data/joplin/collection.json b/stac_fastapi/pgstac/tests/data/joplin/collection.json
deleted file mode 100644
index af7681601..000000000
--- a/stac_fastapi/pgstac/tests/data/joplin/collection.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "id": "joplin",
- "description": "This imagery was acquired by the NOAA Remote Sensing Division to support NOAA national security and emergency response requirements. In addition, it will be used for ongoing research efforts for testing and developing standards for airborne digital imagery. Individual images have been combined into a larger mosaic and tiled for distribution. The approximate ground sample distance (GSD) for each pixel is 35 cm (1.14 feet).",
- "stac_version": "1.0.0",
- "license": "public-domain",
- "links": [],
- "type": "collection",
- "extent": {
- "spatial": {
- "bbox": [
- [
- -94.6911621,
- 37.0332547,
- -94.402771,
- 37.1077651
- ]
- ]
- },
- "temporal": {
- "interval": [
- [
- "2000-02-01T00:00:00Z",
- "2000-02-12T00:00:00Z"
- ]
- ]
- }
- }
-}
\ No newline at end of file
diff --git a/stac_fastapi/pgstac/tests/data/joplin/index.geojson b/stac_fastapi/pgstac/tests/data/joplin/index.geojson
deleted file mode 100644
index 1bc8dde5c..000000000
--- a/stac_fastapi/pgstac/tests/data/joplin/index.geojson
+++ /dev/null
@@ -1,1775 +0,0 @@
-{
- "type": "FeatureCollection",
- "features": [
- {
- "id": "f2cca2a3-288b-4518-8a3e-a4492bb60b08",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6884155,
- 37.0595608
- ],
- [
- -94.6884155,
- 37.0332547
- ],
- [
- -94.6554565,
- 37.0332547
- ],
- [
- -94.6554565,
- 37.0595608
- ],
- [
- -94.6884155,
- 37.0595608
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C350000e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6884155,
- 37.0332547,
- -94.6554565,
- 37.0595608
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "a7e125ba-565d-4aa2-bbf3-c57a9087c2e3",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6884155,
- 37.0814756
- ],
- [
- -94.6884155,
- 37.0551771
- ],
- [
- -94.6582031,
- 37.0551771
- ],
- [
- -94.6582031,
- 37.0814756
- ],
- [
- -94.6884155,
- 37.0814756
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C350000e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6884155,
- 37.0551771,
- -94.6582031,
- 37.0814756
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "f7f164c9-cfdf-436d-a3f0-69864c38ba2a",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6911621,
- 37.1033841
- ],
- [
- -94.6911621,
- 37.0770932
- ],
- [
- -94.6582031,
- 37.0770932
- ],
- [
- -94.6582031,
- 37.1033841
- ],
- [
- -94.6911621,
- 37.1033841
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C350000e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6911621,
- 37.0770932,
- -94.6582031,
- 37.1033841
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "ea0fddf4-56f9-4a16-8a0b-f6b0b123b7cf",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6609497,
- 37.0595608
- ],
- [
- -94.6609497,
- 37.0332547
- ],
- [
- -94.6279907,
- 37.0332547
- ],
- [
- -94.6279907,
- 37.0595608
- ],
- [
- -94.6609497,
- 37.0595608
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C352500e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6609497,
- 37.0332547,
- -94.6279907,
- 37.0595608
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "c811e716-ab07-4d80-ac95-6670f8713bc4",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6609497,
- 37.0814756
- ],
- [
- -94.6609497,
- 37.0551771
- ],
- [
- -94.6279907,
- 37.0551771
- ],
- [
- -94.6279907,
- 37.0814756
- ],
- [
- -94.6609497,
- 37.0814756
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C352500e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6609497,
- 37.0551771,
- -94.6279907,
- 37.0814756
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "d4eccfa2-7d77-4624-9e2a-3f59102285bb",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6609497,
- 37.1033841
- ],
- [
- -94.6609497,
- 37.0770932
- ],
- [
- -94.6279907,
- 37.0770932
- ],
- [
- -94.6279907,
- 37.1033841
- ],
- [
- -94.6609497,
- 37.1033841
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C352500e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6609497,
- 37.0770932,
- -94.6279907,
- 37.1033841
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "fe916452-ba6f-4631-9154-c249924a122d",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6334839,
- 37.0595608
- ],
- [
- -94.6334839,
- 37.0332547
- ],
- [
- -94.6005249,
- 37.0332547
- ],
- [
- -94.6005249,
- 37.0595608
- ],
- [
- -94.6334839,
- 37.0595608
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C355000e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6334839,
- 37.0332547,
- -94.6005249,
- 37.0595608
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "85f923a5-a81f-4acd-bc7f-96c7c915f357",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6334839,
- 37.0814756
- ],
- [
- -94.6334839,
- 37.0551771
- ],
- [
- -94.6005249,
- 37.0551771
- ],
- [
- -94.6005249,
- 37.0814756
- ],
- [
- -94.6334839,
- 37.0814756
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C355000e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6334839,
- 37.0551771,
- -94.6005249,
- 37.0814756
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "29c53e17-d7d1-4394-a80f-36763c8f42dc",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6334839,
- 37.1055746
- ],
- [
- -94.6334839,
- 37.0792845
- ],
- [
- -94.6005249,
- 37.0792845
- ],
- [
- -94.6005249,
- 37.1055746
- ],
- [
- -94.6334839,
- 37.1055746
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C355000e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6334839,
- 37.0792845,
- -94.6005249,
- 37.1055746
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "e0a02e4e-aa0c-412e-8f63-6f5344f829df",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6060181,
- 37.0595608
- ],
- [
- -94.6060181,
- 37.0332547
- ],
- [
- -94.5730591,
- 37.0332547
- ],
- [
- -94.5730591,
- 37.0595608
- ],
- [
- -94.6060181,
- 37.0595608
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C357500e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6060181,
- 37.0332547,
- -94.5730591,
- 37.0595608
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "047ab5f0-dce1-4166-a00d-425a3dbefe02",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6060181,
- 37.0814756
- ],
- [
- -94.6060181,
- 37.057369
- ],
- [
- -94.5730591,
- 37.057369
- ],
- [
- -94.5730591,
- 37.0814756
- ],
- [
- -94.6060181,
- 37.0814756
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C357500e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6060181,
- 37.057369,
- -94.5730591,
- 37.0814756
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "57f88dd2-e4e0-48e6-a2b6-7282d4ab8ea4",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6060181,
- 37.1055746
- ],
- [
- -94.6060181,
- 37.0792845
- ],
- [
- -94.5730591,
- 37.0792845
- ],
- [
- -94.5730591,
- 37.1055746
- ],
- [
- -94.6060181,
- 37.1055746
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C357500e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6060181,
- 37.0792845,
- -94.5730591,
- 37.1055746
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "68f2c2b2-4bce-4c40-9a0d-782c1be1f4f2",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.5758057,
- 37.0595608
- ],
- [
- -94.5758057,
- 37.0332547
- ],
- [
- -94.5428467,
- 37.0332547
- ],
- [
- -94.5428467,
- 37.0595608
- ],
- [
- -94.5758057,
- 37.0595608
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C360000e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.5758057,
- 37.0332547,
- -94.5428467,
- 37.0595608
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "d8461d8c-3d2b-4e4e-a931-7ae61ca06dbf",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.5758057,
- 37.0836668
- ],
- [
- -94.5758057,
- 37.057369
- ],
- [
- -94.5455933,
- 37.057369
- ],
- [
- -94.5455933,
- 37.0836668
- ],
- [
- -94.5758057,
- 37.0836668
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C360000e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.5758057,
- 37.057369,
- -94.5455933,
- 37.0836668
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "aeedef30-cbdd-4364-8781-dbb42d148c99",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.5785522,
- 37.1055746
- ],
- [
- -94.5785522,
- 37.0792845
- ],
- [
- -94.5455933,
- 37.0792845
- ],
- [
- -94.5455933,
- 37.1055746
- ],
- [
- -94.5785522,
- 37.1055746
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C360000e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.5785522,
- 37.0792845,
- -94.5455933,
- 37.1055746
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "9ef4279f-386c-40c7-ad71-8de5d9543aa4",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.5483398,
- 37.0595608
- ],
- [
- -94.5483398,
- 37.0354472
- ],
- [
- -94.5153809,
- 37.0354472
- ],
- [
- -94.5153809,
- 37.0595608
- ],
- [
- -94.5483398,
- 37.0595608
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C362500e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.5483398,
- 37.0354472,
- -94.5153809,
- 37.0595608
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "70cc6c05-9fe0-436a-a264-a52515f3f242",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.5483398,
- 37.0836668
- ],
- [
- -94.5483398,
- 37.057369
- ],
- [
- -94.5153809,
- 37.057369
- ],
- [
- -94.5153809,
- 37.0836668
- ],
- [
- -94.5483398,
- 37.0836668
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C362500e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.5483398,
- 37.057369,
- -94.5153809,
- 37.0836668
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "d191a6fd-7881-4421-805c-e246371e5cc4",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.5483398,
- 37.1055746
- ],
- [
- -94.5483398,
- 37.0792845
- ],
- [
- -94.5181274,
- 37.0792845
- ],
- [
- -94.5181274,
- 37.1055746
- ],
- [
- -94.5483398,
- 37.1055746
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C362500e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.5483398,
- 37.0792845,
- -94.5181274,
- 37.1055746
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "d144adde-df4a-45e8-bed9-f085f91486a2",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.520874,
- 37.0617526
- ],
- [
- -94.520874,
- 37.0354472
- ],
- [
- -94.487915,
- 37.0354472
- ],
- [
- -94.487915,
- 37.0617526
- ],
- [
- -94.520874,
- 37.0617526
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C365000e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.520874,
- 37.0354472,
- -94.487915,
- 37.0617526
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "a4c32abd-9791-422b-87ab-b0f3fa36f053",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.520874,
- 37.0836668
- ],
- [
- -94.520874,
- 37.057369
- ],
- [
- -94.487915,
- 37.057369
- ],
- [
- -94.487915,
- 37.0836668
- ],
- [
- -94.520874,
- 37.0836668
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C365000e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.520874,
- 37.057369,
- -94.487915,
- 37.0836668
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "4610c58e-39f4-4d9d-94ba-ceddbf9ac570",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.520874,
- 37.1055746
- ],
- [
- -94.520874,
- 37.0792845
- ],
- [
- -94.487915,
- 37.0792845
- ],
- [
- -94.487915,
- 37.1055746
- ],
- [
- -94.520874,
- 37.1055746
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C365000e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.520874,
- 37.0792845,
- -94.487915,
- 37.1055746
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "145fa700-16d4-4d34-98e0-7540d5c0885f",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.4934082,
- 37.0617526
- ],
- [
- -94.4934082,
- 37.0354472
- ],
- [
- -94.4604492,
- 37.0354472
- ],
- [
- -94.4604492,
- 37.0617526
- ],
- [
- -94.4934082,
- 37.0617526
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C367500e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.4934082,
- 37.0354472,
- -94.4604492,
- 37.0617526
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "a89dc7b8-a580-435b-8176-d8e4386d620c",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.4934082,
- 37.0836668
- ],
- [
- -94.4934082,
- 37.057369
- ],
- [
- -94.4604492,
- 37.057369
- ],
- [
- -94.4604492,
- 37.0836668
- ],
- [
- -94.4934082,
- 37.0836668
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C367500e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.4934082,
- 37.057369,
- -94.4604492,
- 37.0836668
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "386dfa13-c2b4-4ce6-8e6f-fcac73f4e64e",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.4934082,
- 37.1055746
- ],
- [
- -94.4934082,
- 37.0792845
- ],
- [
- -94.4604492,
- 37.0792845
- ],
- [
- -94.4604492,
- 37.1055746
- ],
- [
- -94.4934082,
- 37.1055746
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C367500e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.4934082,
- 37.0792845,
- -94.4604492,
- 37.1055746
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "4d8a8e40-d089-4ca7-92c8-27d810ee07bf",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.4631958,
- 37.0617526
- ],
- [
- -94.4631958,
- 37.0354472
- ],
- [
- -94.4329834,
- 37.0354472
- ],
- [
- -94.4329834,
- 37.0617526
- ],
- [
- -94.4631958,
- 37.0617526
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C370000e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.4631958,
- 37.0354472,
- -94.4329834,
- 37.0617526
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "f734401c-2df0-4694-a353-cdd3ea760cdc",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.4631958,
- 37.0836668
- ],
- [
- -94.4631958,
- 37.057369
- ],
- [
- -94.4329834,
- 37.057369
- ],
- [
- -94.4329834,
- 37.0836668
- ],
- [
- -94.4631958,
- 37.0836668
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C370000e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.4631958,
- 37.057369,
- -94.4329834,
- 37.0836668
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "da6ef938-c58f-4bab-9d4e-89f6ae667da2",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.4659424,
- 37.1077651
- ],
- [
- -94.4659424,
- 37.0814756
- ],
- [
- -94.4329834,
- 37.0814756
- ],
- [
- -94.4329834,
- 37.1077651
- ],
- [
- -94.4659424,
- 37.1077651
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C370000e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.4659424,
- 37.0814756,
- -94.4329834,
- 37.1077651
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "ad420ced-b005-472b-a6df-3838c2b74504",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.43573,
- 37.0617526
- ],
- [
- -94.43573,
- 37.0354472
- ],
- [
- -94.402771,
- 37.0354472
- ],
- [
- -94.402771,
- 37.0617526
- ],
- [
- -94.43573,
- 37.0617526
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C372500e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.43573,
- 37.0354472,
- -94.402771,
- 37.0617526
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "f490b7af-0019-45e2-854b-3854d07fd063",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.43573,
- 37.0836668
- ],
- [
- -94.43573,
- 37.0595608
- ],
- [
- -94.402771,
- 37.0595608
- ],
- [
- -94.402771,
- 37.0836668
- ],
- [
- -94.43573,
- 37.0836668
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C372500e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.43573,
- 37.0595608,
- -94.402771,
- 37.0836668
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "b853f353-4b72-44d5-aa44-c07dfd307138",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.43573,
- 37.1077651
- ],
- [
- -94.43573,
- 37.0814756
- ],
- [
- -94.4055176,
- 37.0814756
- ],
- [
- -94.4055176,
- 37.1077651
- ],
- [
- -94.43573,
- 37.1077651
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C372500e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.43573,
- 37.0814756,
- -94.4055176,
- 37.1077651
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- }
- ]
-}
\ No newline at end of file
diff --git a/stac_fastapi/pgstac/tests/data/test2_collection.json b/stac_fastapi/pgstac/tests/data/test2_collection.json
deleted file mode 100644
index 32502a360..000000000
--- a/stac_fastapi/pgstac/tests/data/test2_collection.json
+++ /dev/null
@@ -1,271 +0,0 @@
-{
- "id": "test2-collection",
- "type": "Collection",
- "links": [
- {
- "rel": "items",
- "type": "application/geo+json",
- "href": "https://pct-apis-staging.westeurope.cloudapp.azure.com/stac/collections/landsat-c2-l1/items"
- },
- {
- "rel": "parent",
- "type": "application/json",
- "href": "https://pct-apis-staging.westeurope.cloudapp.azure.com/stac/"
- },
- {
- "rel": "root",
- "type": "application/json",
- "href": "https://pct-apis-staging.westeurope.cloudapp.azure.com/stac/"
- },
- {
- "rel": "self",
- "type": "application/json",
- "href": "https://pct-apis-staging.westeurope.cloudapp.azure.com/stac/collections/landsat-c2-l1"
- },
- {
- "rel": "cite-as",
- "href": "https://doi.org/10.5066/P9AF14YV",
- "title": "Landsat 1-5 MSS Collection 2 Level-1"
- },
- {
- "rel": "license",
- "href": "https://www.usgs.gov/core-science-systems/hdds/data-policy",
- "title": "Public Domain"
- },
- {
- "rel": "describedby",
- "href": "https://planetarycomputer.microsoft.com/dataset/landsat-c2-l1",
- "title": "Human readable dataset overview and reference",
- "type": "text/html"
- }
- ],
- "title": "Landsat Collection 2 Level-1",
- "assets": {
- "thumbnail": {
- "href": "https://ai4edatasetspublicassets.blob.core.windows.net/assets/pc_thumbnails/landsat-c2-l1-thumb.png",
- "type": "image/png",
- "roles": ["thumbnail"],
- "title": "Landsat Collection 2 Level-1 thumbnail"
- }
- },
- "extent": {
- "spatial": {
- "bbox": [[-180, -90, 180, 90]]
- },
- "temporal": {
- "interval": [["1972-07-25T00:00:00Z", "2013-01-07T23:23:59Z"]]
- }
- },
- "license": "proprietary",
- "keywords": ["Landsat", "USGS", "NASA", "Satellite", "Global", "Imagery"],
- "providers": [
- {
- "url": "https://landsat.gsfc.nasa.gov/",
- "name": "NASA",
- "roles": ["producer", "licensor"]
- },
- {
- "url": "https://www.usgs.gov/landsat-missions/landsat-collection-2-level-1-data",
- "name": "USGS",
- "roles": ["producer", "processor", "licensor"]
- },
- {
- "url": "https://planetarycomputer.microsoft.com",
- "name": "Microsoft",
- "roles": ["host"]
- }
- ],
- "summaries": {
- "gsd": [79],
- "sci:doi": ["10.5066/P9AF14YV"],
- "eo:bands": [
- {
- "name": "B4",
- "common_name": "green",
- "description": "Visible green (Landsat 1-3 Band B4)",
- "center_wavelength": 0.55,
- "full_width_half_max": 0.1
- },
- {
- "name": "B5",
- "common_name": "red",
- "description": "Visible red (Landsat 1-3 Band B5)",
- "center_wavelength": 0.65,
- "full_width_half_max": 0.1
- },
- {
- "name": "B6",
- "common_name": "nir08",
- "description": "Near infrared (Landsat 1-3 Band B6)",
- "center_wavelength": 0.75,
- "full_width_half_max": 0.1
- },
- {
- "name": "B7",
- "common_name": "nir09",
- "description": "Near infrared (Landsat 1-3 Band B7)",
- "center_wavelength": 0.95,
- "full_width_half_max": 0.3
- },
- {
- "name": "B1",
- "common_name": "green",
- "description": "Visible green (Landsat 4-5 Band B1)",
- "center_wavelength": 0.55,
- "full_width_half_max": 0.1
- },
- {
- "name": "B2",
- "common_name": "red",
- "description": "Visible red (Landsat 4-5 Band B2)",
- "center_wavelength": 0.65,
- "full_width_half_max": 0.1
- },
- {
- "name": "B3",
- "common_name": "nir08",
- "description": "Near infrared (Landsat 4-5 Band B3)",
- "center_wavelength": 0.75,
- "full_width_half_max": 0.1
- },
- {
- "name": "B4",
- "common_name": "nir09",
- "description": "Near infrared (Landsat 4-5 Band B4)",
- "center_wavelength": 0.95,
- "full_width_half_max": 0.3
- }
- ],
- "platform": [
- "landsat-1",
- "landsat-2",
- "landsat-3",
- "landsat-4",
- "landsat-5"
- ],
- "instruments": ["mss"],
- "view:off_nadir": [0]
- },
- "description": "Landsat Collection 2 Level-1 data, consisting of quantized and calibrated scaled Digital Numbers (DN) representing the multispectral image data. These [Level-1](https://www.usgs.gov/landsat-missions/landsat-collection-2-level-1-data) data can be [rescaled](https://www.usgs.gov/landsat-missions/using-usgs-landsat-level-1-data-product) to top of atmosphere (TOA) reflectance and/or radiance. Thermal band data can be rescaled to TOA brightness temperature.\\n\\nThis dataset represents the global archive of Level-1 data from [Landsat Collection 2](https://www.usgs.gov/core-science-systems/nli/landsat/landsat-collection-2) acquired by the [Multispectral Scanner System](https://landsat.gsfc.nasa.gov/multispectral-scanner-system/) onboard Landsat 1 through Landsat 5 from July 7, 1972 to January 7, 2013. Images are stored in [cloud-optimized GeoTIFF](https://www.cogeo.org/) format.\\n",
- "item_assets": {
- "red": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "roles": ["data"],
- "title": "Red Band",
- "description": "Collection 2 Level-1 Red Band Top of Atmosphere Radiance",
- "raster:bands": [
- {
- "unit": "watt/steradian/square_meter/micrometer",
- "nodata": 0,
- "data_type": "uint8",
- "spatial_resolution": 60
- }
- ]
- },
- "green": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "roles": ["data"],
- "title": "Green Band",
- "description": "Collection 2 Level-1 Green Band Top of Atmosphere Radiance",
- "raster:bands": [
- {
- "unit": "watt/steradian/square_meter/micrometer",
- "nodata": 0,
- "data_type": "uint8",
- "spatial_resolution": 60
- }
- ]
- },
- "nir08": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "roles": ["data"],
- "title": "Near Infrared Band 0.8",
- "description": "Collection 2 Level-1 Near Infrared Band 0.8 Top of Atmosphere Radiance",
- "raster:bands": [
- {
- "unit": "watt/steradian/square_meter/micrometer",
- "nodata": 0,
- "data_type": "uint8",
- "spatial_resolution": 60
- }
- ]
- },
- "nir09": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "roles": ["data"],
- "title": "Near Infrared Band 0.9",
- "description": "Collection 2 Level-1 Near Infrared Band 0.9 Top of Atmosphere Radiance",
- "raster:bands": [
- {
- "unit": "watt/steradian/square_meter/micrometer",
- "nodata": 0,
- "data_type": "uint8",
- "spatial_resolution": 60
- }
- ]
- },
- "mtl.txt": {
- "type": "text/plain",
- "roles": ["metadata"],
- "title": "Product Metadata File (txt)",
- "description": "Collection 2 Level-1 Product Metadata File (txt)"
- },
- "mtl.xml": {
- "type": "application/xml",
- "roles": ["metadata"],
- "title": "Product Metadata File (xml)",
- "description": "Collection 2 Level-1 Product Metadata File (xml)"
- },
- "mtl.json": {
- "type": "application/json",
- "roles": ["metadata"],
- "title": "Product Metadata File (json)",
- "description": "Collection 2 Level-1 Product Metadata File (json)"
- },
- "qa_pixel": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "roles": ["cloud"],
- "title": "Pixel Quality Assessment Band",
- "description": "Collection 2 Level-1 Pixel Quality Assessment Band",
- "raster:bands": [
- {
- "unit": "bit index",
- "nodata": 1,
- "data_type": "uint16",
- "spatial_resolution": 60
- }
- ]
- },
- "qa_radsat": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "roles": ["saturation"],
- "title": "Radiometric Saturation and Dropped Pixel Quality Assessment Band",
- "description": "Collection 2 Level-1 Radiometric Saturation and Dropped Pixel Quality Assessment Band",
- "raster:bands": [
- {
- "unit": "bit index",
- "data_type": "uint16",
- "spatial_resolution": 60
- }
- ]
- },
- "thumbnail": {
- "type": "image/jpeg",
- "roles": ["thumbnail"],
- "title": "Thumbnail image"
- },
- "reduced_resolution_browse": {
- "type": "image/jpeg",
- "roles": ["overview"],
- "title": "Reduced resolution browse image"
- }
- },
- "stac_version": "1.0.0",
- "stac_extensions": [
- "https://stac-extensions.github.io/item-assets/v1.0.0/schema.json",
- "https://stac-extensions.github.io/view/v1.0.0/schema.json",
- "https://stac-extensions.github.io/scientific/v1.0.0/schema.json",
- "https://stac-extensions.github.io/raster/v1.0.0/schema.json",
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json"
- ]
-}
diff --git a/stac_fastapi/pgstac/tests/data/test2_item.json b/stac_fastapi/pgstac/tests/data/test2_item.json
deleted file mode 100644
index 62fa2521a..000000000
--- a/stac_fastapi/pgstac/tests/data/test2_item.json
+++ /dev/null
@@ -1,258 +0,0 @@
-{
- "id": "test2-item",
- "bbox": [-84.7340712, 30.8344014, -82.3892149, 32.6891482],
- "type": "Feature",
- "links": [
- {
- "rel": "collection",
- "type": "application/json",
- "href": "https://pct-apis-staging.westeurope.cloudapp.azure.com/stac/collections/landsat-c2-l1"
- },
- {
- "rel": "parent",
- "type": "application/json",
- "href": "https://pct-apis-staging.westeurope.cloudapp.azure.com/stac/collections/landsat-c2-l1"
- },
- {
- "rel": "root",
- "type": "application/json",
- "href": "https://pct-apis-staging.westeurope.cloudapp.azure.com/stac/"
- },
- {
- "rel": "self",
- "type": "application/geo+json",
- "href": "https://pct-apis-staging.westeurope.cloudapp.azure.com/stac/collections/landsat-c2-l1/items/LM05_L1GS_018038_19901223_02_T2"
- },
- {
- "rel": "cite-as",
- "href": "https://doi.org/10.5066/P9AF14YV",
- "title": "Landsat 1-5 MSS Collection 2 Level-1"
- },
- {
- "rel": "via",
- "href": "https://landsatlook.usgs.gov/stac-server/collections/landsat-c2l1/items/LM05_L1GS_018038_19901223_20200827_02_T2",
- "type": "application/json",
- "title": "USGS STAC Item"
- },
- {
- "rel": "preview",
- "href": "https://pct-apis-staging.westeurope.cloudapp.azure.com/data/item/map?collection=landsat-c2-l1&item=LM05_L1GS_018038_19901223_02_T2",
- "title": "Map of item",
- "type": "text/html"
- }
- ],
- "assets": {
- "red": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-1/standard/mss/1990/018/038/LM05_L1GS_018038_19901223_20200827_02_T2/LM05_L1GS_018038_19901223_20200827_02_T2_B2.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "roles": ["data"],
- "title": "Red Band (B2)",
- "eo:bands": [
- {
- "name": "B2",
- "common_name": "red",
- "description": "Landsat 4-5 Band B2",
- "center_wavelength": 0.65,
- "full_width_half_max": 0.1
- }
- ],
- "description": "Collection 2 Level-1 Red Band Top of Atmosphere Radiance",
- "raster:bands": [
- {
- "unit": "watt/steradian/square_meter/micrometer",
- "scale": 0.66024,
- "nodata": 0,
- "offset": 2.03976,
- "data_type": "uint8",
- "spatial_resolution": 60
- }
- ]
- },
- "green": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-1/standard/mss/1990/018/038/LM05_L1GS_018038_19901223_20200827_02_T2/LM05_L1GS_018038_19901223_20200827_02_T2_B1.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "roles": ["data"],
- "title": "Green Band (B1)",
- "eo:bands": [
- {
- "name": "B1",
- "common_name": "green",
- "description": "Landsat 4-5 Band B1",
- "center_wavelength": 0.55,
- "full_width_half_max": 0.1
- }
- ],
- "description": "Collection 2 Level-1 Green Band Top of Atmosphere Radiance",
- "raster:bands": [
- {
- "unit": "watt/steradian/square_meter/micrometer",
- "scale": 0.88504,
- "nodata": 0,
- "offset": 1.51496,
- "data_type": "uint8",
- "spatial_resolution": 60
- }
- ]
- },
- "nir08": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-1/standard/mss/1990/018/038/LM05_L1GS_018038_19901223_20200827_02_T2/LM05_L1GS_018038_19901223_20200827_02_T2_B3.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "roles": ["data"],
- "title": "Near Infrared Band 0.8 (B3)",
- "eo:bands": [
- {
- "name": "B3",
- "common_name": "nir08",
- "description": "Landsat 4-5 Band B3",
- "center_wavelength": 0.75,
- "full_width_half_max": 0.1
- }
- ],
- "description": "Collection 2 Level-1 Near Infrared Band 0.7 Top of Atmosphere Radiance",
- "raster:bands": [
- {
- "unit": "watt/steradian/square_meter/micrometer",
- "scale": 0.55866,
- "nodata": 0,
- "offset": 4.34134,
- "data_type": "uint8",
- "spatial_resolution": 60
- }
- ]
- },
- "nir09": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-1/standard/mss/1990/018/038/LM05_L1GS_018038_19901223_20200827_02_T2/LM05_L1GS_018038_19901223_20200827_02_T2_B4.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "roles": ["data"],
- "title": "Near Infrared Band 0.9 (B4)",
- "eo:bands": [
- {
- "name": "B4",
- "common_name": "nir09",
- "description": "Landsat 4-5 Band B4",
- "center_wavelength": 0.95,
- "full_width_half_max": 0.3
- }
- ],
- "description": "Collection 2 Level-1 Near Infrared Band 0.9 Top of Atmosphere Radiance",
- "raster:bands": [
- {
- "unit": "watt/steradian/square_meter/micrometer",
- "scale": 0.46654,
- "nodata": 0,
- "offset": 1.03346,
- "data_type": "uint8",
- "spatial_resolution": 60
- }
- ]
- },
- "mtl.txt": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-1/standard/mss/1990/018/038/LM05_L1GS_018038_19901223_20200827_02_T2/LM05_L1GS_018038_19901223_20200827_02_T2_MTL.txt",
- "type": "text/plain",
- "roles": ["metadata"],
- "title": "Product Metadata File (txt)",
- "description": "Collection 2 Level-1 Product Metadata File (txt)"
- },
- "mtl.xml": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-1/standard/mss/1990/018/038/LM05_L1GS_018038_19901223_20200827_02_T2/LM05_L1GS_018038_19901223_20200827_02_T2_MTL.xml",
- "type": "application/xml",
- "roles": ["metadata"],
- "title": "Product Metadata File (xml)",
- "description": "Collection 2 Level-1 Product Metadata File (xml)"
- },
- "mtl.json": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-1/standard/mss/1990/018/038/LM05_L1GS_018038_19901223_20200827_02_T2/LM05_L1GS_018038_19901223_20200827_02_T2_MTL.json",
- "type": "application/json",
- "roles": ["metadata"],
- "title": "Product Metadata File (json)",
- "description": "Collection 2 Level-1 Product Metadata File (json)"
- },
- "qa_pixel": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-1/standard/mss/1990/018/038/LM05_L1GS_018038_19901223_20200827_02_T2/LM05_L1GS_018038_19901223_20200827_02_T2_QA_PIXEL.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "roles": ["cloud"],
- "title": "Pixel Quality Assessment Band (QA_PIXEL)",
- "description": "Collection 2 Level-1 Pixel Quality Assessment Band",
- "raster:bands": [
- {
- "unit": "bit index",
- "nodata": 1,
- "data_type": "uint16",
- "spatial_resolution": 60
- }
- ]
- },
- "qa_radsat": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-1/standard/mss/1990/018/038/LM05_L1GS_018038_19901223_20200827_02_T2/LM05_L1GS_018038_19901223_20200827_02_T2_QA_RADSAT.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "roles": ["saturation"],
- "title": "Radiometric Saturation and Dropped Pixel Quality Assessment Band (QA_RADSAT)",
- "description": "Collection 2 Level-1 Radiometric Saturation and Dropped Pixel Quality Assessment Band",
- "raster:bands": [
- {
- "unit": "bit index",
- "data_type": "uint16",
- "spatial_resolution": 60
- }
- ]
- },
- "thumbnail": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-1/standard/mss/1990/018/038/LM05_L1GS_018038_19901223_20200827_02_T2/LM05_L1GS_018038_19901223_20200827_02_T2_thumb_small.jpeg",
- "type": "image/jpeg",
- "roles": ["thumbnail"],
- "title": "Thumbnail image"
- },
- "reduced_resolution_browse": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-1/standard/mss/1990/018/038/LM05_L1GS_018038_19901223_20200827_02_T2/LM05_L1GS_018038_19901223_20200827_02_T2_thumb_large.jpeg",
- "type": "image/jpeg",
- "roles": ["overview"],
- "title": "Reduced resolution browse image"
- }
- },
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [-84.3264316, 32.6891482],
- [-84.7340712, 31.1114869],
- [-82.8283452, 30.8344014],
- [-82.3892149, 32.4079117],
- [-84.3264316, 32.6891482]
- ]
- ]
- },
- "collection": "test2-collection",
- "properties": {
- "gsd": 79,
- "created": "2022-03-31T16:51:57.476085Z",
- "sci:doi": "10.5066/P9AF14YV",
- "datetime": "1990-12-23T15:26:35.581000Z",
- "platform": "landsat-5",
- "proj:epsg": 32617,
- "proj:shape": [3525, 3946],
- "description": "Landsat Collection 2 Level-1",
- "instruments": ["mss"],
- "eo:cloud_cover": 23,
- "proj:transform": [60, 0, 140790, 0, -60, 3622110],
- "view:off_nadir": 0,
- "landsat:wrs_row": "038",
- "landsat:scene_id": "LM50180381990357AAA03",
- "landsat:wrs_path": "018",
- "landsat:wrs_type": "2",
- "view:sun_azimuth": 147.23255058,
- "landsat:correction": "L1GS",
- "view:sun_elevation": 27.04507311,
- "landsat:cloud_cover_land": 28,
- "landsat:collection_number": "02",
- "landsat:collection_category": "T2"
- },
- "stac_version": "1.0.0",
- "stac_extensions": [
- "https://stac-extensions.github.io/raster/v1.0.0/schema.json",
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/view/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json",
- "https://landsat.usgs.gov/stac/landsat-extension/v1.1.1/schema.json",
- "https://stac-extensions.github.io/scientific/v1.0.0/schema.json"
- ]
-}
diff --git a/stac_fastapi/pgstac/tests/data/test_collection.json b/stac_fastapi/pgstac/tests/data/test_collection.json
deleted file mode 100644
index 6a6395f1e..000000000
--- a/stac_fastapi/pgstac/tests/data/test_collection.json
+++ /dev/null
@@ -1,152 +0,0 @@
-{
- "id": "test-collection",
- "stac_extensions": [],
- "type": "Collection",
- "description": "Landat 8 imagery radiometrically calibrated and orthorectified using gound points and Digital Elevation Model (DEM) data to correct relief displacement.",
- "stac_version": "1.0.0",
- "license": "PDDL-1.0",
- "summaries": {
- "platform": ["landsat-8"],
- "instruments": ["oli", "tirs"],
- "gsd": [30],
- "eo:bands": [
- {
- "name": "B1",
- "common_name": "coastal",
- "center_wavelength": 0.44,
- "full_width_half_max": 0.02
- },
- {
- "name": "B2",
- "common_name": "blue",
- "center_wavelength": 0.48,
- "full_width_half_max": 0.06
- },
- {
- "name": "B3",
- "common_name": "green",
- "center_wavelength": 0.56,
- "full_width_half_max": 0.06
- },
- {
- "name": "B4",
- "common_name": "red",
- "center_wavelength": 0.65,
- "full_width_half_max": 0.04
- },
- {
- "name": "B5",
- "common_name": "nir",
- "center_wavelength": 0.86,
- "full_width_half_max": 0.03
- },
- {
- "name": "B6",
- "common_name": "swir16",
- "center_wavelength": 1.6,
- "full_width_half_max": 0.08
- },
- {
- "name": "B7",
- "common_name": "swir22",
- "center_wavelength": 2.2,
- "full_width_half_max": 0.2
- },
- {
- "name": "B8",
- "common_name": "pan",
- "center_wavelength": 0.59,
- "full_width_half_max": 0.18
- },
- {
- "name": "B9",
- "common_name": "cirrus",
- "center_wavelength": 1.37,
- "full_width_half_max": 0.02
- },
- {
- "name": "B10",
- "common_name": "lwir11",
- "center_wavelength": 10.9,
- "full_width_half_max": 0.8
- },
- {
- "name": "B11",
- "common_name": "lwir12",
- "center_wavelength": 12,
- "full_width_half_max": 1
- }
- ]
- },
- "extent": {
- "spatial": {
- "bbox": [
- [
- -180.0,
- -90.0,
- 180.0,
- 90.0
- ]
- ]
- },
- "temporal": {
- "interval": [
- [
- "2013-06-01",
- null
- ]
- ]
- }
- },
- "links": [
- {
- "rel": "license",
- "href": "https://creativecommons.org/licenses/publicdomain/",
- "title": "public domain"
- }
- ],
- "title": "Landsat 8 L1",
- "keywords": [
- "landsat",
- "earth observation",
- "usgs"
- ],
- "providers": [
- {
- "name": "USGS",
- "roles": [
- "producer"
- ],
- "url": "https://landsat.usgs.gov/"
- },
- {
- "name": "Planet Labs",
- "roles": [
- "processor"
- ],
- "url": "https://github.com/landsat-pds/landsat_ingestor"
- },
- {
- "name": "AWS",
- "roles": [
- "host"
- ],
- "url": "https://landsatonaws.com/"
- },
- {
- "name": "Development Seed",
- "roles": [
- "processor"
- ],
- "url": "https://github.com/sat-utils/sat-api"
- },
- {
- "name": "Earth Search by Element84",
- "description": "API of Earth on AWS datasets",
- "roles": [
- "host"
- ],
- "url": "https://element84.com"
- }
- ]
-}
\ No newline at end of file
diff --git a/stac_fastapi/pgstac/tests/data/test_item.json b/stac_fastapi/pgstac/tests/data/test_item.json
deleted file mode 100644
index a6e85a00d..000000000
--- a/stac_fastapi/pgstac/tests/data/test_item.json
+++ /dev/null
@@ -1,510 +0,0 @@
-{
- "type": "Feature",
- "id": "test-item",
- "stac_version": "1.0.0",
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "geometry": {
- "coordinates": [
- [
- [
- 152.15052873427666,
- -33.82243006904891
- ],
- [
- 150.1000346138806,
- -34.257132625788756
- ],
- [
- 149.5776607193635,
- -32.514709769700254
- ],
- [
- 151.6262528041627,
- -32.08081674221862
- ],
- [
- 152.15052873427666,
- -33.82243006904891
- ]
- ]
- ],
- "type": "Polygon"
- },
- "properties": {
- "datetime": "2020-02-12T12:30:22Z",
- "landsat:scene_id": "LC82081612020043LGN00",
- "landsat:row": "161",
- "gsd": 15,
- "eo:bands": [
- {
- "gsd": 30,
- "name": "B1",
- "common_name": "coastal",
- "center_wavelength": 0.44,
- "full_width_half_max": 0.02
- },
- {
- "gsd": 30,
- "name": "B2",
- "common_name": "blue",
- "center_wavelength": 0.48,
- "full_width_half_max": 0.06
- },
- {
- "gsd": 30,
- "name": "B3",
- "common_name": "green",
- "center_wavelength": 0.56,
- "full_width_half_max": 0.06
- },
- {
- "gsd": 30,
- "name": "B4",
- "common_name": "red",
- "center_wavelength": 0.65,
- "full_width_half_max": 0.04
- },
- {
- "gsd": 30,
- "name": "B5",
- "common_name": "nir",
- "center_wavelength": 0.86,
- "full_width_half_max": 0.03
- },
- {
- "gsd": 30,
- "name": "B6",
- "common_name": "swir16",
- "center_wavelength": 1.6,
- "full_width_half_max": 0.08
- },
- {
- "gsd": 30,
- "name": "B7",
- "common_name": "swir22",
- "center_wavelength": 2.2,
- "full_width_half_max": 0.2
- },
- {
- "gsd": 15,
- "name": "B8",
- "common_name": "pan",
- "center_wavelength": 0.59,
- "full_width_half_max": 0.18
- },
- {
- "gsd": 30,
- "name": "B9",
- "common_name": "cirrus",
- "center_wavelength": 1.37,
- "full_width_half_max": 0.02
- },
- {
- "gsd": 100,
- "name": "B10",
- "common_name": "lwir11",
- "center_wavelength": 10.9,
- "full_width_half_max": 0.8
- },
- {
- "gsd": 100,
- "name": "B11",
- "common_name": "lwir12",
- "center_wavelength": 12,
- "full_width_half_max": 1
- }
- ],
- "landsat:revision": "00",
- "view:sun_azimuth": -148.83296771,
- "instrument": "OLI_TIRS",
- "landsat:product_id": "LC08_L1GT_208161_20200212_20200212_01_RT",
- "eo:cloud_cover": 0,
- "landsat:tier": "RT",
- "landsat:processing_level": "L1GT",
- "landsat:column": "208",
- "platform": "landsat-8",
- "proj:epsg": 32756,
- "view:sun_elevation": -37.30791534,
- "view:off_nadir": 0,
- "height": 2500,
- "width": 2500
- },
- "bbox": [
- 149.57574,
- -34.25796,
- 152.15194,
- -32.07915
- ],
- "collection": "test-collection",
- "assets": {
- "ANG": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_ANG.txt",
- "type": "text/plain",
- "title": "Angle Coefficients File",
- "description": "Collection 2 Level-1 Angle Coefficients File (ANG)"
- },
- "SR_B1": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_SR_B1.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Coastal/Aerosol Band (B1)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B1",
- "common_name": "coastal",
- "center_wavelength": 0.44,
- "full_width_half_max": 0.02
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Collection 2 Level-2 Coastal/Aerosol Band (B1) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "SR_B2": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_SR_B2.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Blue Band (B2)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B2",
- "common_name": "blue",
- "center_wavelength": 0.48,
- "full_width_half_max": 0.06
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Collection 2 Level-2 Blue Band (B2) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "SR_B3": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_SR_B3.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Green Band (B3)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B3",
- "common_name": "green",
- "center_wavelength": 0.56,
- "full_width_half_max": 0.06
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Collection 2 Level-2 Green Band (B3) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "SR_B4": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_SR_B4.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Red Band (B4)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B4",
- "common_name": "red",
- "center_wavelength": 0.65,
- "full_width_half_max": 0.04
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Collection 2 Level-2 Red Band (B4) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "SR_B5": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_SR_B5.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Near Infrared Band 0.8 (B5)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B5",
- "common_name": "nir08",
- "center_wavelength": 0.86,
- "full_width_half_max": 0.03
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Collection 2 Level-2 Near Infrared Band 0.8 (B5) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "SR_B6": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_SR_B6.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Short-wave Infrared Band 1.6 (B6)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B6",
- "common_name": "swir16",
- "center_wavelength": 1.6,
- "full_width_half_max": 0.08
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Collection 2 Level-2 Short-wave Infrared Band 1.6 (B6) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "SR_B7": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_SR_B7.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Short-wave Infrared Band 2.2 (B7)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B7",
- "common_name": "swir22",
- "center_wavelength": 2.2,
- "full_width_half_max": 0.2
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Collection 2 Level-2 Short-wave Infrared Band 2.2 (B7) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "ST_QA": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_ST_QA.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Surface Temperature Quality Assessment Band",
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Landsat Collection 2 Level-2 Surface Temperature Band Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "ST_B10": {
- "gsd": 100,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_ST_B10.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Surface Temperature Band (B10)",
- "eo:bands": [
- {
- "gsd": 100,
- "name": "ST_B10",
- "common_name": "lwir11",
- "center_wavelength": 10.9,
- "full_width_half_max": 0.8
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Landsat Collection 2 Level-2 Surface Temperature Band (B10) Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "MTL.txt": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_MTL.txt",
- "type": "text/plain",
- "title": "Product Metadata File",
- "description": "Collection 2 Level-1 Product Metadata File (MTL)"
- },
- "MTL.xml": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_MTL.xml",
- "type": "application/xml",
- "title": "Product Metadata File (xml)",
- "description": "Collection 2 Level-1 Product Metadata File (xml)"
- },
- "ST_DRAD": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_ST_DRAD.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Downwelled Radiance Band",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "ST_DRAD",
- "description": "downwelled radiance"
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Landsat Collection 2 Level-2 Downwelled Radiance Band Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "ST_EMIS": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_ST_EMIS.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Emissivity Band",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "ST_EMIS",
- "description": "emissivity"
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Landsat Collection 2 Level-2 Emissivity Band Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "ST_EMSD": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_ST_EMSD.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Emissivity Standard Deviation Band",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "ST_EMSD",
- "description": "emissivity standard deviation"
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Landsat Collection 2 Level-2 Emissivity Standard Deviation Band Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- }
- },
- "links": [
- {
- "href": "http://localhost:8081/collections/landsat-8-l1/items/LC82081612020043",
- "rel": "self",
- "type": "application/geo+json"
- },
- {
- "href": "http://localhost:8081/collections/landsat-8-l1",
- "rel": "parent",
- "type": "application/json"
- },
- {
- "href": "http://localhost:8081/collections/landsat-8-l1",
- "rel": "collection",
- "type": "application/json"
- },
- {
- "href": "http://localhost:8081/",
- "rel": "root",
- "type": "application/json"
- },
- {
- "href": "preview.html",
- "rel": "preview",
- "type": "application/html"
- }
- ]
-}
\ No newline at end of file
diff --git a/stac_fastapi/pgstac/tests/data/test_item2.json b/stac_fastapi/pgstac/tests/data/test_item2.json
deleted file mode 100644
index 0d8ec7630..000000000
--- a/stac_fastapi/pgstac/tests/data/test_item2.json
+++ /dev/null
@@ -1,646 +0,0 @@
-{
- "type": "Feature",
- "id": "test_item2",
- "stac_version": "1.0.0",
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "bbox": [
- -123.37257493384075,
- 46.35430508465464,
- -120.21745704411174,
- 48.51504491534536
- ],
- "links": [
- {
- "rel": "collection",
- "type": "application/json",
- "href": "http://localhost:8081/api/stac/v1/collections/landsat-8-c2-l2"
- },
- {
- "rel": "parent",
- "type": "application/json",
- "href": "http://localhost:8081/api/stac/v1/collections/landsat-8-c2-l2"
- },
- {
- "rel": "root",
- "type": "application/json",
- "href": "http://localhost:8081/api/stac/v1/"
- },
- {
- "rel": "self",
- "type": "application/geo+json",
- "href": "http://localhost:8081/api/stac/v1/collections/landsat-8-c2-l2/items/LC08_L2SP_046027_20200908_02_T1"
- },
- {
- "rel": "alternate",
- "type": "application/json",
- "title": "tiles",
- "href": "http://localhost:8081/api/stac/v1/collections/landsat-8-c2-l2/items/LC08_L2SP_046027_20200908_02_T1/tiles"
- },
- {
- "rel": "alternate",
- "href": "https://landsatlook.usgs.gov/stac-browser/collection02/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_02_T1",
- "type": "text/html",
- "title": "USGS stac-browser page"
- },
- {
- "rel": "preview",
- "href": "http://localhost:8081/api/data/v1/item/map?collection=landsat-8-c2-l2&item=LC08_L2SP_046027_20200908_02_T1",
- "title": "Map of item",
- "type": "text/html"
- }
- ],
- "assets": {
- "ANG": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_ANG.txt",
- "type": "text/plain",
- "title": "Angle Coefficients File",
- "description": "Collection 2 Level-1 Angle Coefficients File (ANG)"
- },
- "SR_B1": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_SR_B1.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Coastal/Aerosol Band (B1)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B1",
- "common_name": "coastal",
- "center_wavelength": 0.44,
- "full_width_half_max": 0.02
- }
- ],
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Collection 2 Level-2 Coastal/Aerosol Band (B1) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "SR_B2": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_SR_B2.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Blue Band (B2)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B2",
- "common_name": "blue",
- "center_wavelength": 0.48,
- "full_width_half_max": 0.06
- }
- ],
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Collection 2 Level-2 Blue Band (B2) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "SR_B3": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_SR_B3.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Green Band (B3)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B3",
- "common_name": "green",
- "center_wavelength": 0.56,
- "full_width_half_max": 0.06
- }
- ],
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Collection 2 Level-2 Green Band (B3) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "SR_B4": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_SR_B4.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Red Band (B4)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B4",
- "common_name": "red",
- "center_wavelength": 0.65,
- "full_width_half_max": 0.04
- }
- ],
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Collection 2 Level-2 Red Band (B4) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "SR_B5": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_SR_B5.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Near Infrared Band 0.8 (B5)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B5",
- "common_name": "nir08",
- "center_wavelength": 0.86,
- "full_width_half_max": 0.03
- }
- ],
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Collection 2 Level-2 Near Infrared Band 0.8 (B5) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "SR_B6": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_SR_B6.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Short-wave Infrared Band 1.6 (B6)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B6",
- "common_name": "swir16",
- "center_wavelength": 1.6,
- "full_width_half_max": 0.08
- }
- ],
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Collection 2 Level-2 Short-wave Infrared Band 1.6 (B6) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "SR_B7": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_SR_B7.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Short-wave Infrared Band 2.2 (B7)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B7",
- "common_name": "swir22",
- "center_wavelength": 2.2,
- "full_width_half_max": 0.2
- }
- ],
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Collection 2 Level-2 Short-wave Infrared Band 2.2 (B7) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "ST_QA": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_ST_QA.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Surface Temperature Quality Assessment Band",
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Landsat Collection 2 Level-2 Surface Temperature Band Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "ST_B10": {
- "gsd": 100,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_ST_B10.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Surface Temperature Band (B10)",
- "eo:bands": [
- {
- "gsd": 100,
- "name": "ST_B10",
- "common_name": "lwir11",
- "center_wavelength": 10.9,
- "full_width_half_max": 0.8
- }
- ],
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Landsat Collection 2 Level-2 Surface Temperature Band (B10) Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "MTL.txt": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_MTL.txt",
- "type": "text/plain",
- "title": "Product Metadata File",
- "description": "Collection 2 Level-1 Product Metadata File (MTL)"
- },
- "MTL.xml": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_MTL.xml",
- "type": "application/xml",
- "title": "Product Metadata File (xml)",
- "description": "Collection 2 Level-1 Product Metadata File (xml)"
- },
- "ST_DRAD": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_ST_DRAD.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Downwelled Radiance Band",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "ST_DRAD",
- "description": "downwelled radiance"
- }
- ],
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Landsat Collection 2 Level-2 Downwelled Radiance Band Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "ST_EMIS": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_ST_EMIS.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Emissivity Band",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "ST_EMIS",
- "description": "emissivity"
- }
- ],
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Landsat Collection 2 Level-2 Emissivity Band Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "ST_EMSD": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_ST_EMSD.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Emissivity Standard Deviation Band",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "ST_EMSD",
- "description": "emissivity standard deviation"
- }
- ],
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Landsat Collection 2 Level-2 Emissivity Standard Deviation Band Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "ST_TRAD": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_ST_TRAD.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Thermal Radiance Band",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "ST_TRAD",
- "description": "thermal radiance"
- }
- ],
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Landsat Collection 2 Level-2 Thermal Radiance Band Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "ST_URAD": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_ST_URAD.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Upwelled Radiance Band",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "ST_URAD",
- "description": "upwelled radiance"
- }
- ],
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Landsat Collection 2 Level-2 Upwelled Radiance Band Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "MTL.json": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_MTL.json",
- "type": "application/json",
- "title": "Product Metadata File (json)",
- "description": "Collection 2 Level-1 Product Metadata File (json)"
- },
- "QA_PIXEL": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_QA_PIXEL.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Pixel Quality Assessment Band",
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Collection 2 Level-1 Pixel Quality Assessment Band",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "ST_ATRAN": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_ST_ATRAN.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Atmospheric Transmittance Band",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "ST_ATRAN",
- "description": "atmospheric transmission"
- }
- ],
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Landsat Collection 2 Level-2 Atmospheric Transmittance Band Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "ST_CDIST": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_ST_CDIST.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Cloud Distance Band",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "ST_CDIST",
- "description": "distance to nearest cloud"
- }
- ],
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Landsat Collection 2 Level-2 Cloud Distance Band Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "QA_RADSAT": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_QA_RADSAT.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Radiometric Saturation Quality Assessment Band",
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Collection 2 Level-1 Radiometric Saturation Quality Assessment Band",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "thumbnail": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_thumb_small.jpeg",
- "type": "image/jpeg",
- "title": "Thumbnail image"
- },
- "SR_QA_AEROSOL": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_SR_QA_AEROSOL.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Aerosol Quality Analysis Band",
- "proj:shape": [
- 7891,
- 7771
- ],
- "description": "Collection 2 Level-2 Aerosol Quality Analysis Band (ANG) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 472485,
- 0,
- -30,
- 5373615
- ]
- },
- "reduced_resolution_browse": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2020/046/027/LC08_L2SP_046027_20200908_20200919_02_T1/LC08_L2SP_046027_20200908_20200919_02_T1_thumb_large.jpeg",
- "type": "image/jpeg",
- "title": "Reduced resolution browse image"
- },
- "tilejson": {
- "title": "TileJSON with default rendering",
- "href": "https://planetarycomputer.microsoft.com/api/data/v1/item/tilejson.json?collection=landsat-8-c2-l2&items=LC08_L2SP_046027_20200908_02_T1&assets=SR_B4,SR_B3,SR_B2&color_formula=gamma+RGB+2.7%2C+saturation+1.5%2C+sigmoidal+RGB+15+0.55",
- "type": "application/json",
- "roles": [
- "tiles"
- ]
- },
- "rendered_preview": {
- "title": "Rendered preview",
- "rel": "preview",
- "href": "https://planetarycomputer.microsoft.com/api/data/v1/item/preview.png?collection=landsat-8-c2-l2&items=LC08_L2SP_046027_20200908_02_T1&assets=SR_B4,SR_B3,SR_B2&color_formula=gamma+RGB+2.7%2C+saturation+1.5%2C+sigmoidal+RGB+15+0.55",
- "roles": [
- "overview"
- ],
- "type": "image/png"
- }
- },
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -122.73659863,
- 48.512551
- ],
- [
- -120.21828301,
- 48.09736515
- ],
- [
- -120.85665503,
- 46.35688928
- ],
- [
- -123.37063967,
- 46.78158223
- ],
- [
- -122.73659863,
- 48.512551
- ]
- ]
- ]
- },
- "collection": "test-collection",
- "properties": {
- "datetime": "2020-09-08T18:55:51.575595Z",
- "platform": "landsat-8",
- "proj:bbox": [
- 472485,
- 5136885,
- 705615,
- 5373615
- ],
- "proj:epsg": 32610,
- "description": "Landsat Collection 2 Level-2 Surface Reflectance Product",
- "instruments": [
- "oli",
- "tirs"
- ],
- "eo:cloud_cover": 0.19,
- "view:off_nadir": 0,
- "landsat:wrs_row": "027",
- "landsat:scene_id": "LC80460272020252LGN00",
- "landsat:wrs_path": "046",
- "landsat:wrs_type": "2",
- "view:sun_azimuth": 155.2327918,
- "view:sun_elevation": 45.33819766,
- "landsat:cloud_cover_land": 0.21,
- "landsat:processing_level": "L2SP",
- "landsat:collection_number": "02",
- "landsat:collection_category": "T1"
- }
-}
\ No newline at end of file
diff --git a/stac_fastapi/pgstac/tests/resources/__init__.py b/stac_fastapi/pgstac/tests/resources/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/stac_fastapi/pgstac/tests/resources/test_collection.py b/stac_fastapi/pgstac/tests/resources/test_collection.py
deleted file mode 100644
index 6910f0fe7..000000000
--- a/stac_fastapi/pgstac/tests/resources/test_collection.py
+++ /dev/null
@@ -1,244 +0,0 @@
-from typing import Callable
-
-import pystac
-import pytest
-from stac_pydantic import Collection
-
-
-async def test_create_collection(app_client, load_test_data: Callable):
- in_json = load_test_data("test_collection.json")
- in_coll = Collection.parse_obj(in_json)
- resp = await app_client.post(
- "/collections",
- json=in_json,
- )
- assert resp.status_code == 200
- post_coll = Collection.parse_obj(resp.json())
- assert in_coll.dict(exclude={"links"}) == post_coll.dict(exclude={"links"})
- resp = await app_client.get(f"/collections/{post_coll.id}")
- assert resp.status_code == 200
- get_coll = Collection.parse_obj(resp.json())
- assert post_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"})
-
- post_self_link = next(
- (link for link in post_coll.links if link.rel == "self"), None
- )
- get_self_link = next((link for link in get_coll.links if link.rel == "self"), None)
- assert post_self_link is not None and get_self_link is not None
- assert post_self_link.href == get_self_link.href
-
-
-async def test_update_collection(app_client, load_test_data, load_test_collection):
- in_coll = load_test_collection
- in_coll.keywords.append("newkeyword")
-
- resp = await app_client.put("/collections", json=in_coll.dict())
- assert resp.status_code == 200
- put_coll = Collection.parse_obj(resp.json())
-
- resp = await app_client.get(f"/collections/{in_coll.id}")
- assert resp.status_code == 200
-
- get_coll = Collection.parse_obj(resp.json())
- assert in_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"})
- assert "newkeyword" in get_coll.keywords
-
- put_self_link = next((link for link in put_coll.links if link.rel == "self"), None)
- get_self_link = next((link for link in get_coll.links if link.rel == "self"), None)
- assert put_self_link is not None and get_self_link is not None
- assert put_self_link.href == get_self_link.href
-
-
-async def test_delete_collection(
- app_client, load_test_data: Callable, load_test_collection
-):
- in_coll = load_test_collection
-
- resp = await app_client.delete(f"/collections/{in_coll.id}")
- assert resp.status_code == 200
-
- resp = await app_client.get(f"/collections/{in_coll.id}")
- assert resp.status_code == 404
-
-
-async def test_create_collection_conflict(app_client, load_test_data: Callable):
- in_json = load_test_data("test_collection.json")
- Collection.parse_obj(in_json)
- resp = await app_client.post(
- "/collections",
- json=in_json,
- )
- assert resp.status_code == 200
- Collection.parse_obj(resp.json())
- resp = await app_client.post(
- "/collections",
- json=in_json,
- )
- assert resp.status_code == 409
-
-
-async def test_delete_missing_collection(
- app_client,
-):
- resp = await app_client.delete("/collections")
- assert resp.status_code == 405
-
-
-async def test_update_new_collection(app_client, load_test_collection):
- in_coll = load_test_collection
- in_coll.id = "test-updatenew"
-
- resp = await app_client.put("/collections", json=in_coll.dict())
- assert resp.status_code == 404
-
-
-async def test_nocollections(
- app_client,
-):
- resp = await app_client.get("/collections")
- assert resp.status_code == 200
-
-
-async def test_returns_valid_collection(app_client, load_test_data):
- """Test updating a collection which already exists"""
- in_json = load_test_data("test_collection.json")
- resp = await app_client.post(
- "/collections",
- json=in_json,
- )
- assert resp.status_code == 200
-
- resp = await app_client.get(f"/collections/{in_json['id']}")
- assert resp.status_code == 200
- resp_json = resp.json()
-
- # Mock root to allow validation
- mock_root = pystac.Catalog(
- id="test", description="test desc", href="https://example.com"
- )
- collection = pystac.Collection.from_dict(
- resp_json, root=mock_root, preserve_dict=False
- )
- collection.validate()
-
-
-async def test_returns_valid_links_in_collections(app_client, load_test_data):
- """Test links from listing collections"""
- in_json = load_test_data("test_collection.json")
- resp = await app_client.post(
- "/collections",
- json=in_json,
- )
- assert resp.status_code == 200
-
- # Get collection by ID
- resp = await app_client.get(f"/collections/{in_json['id']}")
- assert resp.status_code == 200
- resp_json = resp.json()
-
- # Mock root to allow validation
- mock_root = pystac.Catalog(
- id="test", description="test desc", href="https://example.com"
- )
- collection = pystac.Collection.from_dict(
- resp_json, root=mock_root, preserve_dict=False
- )
- assert collection.validate()
-
- # List collections
- resp = await app_client.get("/collections")
- assert resp.status_code == 200
- resp_json = resp.json()
- collections = resp_json["collections"]
- # Find collection in list by ID
- single_coll = next(coll for coll in collections if coll["id"] == in_json["id"])
- is_coll_from_list_valid = False
- single_coll_mocked_link = dict()
- if single_coll is not None:
- single_coll_mocked_link = pystac.Collection.from_dict(
- single_coll, root=mock_root, preserve_dict=False
- )
- is_coll_from_list_valid = single_coll_mocked_link.validate()
-
- assert is_coll_from_list_valid
-
- # Check links from the collection GET and list
- assert [
- i
- for i in collection.to_dict()["links"]
- if i not in single_coll_mocked_link.to_dict()["links"]
- ] == []
-
-
-async def test_returns_license_link(app_client, load_test_collection):
- coll = load_test_collection
-
- resp = await app_client.get(f"/collections/{coll.id}")
- assert resp.status_code == 200
- resp_json = resp.json()
- link_rel_types = [link["rel"] for link in resp_json["links"]]
- assert "license" in link_rel_types
-
-
-@pytest.mark.asyncio
-async def test_get_collection_forwarded_header(app_client, load_test_collection):
- coll = load_test_collection
- resp = await app_client.get(
- f"/collections/{coll.id}",
- headers={"Forwarded": "proto=https;host=test:1234"},
- )
- for link in [
- link
- for link in resp.json()["links"]
- if link["rel"] in ["items", "parent", "root", "self"]
- ]:
- assert link["href"].startswith("https://test:1234/")
-
-
-@pytest.mark.asyncio
-async def test_get_collection_x_forwarded_headers(app_client, load_test_collection):
- coll = load_test_collection
- resp = await app_client.get(
- f"/collections/{coll.id}",
- headers={
- "X-Forwarded-Port": "1234",
- "X-Forwarded-Proto": "https",
- },
- )
- for link in [
- link
- for link in resp.json()["links"]
- if link["rel"] in ["items", "parent", "root", "self"]
- ]:
- assert link["href"].startswith("https://test:1234/")
-
-
-@pytest.mark.asyncio
-async def test_get_collection_duplicate_forwarded_headers(
- app_client, load_test_collection
-):
- coll = load_test_collection
- resp = await app_client.get(
- f"/collections/{coll.id}",
- headers={
- "Forwarded": "proto=https;host=test:1234",
- "X-Forwarded-Port": "4321",
- "X-Forwarded-Proto": "http",
- },
- )
- for link in [
- link
- for link in resp.json()["links"]
- if link["rel"] in ["items", "parent", "root", "self"]
- ]:
- assert link["href"].startswith("https://test:1234/")
-
-
-@pytest.mark.asyncio
-async def test_get_collections_forwarded_header(app_client, load_test_collection):
- resp = await app_client.get(
- "/collections",
- headers={"Forwarded": "proto=https;host=test:1234"},
- )
- for link in resp.json()["links"]:
- assert link["href"].startswith("https://test:1234/")
diff --git a/stac_fastapi/pgstac/tests/resources/test_conformance.py b/stac_fastapi/pgstac/tests/resources/test_conformance.py
deleted file mode 100644
index b9f78852c..000000000
--- a/stac_fastapi/pgstac/tests/resources/test_conformance.py
+++ /dev/null
@@ -1,76 +0,0 @@
-import urllib.parse
-from typing import Dict, Optional
-
-import pytest
-
-
-@pytest.fixture(scope="function")
-async def response(app_client):
- return await app_client.get("/")
-
-
-@pytest.fixture(scope="function")
-async def response_json(response) -> Dict:
- return response.json()
-
-
-def get_link(landing_page, rel_type, method: Optional[str] = None):
- return next(
- filter(
- lambda link: link["rel"] == rel_type
- and (not method or link.get("method") == method),
- landing_page["links"],
- ),
- None,
- )
-
-
-def test_landing_page_health(response):
- """Test landing page"""
- assert response.status_code == 200
- assert response.headers["content-type"] == "application/json"
-
-
-# Parameters for test_landing_page_links test below.
-# Each tuple has the following values (in this order):
-# - Rel type of link to test
-# - Expected MIME/Media Type
-# - Expected relative path
-link_tests = [
- ("root", "application/json", "/"),
- ("conformance", "application/json", "/conformance"),
- ("service-doc", "text/html", "/api.html"),
- ("service-desc", "application/vnd.oai.openapi+json;version=3.0", "/api"),
-]
-
-
-@pytest.mark.parametrize("rel_type,expected_media_type,expected_path", link_tests)
-async def test_landing_page_links(
- response_json: Dict, app_client, app, rel_type, expected_media_type, expected_path
-):
- link = get_link(response_json, rel_type)
-
- assert link is not None, f"Missing {rel_type} link in landing page"
- assert link.get("type") == expected_media_type
-
- link_path = urllib.parse.urlsplit(link.get("href")).path
- assert link_path == app.state.router_prefix + expected_path
-
- resp = await app_client.get(link_path.rsplit("/", 1)[-1])
- assert resp.status_code == 200
-
-
-# This endpoint currently returns a 404 for empty result sets, but testing for this response
-# code here seems meaningless since it would be the same as if the endpoint did not exist. Once
-# https://github.com/stac-utils/stac-fastapi/pull/227 has been merged we can add this to the
-# parameterized tests above.
-def test_search_link(response_json: Dict, app):
- for search_link in [
- get_link(response_json, "search", "GET"),
- get_link(response_json, "search", "POST"),
- ]:
- assert search_link is not None
- assert search_link.get("type") == "application/geo+json"
-
- search_path = urllib.parse.urlsplit(search_link.get("href")).path
- assert search_path == app.state.router_prefix + "/search"
diff --git a/stac_fastapi/pgstac/tests/resources/test_item.py b/stac_fastapi/pgstac/tests/resources/test_item.py
deleted file mode 100644
index 43e1f22ef..000000000
--- a/stac_fastapi/pgstac/tests/resources/test_item.py
+++ /dev/null
@@ -1,1511 +0,0 @@
-import json
-import random
-import uuid
-from datetime import timedelta
-from http.client import HTTP_PORT
-from string import ascii_letters
-from typing import Callable
-from urllib.parse import parse_qs, urljoin, urlparse
-
-import pystac
-import pytest
-from httpx import AsyncClient
-from pystac.utils import datetime_to_str
-from shapely.geometry import Polygon
-from stac_pydantic import Collection, Item
-from starlette.requests import Request
-
-from stac_fastapi.pgstac.models.links import CollectionLinks
-from stac_fastapi.types.rfc3339 import rfc3339_str_to_datetime
-
-
-async def test_create_collection(app_client, load_test_data: Callable):
- in_json = load_test_data("test_collection.json")
- in_coll = Collection.parse_obj(in_json)
- resp = await app_client.post(
- "/collections",
- json=in_json,
- )
- assert resp.status_code == 200
- post_coll = Collection.parse_obj(resp.json())
- assert in_coll.dict(exclude={"links"}) == post_coll.dict(exclude={"links"})
- resp = await app_client.get(f"/collections/{post_coll.id}")
- assert resp.status_code == 200
- get_coll = Collection.parse_obj(resp.json())
- assert post_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"})
-
-
-async def test_update_collection(app_client, load_test_data, load_test_collection):
- in_coll = load_test_collection
- in_coll.keywords.append("newkeyword")
-
- resp = await app_client.put("/collections", json=in_coll.dict())
- assert resp.status_code == 200
-
- resp = await app_client.get(f"/collections/{in_coll.id}")
- assert resp.status_code == 200
-
- get_coll = Collection.parse_obj(resp.json())
- assert in_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"})
- assert "newkeyword" in get_coll.keywords
-
-
-async def test_delete_collection(
- app_client, load_test_data: Callable, load_test_collection
-):
- in_coll = load_test_collection
-
- resp = await app_client.delete(f"/collections/{in_coll.id}")
- assert resp.status_code == 200
-
- resp = await app_client.get(f"/collections/{in_coll.id}")
- assert resp.status_code == 404
-
-
-async def test_create_item(app_client, load_test_data: Callable, load_test_collection):
- coll = load_test_collection
-
- in_json = load_test_data("test_item.json")
- in_item = Item.parse_obj(in_json)
- resp = await app_client.post(
- f"/collections/{coll.id}/items",
- json=in_json,
- )
- assert resp.status_code == 200
-
- post_item = Item.parse_obj(resp.json())
- assert in_item.dict(exclude={"links"}) == post_item.dict(exclude={"links"})
-
- resp = await app_client.get(f"/collections/{coll.id}/items/{post_item.id}")
-
- assert resp.status_code == 200
- get_item = Item.parse_obj(resp.json())
- assert in_item.dict(exclude={"links"}) == get_item.dict(exclude={"links"})
-
- post_self_link = next(
- (link for link in post_item.links if link.rel == "self"), None
- )
- get_self_link = next((link for link in get_item.links if link.rel == "self"), None)
- assert post_self_link is not None and get_self_link is not None
- assert post_self_link.href == get_self_link.href
-
-
-async def test_create_item_mismatched_collection_id(
- app_client, load_test_data: Callable, load_test_collection
-):
- # If the collection_id path parameter and the Item's "collection" property do not match, a 400 response should
- # be returned.
- coll = load_test_collection
-
- in_json = load_test_data("test_item.json")
- in_json["collection"] = random.choice(ascii_letters)
- assert in_json["collection"] != coll.id
-
- resp = await app_client.post(
- f"/collections/{coll.id}/items",
- json=in_json,
- )
- assert resp.status_code == 400
-
-
-async def test_fetches_valid_item(
- app_client, load_test_data: Callable, load_test_collection
-):
- coll = load_test_collection
-
- in_json = load_test_data("test_item.json")
- in_item = Item.parse_obj(in_json)
- resp = await app_client.post(
- f"/collections/{coll.id}/items",
- json=in_json,
- )
- assert resp.status_code == 200
-
- post_item = Item.parse_obj(resp.json())
- assert in_item.dict(exclude={"links"}) == post_item.dict(exclude={"links"})
-
- resp = await app_client.get(f"/collections/{coll.id}/items/{post_item.id}")
-
- assert resp.status_code == 200
- item_dict = resp.json()
- # Mock root to allow validation
- mock_root = pystac.Catalog(
- id="test", description="test desc", href="https://example.com"
- )
- item = pystac.Item.from_dict(item_dict, preserve_dict=False, root=mock_root)
- item.validate()
-
-
-async def test_update_item(
- app_client, load_test_data: Callable, load_test_collection, load_test_item
-):
- coll = load_test_collection
- item = load_test_item
-
- item.properties.description = "Update Test"
-
- resp = await app_client.put(
- f"/collections/{coll.id}/items/{item.id}", content=item.json()
- )
- assert resp.status_code == 200
- put_item = Item.parse_obj(resp.json())
-
- resp = await app_client.get(f"/collections/{coll.id}/items/{item.id}")
- assert resp.status_code == 200
-
- get_item = Item.parse_obj(resp.json())
- assert item.dict(exclude={"links"}) == get_item.dict(exclude={"links"})
- assert get_item.properties.description == "Update Test"
-
- post_self_link = next((link for link in put_item.links if link.rel == "self"), None)
- get_self_link = next((link for link in get_item.links if link.rel == "self"), None)
- assert post_self_link is not None and get_self_link is not None
- assert post_self_link.href == get_self_link.href
-
-
-async def test_update_item_mismatched_collection_id(
- app_client, load_test_data: Callable, load_test_collection, load_test_item
-) -> None:
- coll = load_test_collection
-
- in_json = load_test_data("test_item.json")
-
- in_json["collection"] = random.choice(ascii_letters)
- assert in_json["collection"] != coll.id
-
- item_id = in_json["id"]
-
- resp = await app_client.put(
- f"/collections/{coll.id}/items/{item_id}",
- json=in_json,
- )
- assert resp.status_code == 400
-
-
-async def test_delete_item(
- app_client, load_test_data: Callable, load_test_collection, load_test_item
-):
- coll = load_test_collection
- item = load_test_item
-
- resp = await app_client.delete(f"/collections/{coll.id}/items/{item.id}")
- assert resp.status_code == 200
-
- resp = await app_client.get(f"/collections/{coll.id}/items/{item.id}")
- assert resp.status_code == 404
-
-
-async def test_get_collection_items(app_client, load_test_collection, load_test_item):
- coll = load_test_collection
- item = load_test_item
-
- for _ in range(4):
- item.id = str(uuid.uuid4())
- resp = await app_client.post(
- f"/collections/{coll.id}/items",
- content=item.json(),
- )
- assert resp.status_code == 200
-
- resp = await app_client.get(
- f"/collections/{coll.id}/items",
- )
- assert resp.status_code == 200
- fc = resp.json()
- assert "features" in fc
- assert len(fc["features"]) == 5
-
-
-async def test_create_item_conflict(
- app_client, load_test_data: Callable, load_test_collection
-):
- coll = load_test_collection
- in_json = load_test_data("test_item.json")
- Item.parse_obj(in_json)
- resp = await app_client.post(
- f"/collections/{coll.id}/items",
- json=in_json,
- )
- assert resp.status_code == 200
-
- resp = await app_client.post(
- f"/collections/{coll.id}/items",
- json=in_json,
- )
- assert resp.status_code == 409
-
-
-async def test_delete_missing_item(
- app_client, load_test_data: Callable, load_test_collection, load_test_item
-):
- coll = load_test_collection
- item = load_test_item
-
- resp = await app_client.delete(f"/collections/{coll.id}/items/{item.id}")
- assert resp.status_code == 200
-
- resp = await app_client.delete(f"/collections/{coll.id}/items/{item.id}")
- assert resp.status_code == 404
-
-
-async def test_create_item_missing_collection(
- app_client, load_test_data: Callable, load_test_collection
-):
- coll = load_test_collection
- item = load_test_data("test_item.json")
- item["collection"] = None
-
- resp = await app_client.post(f"/collections/{coll.id}/items", json=item)
- assert resp.status_code == 200
-
- post_item = resp.json()
- assert post_item["collection"] == coll.id
-
-
-async def test_update_new_item(
- app_client, load_test_data: Callable, load_test_collection, load_test_item
-):
- coll = load_test_collection
- item = load_test_item
- item.id = "test-updatenewitem"
-
- resp = await app_client.put(
- f"/collections/{coll.id}/items/{item.id}", content=item.json()
- )
- assert resp.status_code == 404
-
-
-async def test_update_item_missing_collection(
- app_client, load_test_data: Callable, load_test_collection, load_test_item
-):
- coll = load_test_collection
- item = load_test_item
- item.collection = None
-
- resp = await app_client.put(
- f"/collections/{coll.id}/items/{item.id}", content=item.json()
- )
- assert resp.status_code == 200
-
- put_item = resp.json()
- assert put_item["collection"] == coll.id
-
-
-async def test_pagination(app_client, load_test_data, load_test_collection):
- """Test item collection pagination (paging extension)"""
- coll = load_test_collection
- item_count = 21
- test_item = load_test_data("test_item.json")
-
- for idx in range(1, item_count):
- item = Item.parse_obj(test_item)
- item.id = item.id + str(idx)
- item.properties.datetime = f"2020-01-{idx:02d}T00:00:00"
- resp = await app_client.post(f"/collections/{coll.id}/items", json=item.dict())
- assert resp.status_code == 200
-
- resp = await app_client.get(f"/collections/{coll.id}/items", params={"limit": 3})
- assert resp.status_code == 200
- first_page = resp.json()
- assert len(first_page["features"]) == 3
-
- nextlink = [
- link["href"] for link in first_page["links"] if link["rel"] == "next"
- ].pop()
-
- assert nextlink is not None
-
- assert [f["id"] for f in first_page["features"]] == [
- "test-item20",
- "test-item19",
- "test-item18",
- ]
-
- resp = await app_client.get(nextlink)
- assert resp.status_code == 200
- second_page = resp.json()
- assert len(first_page["features"]) == 3
-
- nextlink = [
- link["href"] for link in second_page["links"] if link["rel"] == "next"
- ].pop()
-
- assert nextlink is not None
-
- prevlink = [
- link["href"] for link in second_page["links"] if link["rel"] == "previous"
- ].pop()
-
- assert prevlink is not None
-
- assert [f["id"] for f in second_page["features"]] == [
- "test-item17",
- "test-item16",
- "test-item15",
- ]
-
- resp = await app_client.get(prevlink)
- assert resp.status_code == 200
- back_page = resp.json()
- assert len(back_page["features"]) == 3
- assert [f["id"] for f in back_page["features"]] == [
- "test-item20",
- "test-item19",
- "test-item18",
- ]
-
-
-async def test_item_search_by_id_post(app_client, load_test_data, load_test_collection):
- """Test POST search by item id (core)"""
- ids = ["test1", "test2", "test3"]
- for id in ids:
- test_item = load_test_data("test_item.json")
- test_item["id"] = id
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- params = {"collections": [test_item["collection"]], "ids": ids}
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == len(ids)
- assert set([feat["id"] for feat in resp_json["features"]]) == set(ids)
-
-
-async def test_item_search_by_id_no_results_post(
- app_client, load_test_data, load_test_collection
-):
- """Test POST search by item id (core) when there are no results"""
- test_item = load_test_data("test_item.json")
-
- search_ids = ["nonexistent_id"]
-
- params = {"collections": [test_item["collection"]], "ids": search_ids}
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 0
-
-
-async def test_item_search_spatial_query_post(
- app_client, load_test_data, load_test_collection
-):
- """Test POST search with spatial query (core)"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # Add second item with a different datetime.
- second_test_item = load_test_data("test_item2.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=second_test_item
- )
- assert resp.status_code == 200
-
- params = {
- "collections": [test_item["collection"]],
- "intersects": test_item["geometry"],
- }
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
- assert resp_json["features"][0]["id"] == test_item["id"]
-
-
-async def test_item_search_temporal_query_post(
- app_client, load_test_data, load_test_collection
-):
- """Test POST search with single-tailed spatio-temporal query (core)"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # Add second item with a different datetime.
- second_test_item = load_test_data("test_item2.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=second_test_item
- )
- assert resp.status_code == 200
-
- item_date = rfc3339_str_to_datetime(test_item["properties"]["datetime"])
-
- params = {
- "collections": [test_item["collection"]],
- "intersects": test_item["geometry"],
- "datetime": datetime_to_str(item_date),
- }
-
- resp = await app_client.post("/search", json=params)
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
- assert resp_json["features"][0]["id"] == test_item["id"]
-
-
-async def test_item_search_temporal_window_post(
- app_client, load_test_data, load_test_collection
-):
- """Test POST search with two-tailed spatio-temporal query (core)"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # Add second item with a different datetime.
- second_test_item = load_test_data("test_item2.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=second_test_item
- )
- assert resp.status_code == 200
-
- item_date = rfc3339_str_to_datetime(test_item["properties"]["datetime"])
- item_date_before = item_date - timedelta(seconds=1)
- item_date_after = item_date + timedelta(seconds=1)
-
- params = {
- "collections": [test_item["collection"]],
- "datetime": f"{datetime_to_str(item_date_before)}/{datetime_to_str(item_date_after)}",
- }
-
- resp = await app_client.post("/search", json=params)
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
- assert resp_json["features"][0]["id"] == test_item["id"]
-
-
-async def test_item_search_temporal_open_window(
- app_client, load_test_data, load_test_collection
-):
- for dt in ["/", "../..", "../", "/.."]:
- resp = await app_client.post("/search", json={"datetime": dt})
- assert resp.status_code == 400
-
-
-async def test_item_search_sort_post(app_client, load_test_data, load_test_collection):
- """Test POST search with sorting (sort extension)"""
- first_item = load_test_data("test_item.json")
- item_date = rfc3339_str_to_datetime(first_item["properties"]["datetime"])
- resp = await app_client.post(
- f"/collections/{first_item['collection']}/items", json=first_item
- )
- assert resp.status_code == 200
-
- second_item = load_test_data("test_item.json")
- second_item["id"] = "another-item"
- another_item_date = item_date - timedelta(days=1)
- second_item["properties"]["datetime"] = datetime_to_str(another_item_date)
- resp = await app_client.post(
- f"/collections/{second_item['collection']}/items", json=second_item
- )
- assert resp.status_code == 200
-
- params = {
- "collections": [first_item["collection"]],
- "sortby": [{"field": "datetime", "direction": "desc"}],
- }
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert resp_json["features"][0]["id"] == first_item["id"]
- assert resp_json["features"][1]["id"] == second_item["id"]
-
-
-async def test_item_search_by_id_get(app_client, load_test_data, load_test_collection):
- """Test GET search by item id (core)"""
- ids = ["test1", "test2", "test3"]
- for id in ids:
- test_item = load_test_data("test_item.json")
- test_item["id"] = id
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- params = {"collections": test_item["collection"], "ids": ",".join(ids)}
- resp = await app_client.get("/search", params=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == len(ids)
- assert set([feat["id"] for feat in resp_json["features"]]) == set(ids)
-
-
-async def test_item_search_bbox_get(app_client, load_test_data, load_test_collection):
- """Test GET search with spatial query (core)"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # Add second item with a different datetime.
- second_test_item = load_test_data("test_item2.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=second_test_item
- )
- assert resp.status_code == 200
-
- params = {
- "collections": test_item["collection"],
- "bbox": ",".join([str(coord) for coord in test_item["bbox"]]),
- }
- resp = await app_client.get("/search", params=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
- assert resp_json["features"][0]["id"] == test_item["id"]
-
-
-async def test_item_search_get_without_collections(
- app_client, load_test_data, load_test_collection
-):
- """Test GET search without specifying collections"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # Add second item with a different datetime.
- second_test_item = load_test_data("test_item2.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=second_test_item
- )
- assert resp.status_code == 200
-
- params = {
- "bbox": ",".join([str(coord) for coord in test_item["bbox"]]),
- }
- resp = await app_client.get("/search", params=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
- assert resp_json["features"][0]["id"] == test_item["id"]
-
-
-async def test_item_search_temporal_window_get(
- app_client, load_test_data, load_test_collection
-):
- """Test GET search with spatio-temporal query (core)"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # Add second item with a different datetime.
- second_test_item = load_test_data("test_item2.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=second_test_item
- )
- assert resp.status_code == 200
-
- item_date = rfc3339_str_to_datetime(test_item["properties"]["datetime"])
- item_date_before = item_date - timedelta(seconds=1)
- item_date_after = item_date + timedelta(seconds=1)
-
- params = {
- "collections": test_item["collection"],
- "datetime": f"{datetime_to_str(item_date_before)}/{datetime_to_str(item_date_after)}",
- }
- resp = await app_client.get("/search", params=params)
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
- assert resp_json["features"][0]["id"] == test_item["id"]
-
-
-async def test_item_search_sort_get(app_client, load_test_data, load_test_collection):
- """Test GET search with sorting (sort extension)"""
- first_item = load_test_data("test_item.json")
- item_date = rfc3339_str_to_datetime(first_item["properties"]["datetime"])
- resp = await app_client.post(
- f"/collections/{first_item['collection']}/items", json=first_item
- )
- assert resp.status_code == 200
-
- second_item = load_test_data("test_item.json")
- second_item["id"] = "another-item"
- another_item_date = item_date - timedelta(days=1)
- second_item["properties"]["datetime"] = datetime_to_str(another_item_date)
- resp = await app_client.post(
- f"/collections/{second_item['collection']}/items", json=second_item
- )
- assert resp.status_code == 200
- params = {"collections": [first_item["collection"]], "sortby": "-datetime"}
- resp = await app_client.get("/search", params=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert resp_json["features"][0]["id"] == first_item["id"]
- assert resp_json["features"][1]["id"] == second_item["id"]
-
-
-async def test_item_search_post_without_collection(
- app_client, load_test_data, load_test_collection
-):
- """Test POST search without specifying a collection"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- second_test_item = load_test_data("test_item2.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=second_test_item
- )
- assert resp.status_code == 200
-
- params = {
- "bbox": test_item["bbox"],
- }
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert resp_json["features"][0]["id"] == test_item["id"]
-
-
-async def test_item_search_properties_jsonb(
- app_client, load_test_data, load_test_collection
-):
- """Test POST search with JSONB query (query extension)"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- second_test_item = load_test_data("test_item2.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=second_test_item
- )
- assert resp.status_code == 200
-
- # EPSG is a JSONB key
- params = {"query": {"proj:epsg": {"gt": test_item["properties"]["proj:epsg"] - 1}}}
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
-
-async def test_item_search_properties_field(
- app_client, load_test_data, load_test_collection
-):
- """Test POST search indexed field with query (query extension)"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- second_test_item = load_test_data("test_item2.json")
- second_test_item["properties"]["eo:cloud_cover"] = 5
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=second_test_item
- )
- assert resp.status_code == 200
-
- params = {"query": {"eo:cloud_cover": {"eq": 0}}}
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
-
-async def test_item_search_get_query_extension(
- app_client, load_test_data, load_test_collection
-):
- """Test GET search with JSONB query (query extension)"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- second_test_item = load_test_data("test_item2.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=second_test_item
- )
- assert resp.status_code == 200
-
- # EPSG is a JSONB key
- params = {
- "collections": [test_item["collection"]],
- "query": json.dumps(
- {"proj:epsg": {"gt": test_item["properties"]["proj:epsg"] + 1}}
- ),
- }
- resp = await app_client.get("/search", params=params)
- # No items found should still return a 200 but with an empty list of features
- assert resp.status_code == 200
- assert len(resp.json()["features"]) == 0
-
- params["query"] = json.dumps(
- {"proj:epsg": {"eq": test_item["properties"]["proj:epsg"]}}
- )
- resp = await app_client.get("/search", params=params)
- resp_json = resp.json()
- assert len(resp.json()["features"]) == 1
- assert (
- resp_json["features"][0]["properties"]["proj:epsg"]
- == test_item["properties"]["proj:epsg"]
- )
-
-
-async def test_item_search_get_filter_extension_cql(
- app_client, load_test_data, load_test_collection
-):
- """Test GET search with JSONB query (cql json filter extension)"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- second_test_item = load_test_data("test_item2.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=second_test_item
- )
- assert resp.status_code == 200
-
- # EPSG is a JSONB key
- params = {
- "collections": [test_item["collection"]],
- "filter": {
- "gt": [
- {"property": "proj:epsg"},
- test_item["properties"]["proj:epsg"] + 1,
- ]
- },
- }
- resp = await app_client.post("/search", json=params)
- resp_json = resp.json()
-
- assert resp.status_code == 200
- assert len(resp_json.get("features")) == 0
-
- params = {
- "collections": [test_item["collection"]],
- "filter": {
- "eq": [
- {"property": "proj:epsg"},
- test_item["properties"]["proj:epsg"],
- ]
- },
- }
- resp = await app_client.post("/search", json=params)
- resp_json = resp.json()
- assert len(resp.json()["features"]) == 1
- assert (
- resp_json["features"][0]["properties"]["proj:epsg"]
- == test_item["properties"]["proj:epsg"]
- )
-
-
-async def test_item_search_get_filter_extension_cql2(
- app_client, load_test_data, load_test_collection
-):
- """Test GET search with JSONB query (cql2 json filter extension)"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- second_test_item = load_test_data("test_item2.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=second_test_item
- )
- assert resp.status_code == 200
-
- # EPSG is a JSONB key
- params = {
- "collections": [test_item["collection"]],
- "filter-lang": "cql2-json",
- "filter": {
- "op": "gt",
- "args": [
- {"property": "proj:epsg"},
- test_item["properties"]["proj:epsg"] + 1,
- ],
- },
- }
- resp = await app_client.post("/search", json=params)
- resp_json = resp.json()
-
- assert resp.status_code == 200
- assert len(resp_json.get("features")) == 0
-
- params = {
- "collections": [test_item["collection"]],
- "filter-lang": "cql2-json",
- "filter": {
- "op": "eq",
- "args": [
- {"property": "proj:epsg"},
- test_item["properties"]["proj:epsg"],
- ],
- },
- }
- resp = await app_client.post("/search", json=params)
- resp_json = resp.json()
- assert len(resp.json()["features"]) == 1
- assert (
- resp_json["features"][0]["properties"]["proj:epsg"]
- == test_item["properties"]["proj:epsg"]
- )
-
-
-async def test_item_search_get_filter_extension_cql2_with_query_fails(
- app_client, load_test_data, load_test_collection
-):
- """Test GET search with JSONB query (cql2 json filter extension)"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- second_test_item = load_test_data("test_item2.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=second_test_item
- )
- assert resp.status_code == 200
-
- # EPSG is a JSONB key
- params = {
- "collections": [test_item["collection"]],
- "filter-lang": "cql2-json",
- "filter": {
- "op": "gt",
- "args": [
- {"property": "proj:epsg"},
- test_item["properties"]["proj:epsg"] + 1,
- ],
- },
- "query": {"eo:cloud_cover": {"eq": 0}},
- }
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 400
-
-
-async def test_get_missing_item_collection(app_client):
- """Test reading a collection which does not exist"""
- resp = await app_client.get("/collections/invalid-collection/items")
- assert resp.status_code == 404
-
-
-async def test_get_item_from_missing_item_collection(app_client):
- """Test reading an item from a collection which does not exist"""
- resp = await app_client.get("/collections/invalid-collection/items/some-item")
- assert resp.status_code == 404
-
-
-async def test_pagination_item_collection(
- app_client, load_test_data, load_test_collection
-):
- """Test item collection pagination links (paging extension)"""
- test_item = load_test_data("test_item.json")
- ids = []
-
- # Ingest 5 items
- for idx in range(5):
- uid = str(uuid.uuid4())
- test_item["id"] = uid
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
- ids.append(uid)
-
- # Paginate through all 5 items with a limit of 1 (expecting 5 requests)
- page = await app_client.get(
- f"/collections/{test_item['collection']}/items", params={"limit": 1}
- )
- idx = 0
- item_ids = []
- while True:
- idx += 1
- page_data = page.json()
- item_ids.append(page_data["features"][0]["id"])
- nextlink = [
- link["href"] for link in page_data["links"] if link["rel"] == "next"
- ]
- if len(nextlink) < 1:
- break
- page = await app_client.get(nextlink.pop())
- if idx >= 10:
- assert False
-
- # Our limit is 1 so we expect len(ids) number of requests before we run out of pages
- assert idx == len(ids)
-
- # Confirm we have paginated through all items
- assert not set(item_ids) - set(ids)
-
-
-async def test_pagination_post(app_client, load_test_data, load_test_collection):
- """Test POST pagination (paging extension)"""
- test_item = load_test_data("test_item.json")
- ids = []
-
- # Ingest 5 items
- for idx in range(5):
- uid = str(uuid.uuid4())
- test_item["id"] = uid
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
- ids.append(uid)
-
- # Paginate through all 5 items with a limit of 1 (expecting 5 requests)
- request_body = {
- "filter-lang": "cql2-json",
- "filter": {"op": "in", "args": [{"property": "id"}, ids]},
- "limit": 1,
- }
- page = await app_client.post("/search", json=request_body)
- idx = 0
- item_ids = []
- while True:
- idx += 1
- page_data = page.json()
- item_ids.append(page_data["features"][0]["id"])
- next_link = list(filter(lambda link: link["rel"] == "next", page_data["links"]))
- if not next_link:
- break
- # Merge request bodies
- request_body.update(next_link[0]["body"])
- page = await app_client.post("/search", json=request_body)
-
- if idx > 10:
- assert False
-
- # Our limit is 1 so we expect len(ids) number of requests before we run out of pages
- assert idx == len(ids)
-
- # Confirm we have paginated through all items
- assert not set(item_ids) - set(ids)
-
-
-async def test_pagination_token_idempotent(
- app_client, load_test_data, load_test_collection
-):
- """Test that pagination tokens are idempotent (paging extension)"""
- test_item = load_test_data("test_item.json")
- ids = []
-
- # Ingest 5 items
- for idx in range(5):
- uid = str(uuid.uuid4())
- test_item["id"] = uid
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
- ids.append(uid)
-
- page = await app_client.post(
- "/search",
- json={
- "filter-lang": "cql2-json",
- "filter": {"op": "in", "args": [{"property": "id"}, ids]},
- "limit": 3,
- },
- )
- page_data = page.json()
- next_link = list(filter(lambda link: link["rel"] == "next", page_data["links"]))
-
- # Confirm token is idempotent
- resp1 = await app_client.get(
- "/search", params=parse_qs(urlparse(next_link[0]["href"]).query)
- )
- resp2 = await app_client.get(
- "/search", params=parse_qs(urlparse(next_link[0]["href"]).query)
- )
- resp1_data = resp1.json()
- resp2_data = resp2.json()
-
- # Two different requests with the same pagination token should return the same items
- assert [item["id"] for item in resp1_data["features"]] == [
- item["id"] for item in resp2_data["features"]
- ]
-
-
-async def test_field_extension_get(app_client, load_test_data, load_test_collection):
- """Test GET search with included fields (fields extension)"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- params = {"fields": "+properties.proj:epsg,+properties.gsd,+collection"}
- resp = await app_client.get("/search", params=params)
- feat_properties = resp.json()["features"][0]["properties"]
- assert not set(feat_properties) - {"proj:epsg", "gsd", "datetime"}
-
-
-async def test_field_extension_post(app_client, load_test_data, load_test_collection):
- """Test POST search with included and excluded fields (fields extension)"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- body = {
- "fields": {
- "exclude": ["assets.B1"],
- "include": [
- "properties.eo:cloud_cover",
- "properties.orientation",
- "assets",
- "collection",
- ],
- }
- }
-
- resp = await app_client.post("/search", json=body)
- resp_json = resp.json()
- assert "B1" not in resp_json["features"][0]["assets"].keys()
- assert not set(resp_json["features"][0]["properties"]) - {
- "orientation",
- "eo:cloud_cover",
- "datetime",
- }
-
-
-async def test_field_extension_exclude_and_include(
- app_client, load_test_data, load_test_collection
-):
- """Test POST search including/excluding same field (fields extension)"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- body = {
- "fields": {
- "exclude": ["properties.eo:cloud_cover"],
- "include": ["properties.eo:cloud_cover", "collection"],
- }
- }
-
- resp = await app_client.post("/search", json=body)
- resp_json = resp.json()
- assert "properties" not in resp_json["features"][0]
-
-
-async def test_field_extension_exclude_default_includes(
- app_client, load_test_data, load_test_collection
-):
- """Test POST search excluding a forbidden field (fields extension)"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- body = {"fields": {"exclude": ["geometry"]}}
-
- resp = await app_client.post("/search", json=body)
- resp_json = resp.json()
- assert "geometry" not in resp_json["features"][0]
-
-
-async def test_field_extension_include_multiple_subkeys(
- app_client, load_test_item, load_test_collection
-):
- """Test that multiple subkeys of an object field are included"""
- body = {"fields": {"include": ["properties.width", "properties.height"]}}
-
- resp = await app_client.post("/search", json=body)
- assert resp.status_code == 200
- resp_json = resp.json()
-
- resp_prop_keys = resp_json["features"][0]["properties"].keys()
- assert set(resp_prop_keys) == set(["width", "height"])
-
-
-async def test_field_extension_include_multiple_deeply_nested_subkeys(
- app_client, load_test_item, load_test_collection
-):
- """Test that multiple deeply nested subkeys of an object field are included"""
- body = {"fields": {"include": ["assets.ANG.type", "assets.ANG.href"]}}
-
- resp = await app_client.post("/search", json=body)
- assert resp.status_code == 200
- resp_json = resp.json()
-
- resp_assets = resp_json["features"][0]["assets"]
- assert set(resp_assets.keys()) == set(["ANG"])
- assert set(resp_assets["ANG"].keys()) == set(["type", "href"])
-
-
-async def test_field_extension_exclude_multiple_deeply_nested_subkeys(
- app_client, load_test_item, load_test_collection
-):
- """Test that multiple deeply nested subkeys of an object field are excluded"""
- body = {"fields": {"exclude": ["assets.ANG.type", "assets.ANG.href"]}}
-
- resp = await app_client.post("/search", json=body)
- assert resp.status_code == 200
- resp_json = resp.json()
-
- resp_assets = resp_json["features"][0]["assets"]
- assert len(resp_assets.keys()) > 0
- assert "type" not in resp_assets["ANG"]
- assert "href" not in resp_assets["ANG"]
-
-
-async def test_field_extension_exclude_deeply_nested_included_subkeys(
- app_client, load_test_item, load_test_collection
-):
- """Test that deeply nested keys of a nested object that was included are excluded"""
- body = {
- "fields": {
- "include": ["assets.ANG.type", "assets.ANG.href"],
- "exclude": ["assets.ANG.href"],
- }
- }
-
- resp = await app_client.post("/search", json=body)
- assert resp.status_code == 200
- resp_json = resp.json()
-
- resp_assets = resp_json["features"][0]["assets"]
- assert "type" in resp_assets["ANG"]
- assert "href" not in resp_assets["ANG"]
-
-
-async def test_field_extension_exclude_links(
- app_client, load_test_item, load_test_collection
-):
- """Links have special injection behavior, ensure they can be excluded with the fields extension"""
- body = {"fields": {"exclude": ["links"]}}
-
- resp = await app_client.post("/search", json=body)
- assert resp.status_code == 200
- resp_json = resp.json()
-
- assert "links" not in resp_json["features"][0]
-
-
-async def test_field_extension_include_only_non_existant_field(
- app_client, load_test_item, load_test_collection
-):
- """Including only a non-existant field should return the full item"""
- body = {"fields": {"include": ["non_existant_field"]}}
-
- resp = await app_client.post("/search", json=body)
- assert resp.status_code == 200
- resp_json = resp.json()
-
- assert list(resp_json["features"][0].keys()) == ["id", "collection", "links"]
-
-
-async def test_search_intersects_and_bbox(app_client):
- """Test POST search intersects and bbox are mutually exclusive (core)"""
- bbox = [-118, 34, -117, 35]
- geoj = Polygon.from_bounds(*bbox).__geo_interface__
- params = {"bbox": bbox, "intersects": geoj}
- resp = await app_client.post("/search", json=params)
- assert resp.status_code == 400
-
-
-async def test_get_missing_item(app_client, load_test_data):
- """Test read item which does not exist (transactions extension)"""
- test_coll = load_test_data("test_collection.json")
- resp = await app_client.get(f"/collections/{test_coll['id']}/items/invalid-item")
- assert resp.status_code == 404
-
-
-async def test_relative_link_construction(app):
- req = Request(
- scope={
- "type": "http",
- "scheme": "http",
- "method": "PUT",
- "root_path": "/stac", # root_path should not have proto, domain, or port
- "path": "/",
- "raw_path": b"/tab/abc",
- "query_string": b"",
- "headers": {},
- "app": app,
- "server": ("test", HTTP_PORT),
- }
- )
- links = CollectionLinks(collection_id="naip", request=req)
- assert links.link_items()["href"] == (
- "http://test/stac{}/collections/naip/items".format(app.state.router_prefix)
- )
-
-
-async def test_search_bbox_errors(app_client):
- body = {"query": {"bbox": [0]}}
- resp = await app_client.post("/search", json=body)
- assert resp.status_code == 400
-
- body = {"query": {"bbox": [100.0, 0.0, 0.0, 105.0, 1.0, 1.0]}}
- resp = await app_client.post("/search", json=body)
- assert resp.status_code == 400
-
- params = {"bbox": "100.0,0.0,0.0,105.0"}
- resp = await app_client.get("/search", params=params)
- assert resp.status_code == 400
-
-
-async def test_preserves_extra_link(
- app_client: AsyncClient, load_test_data, load_test_collection
-):
- coll = load_test_collection
- test_item = load_test_data("test_item.json")
- expected_href = urljoin(str(app_client.base_url), "preview.html")
-
- resp = await app_client.post(f"/collections/{coll.id}/items", json=test_item)
- assert resp.status_code == 200
-
- response_item = await app_client.get(
- f"/collections/{coll.id}/items/{test_item['id']}",
- params={"limit": 1},
- )
- assert response_item.status_code == 200
- item = response_item.json()
- extra_link = [link for link in item["links"] if link["rel"] == "preview"]
- assert extra_link
- assert extra_link[0]["href"] == expected_href
-
-
-async def test_item_search_get_filter_extension_cql_explicitlang(
- app_client, load_test_data, load_test_collection
-):
- """Test GET search with JSONB query (cql json filter extension)"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # EPSG is a JSONB key
- params = {
- "collections": [test_item["collection"]],
- "filter-lang": "cql-json",
- "filter": {
- "gt": [
- {"property": "proj:epsg"},
- test_item["properties"]["proj:epsg"] + 1,
- ]
- },
- }
- resp = await app_client.post("/search", json=params)
- resp_json = resp.json()
-
- assert resp.status_code == 200
- assert len(resp_json.get("features")) == 0
-
- params = {
- "collections": [test_item["collection"]],
- "filter-lang": "cql-json",
- "filter": {
- "eq": [
- {"property": "proj:epsg"},
- test_item["properties"]["proj:epsg"],
- ]
- },
- }
- resp = await app_client.post("/search", json=params)
- resp_json = resp.json()
- assert len(resp.json()["features"]) == 1
- assert (
- resp_json["features"][0]["properties"]["proj:epsg"]
- == test_item["properties"]["proj:epsg"]
- )
-
-
-async def test_item_search_get_filter_extension_cql2_2(
- app_client, load_test_data, load_test_collection
-):
- """Test GET search with JSONB query (cql json filter extension)"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # EPSG is a JSONB key
- params = {
- "filter-lang": "cql2-json",
- "filter": {
- "op": "and",
- "args": [
- {
- "op": "eq",
- "args": [
- {"property": "proj:epsg"},
- test_item["properties"]["proj:epsg"] + 1,
- ],
- },
- {
- "op": "in",
- "args": [
- {"property": "collection"},
- [test_item["collection"]],
- ],
- },
- ],
- },
- }
- resp = await app_client.post("/search", json=params)
- resp_json = resp.json()
-
- assert resp.status_code == 200
- assert len(resp_json.get("features")) == 0
-
- params = {
- "filter-lang": "cql2-json",
- "filter": {
- "op": "and",
- "args": [
- {
- "op": "eq",
- "args": [
- {"property": "proj:epsg"},
- test_item["properties"]["proj:epsg"],
- ],
- },
- {
- "op": "in",
- "args": [
- {"property": "collection"},
- [test_item["collection"]],
- ],
- },
- ],
- },
- }
- resp = await app_client.post("/search", json=params)
- resp_json = resp.json()
- assert len(resp.json()["features"]) == 1
- assert (
- resp_json["features"][0]["properties"]["proj:epsg"]
- == test_item["properties"]["proj:epsg"]
- )
-
-
-async def test_search_datetime_validation_errors(app_client):
- bad_datetimes = [
- "37-01-01T12:00:27.87Z",
- "1985-13-12T23:20:50.52Z",
- "1985-12-32T23:20:50.52Z",
- "1985-12-01T25:20:50.52Z",
- "1985-12-01T00:60:50.52Z",
- "1985-12-01T00:06:61.52Z",
- "1990-12-31T23:59:61Z",
- "1986-04-12T23:20:50.52Z/1985-04-12T23:20:50.52Z",
- ]
- for dt in bad_datetimes:
- body = {"query": {"datetime": dt}}
- resp = await app_client.post("/search", json=body)
- assert resp.status_code == 400
-
- resp = await app_client.get("/search?datetime={}".format(dt))
- assert resp.status_code == 400
-
-
-async def test_filter_cql2text(app_client, load_test_data, load_test_collection):
- """Test GET search with cql2-text"""
- test_item = load_test_data("test_item.json")
- resp = await app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- epsg = test_item["properties"]["proj:epsg"]
- collection = test_item["collection"]
-
- filter = f"proj:epsg={epsg} AND collection = '{collection}'"
- params = {"filter": filter, "filter-lang": "cql2-text"}
- resp = await app_client.get("/search", params=params)
- resp_json = resp.json()
- assert len(resp.json()["features"]) == 1
- assert (
- resp_json["features"][0]["properties"]["proj:epsg"]
- == test_item["properties"]["proj:epsg"]
- )
-
- filter = f"proj:epsg={epsg + 1} AND collection = '{collection}'"
- params = {"filter": filter, "filter-lang": "cql2-text"}
- resp = await app_client.get("/search", params=params)
- resp_json = resp.json()
- assert len(resp.json()["features"]) == 0
-
-
-async def test_item_merge_raster_bands(
- app_client, load_test2_item, load_test2_collection
-):
- resp = await app_client.get("/collections/test2-collection/items/test2-item")
- resp_json = resp.json()
- red_bands = resp_json["assets"]["red"]["raster:bands"]
-
- # The merged item should have merged the band dicts from base and item
- # into a single dict
- assert len(red_bands) == 1
- # The merged item should have the full 6 bands
- assert len(red_bands[0].keys()) == 6
- # The merged item should have kept the item value rather than the base value
- assert red_bands[0]["offset"] == 2.03976
-
-
-@pytest.mark.asyncio
-async def test_get_collection_items_forwarded_header(
- app_client, load_test_collection, load_test_item
-):
- coll = load_test_collection
- resp = await app_client.get(
- f"/collections/{coll.id}/items",
- headers={"Forwarded": "proto=https;host=test:1234"},
- )
- for link in resp.json()["features"][0]["links"]:
- assert link["href"].startswith("https://test:1234/")
-
-
-@pytest.mark.asyncio
-async def test_get_collection_items_x_forwarded_headers(
- app_client, load_test_collection, load_test_item
-):
- coll = load_test_collection
- resp = await app_client.get(
- f"/collections/{coll.id}/items",
- headers={
- "X-Forwarded-Port": "1234",
- "X-Forwarded-Proto": "https",
- },
- )
- for link in resp.json()["features"][0]["links"]:
- assert link["href"].startswith("https://test:1234/")
-
-
-@pytest.mark.asyncio
-async def test_get_collection_items_duplicate_forwarded_headers(
- app_client, load_test_collection, load_test_item
-):
- coll = load_test_collection
- resp = await app_client.get(
- f"/collections/{coll.id}/items",
- headers={
- "Forwarded": "proto=https;host=test:1234",
- "X-Forwarded-Port": "4321",
- "X-Forwarded-Proto": "http",
- },
- )
- for link in resp.json()["features"][0]["links"]:
- assert link["href"].startswith("https://test:1234/")
diff --git a/stac_fastapi/pgstac/tests/resources/test_mgmt.py b/stac_fastapi/pgstac/tests/resources/test_mgmt.py
deleted file mode 100644
index 9d2bc3dca..000000000
--- a/stac_fastapi/pgstac/tests/resources/test_mgmt.py
+++ /dev/null
@@ -1,9 +0,0 @@
-async def test_ping_no_param(app_client):
- """
- Test ping endpoint with a mocked client.
- Args:
- app_client (TestClient): mocked client fixture
- """
- res = await app_client.get("/_mgmt/ping")
- assert res.status_code == 200
- assert res.json() == {"message": "PONG"}
diff --git a/stac_fastapi/sqlalchemy/README.md b/stac_fastapi/sqlalchemy/README.md
deleted file mode 100644
index 40bd804ed..000000000
--- a/stac_fastapi/sqlalchemy/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Requirements
-
-The SQLAlchemy backend requires **PostGIS>=3**.
diff --git a/stac_fastapi/sqlalchemy/alembic.ini b/stac_fastapi/sqlalchemy/alembic.ini
deleted file mode 100644
index 7dec63538..000000000
--- a/stac_fastapi/sqlalchemy/alembic.ini
+++ /dev/null
@@ -1,85 +0,0 @@
-# A generic, single database configuration.
-
-[alembic]
-# path to migration scripts
-script_location = alembic
-
-# template used to generate migration files
-# file_template = %%(rev)s_%%(slug)s
-
-# timezone to use when rendering the date
-# within the migration file as well as the filename.
-# string value is passed to dateutil.tz.gettz()
-# leave blank for localtime
-# timezone =
-
-# max length of characters to apply to the
-# "slug" field
-# truncate_slug_length = 40
-
-# set to 'true' to run the environment during
-# the 'revision' command, regardless of autogenerate
-# revision_environment = false
-
-# set to 'true' to allow .pyc and .pyo files without
-# a source .py file to be detected as revisions in the
-# versions/ directory
-# sourceless = false
-
-# version location specification; this defaults
-# to alembic/versions. When using multiple version
-# directories, initial revisions must be specified with --version-path
-# version_locations = %(here)s/bar %(here)s/bat alembic/versions
-
-# the output encoding used when revision files
-# are written from script.py.mako
-# output_encoding = utf-8
-
-;sqlalchemy.url = postgresql://alex:password@localhost:5432/postgres
-
-
-[post_write_hooks]
-# post_write_hooks defines scripts or Python functions that are run
-# on newly generated revision scripts. See the documentation for further
-# detail and examples
-
-# format using "black" - use the console_scripts runner, against the "black" entrypoint
-# hooks=black
-# black.type=console_scripts
-# black.entrypoint=black
-# black.options=-l 79
-
-# Logging configuration
-[loggers]
-keys = root,sqlalchemy,alembic
-
-[handlers]
-keys = console
-
-[formatters]
-keys = generic
-
-[logger_root]
-level = WARN
-handlers = console
-qualname =
-
-[logger_sqlalchemy]
-level = WARN
-handlers =
-qualname = sqlalchemy.engine
-
-[logger_alembic]
-level = INFO
-handlers =
-qualname = alembic
-
-[handler_console]
-class = StreamHandler
-args = (sys.stderr,)
-level = NOTSET
-formatter = generic
-
-[formatter_generic]
-format = %(levelname)-5.5s [%(name)s] %(message)s
-datefmt = %H:%M:%S
diff --git a/stac_fastapi/sqlalchemy/alembic/README b/stac_fastapi/sqlalchemy/alembic/README
deleted file mode 100644
index 98e4f9c44..000000000
--- a/stac_fastapi/sqlalchemy/alembic/README
+++ /dev/null
@@ -1 +0,0 @@
-Generic single-database configuration.
\ No newline at end of file
diff --git a/stac_fastapi/sqlalchemy/alembic/env.py b/stac_fastapi/sqlalchemy/alembic/env.py
deleted file mode 100644
index 20af555b0..000000000
--- a/stac_fastapi/sqlalchemy/alembic/env.py
+++ /dev/null
@@ -1,90 +0,0 @@
-"""Migration environment."""
-import os
-from logging.config import fileConfig
-
-from alembic import context
-from sqlalchemy import engine_from_config, pool
-
-# this is the Alembic Config object, which provides
-# access to the values within the .ini file in use.
-config = context.config
-
-# Interpret the config file for Python logging.
-# This line sets up loggers basically.
-fileConfig(config.config_file_name)
-
-# add your model's MetaData object here
-# for 'autogenerate' support
-# from myapp import mymodel
-# target_metadata = mymodel.Base.metadata
-target_metadata = None
-
-# other values from the config, defined by the needs of env.py,
-# can be acquired:
-# my_important_option = config.get_main_option("my_important_option")
-# ... etc.
-
-
-def get_connection_url() -> str:
- """
- Get connection URL from environment variables
- (see environment variables set in docker-compose)
- """
- postgres_user = os.environ["POSTGRES_USER"]
- postgres_pass = os.environ["POSTGRES_PASS"]
- postgres_host = os.environ["POSTGRES_HOST"]
- postgres_port = os.environ["POSTGRES_PORT"]
- postgres_dbname = os.environ["POSTGRES_DBNAME"]
- return f"postgresql://{postgres_user}:{postgres_pass}@{postgres_host}:{postgres_port}/{postgres_dbname}"
-
-
-def run_migrations_offline():
- """Run migrations in 'offline' mode.
-
- This configures the context with just a URL
- and not an Engine, though an Engine is acceptable
- here as well. By skipping the Engine creation
- we don't even need a DBAPI to be available.
-
- Calls to context.execute() here emit the given string to the
- script output.
-
- """
- url = get_connection_url()
- context.configure(
- url=url,
- target_metadata=target_metadata,
- literal_binds=True,
- dialect_opts={"paramstyle": "named"},
- )
-
- with context.begin_transaction():
- context.run_migrations()
-
-
-def run_migrations_online():
- """Run migrations in 'online' mode.
-
- In this scenario we need to create an Engine
- and associate a connection with the context.
-
- """
- configuration = config.get_section(config.config_ini_section)
- configuration["sqlalchemy.url"] = get_connection_url()
- connectable = engine_from_config(
- configuration,
- prefix="sqlalchemy.",
- poolclass=pool.NullPool,
- )
-
- with connectable.connect() as connection:
- context.configure(connection=connection, target_metadata=target_metadata)
-
- with context.begin_transaction():
- context.run_migrations()
-
-
-if context.is_offline_mode():
- run_migrations_offline()
-else:
- run_migrations_online()
diff --git a/stac_fastapi/sqlalchemy/alembic/script.py.mako b/stac_fastapi/sqlalchemy/alembic/script.py.mako
deleted file mode 100644
index 2c0156303..000000000
--- a/stac_fastapi/sqlalchemy/alembic/script.py.mako
+++ /dev/null
@@ -1,24 +0,0 @@
-"""${message}
-
-Revision ID: ${up_revision}
-Revises: ${down_revision | comma,n}
-Create Date: ${create_date}
-
-"""
-from alembic import op
-import sqlalchemy as sa
-${imports if imports else ""}
-
-# revision identifiers, used by Alembic.
-revision = ${repr(up_revision)}
-down_revision = ${repr(down_revision)}
-branch_labels = ${repr(branch_labels)}
-depends_on = ${repr(depends_on)}
-
-
-def upgrade():
- ${upgrades if upgrades else "pass"}
-
-
-def downgrade():
- ${downgrades if downgrades else "pass"}
diff --git a/stac_fastapi/sqlalchemy/alembic/versions/131aab4d9e49_create_tables.py b/stac_fastapi/sqlalchemy/alembic/versions/131aab4d9e49_create_tables.py
deleted file mode 100644
index efc333803..000000000
--- a/stac_fastapi/sqlalchemy/alembic/versions/131aab4d9e49_create_tables.py
+++ /dev/null
@@ -1,76 +0,0 @@
-"""create initial schema
-
-Revision ID: 131aab4d9e49
-Revises:
-Create Date: 2020-02-09 13:03:09.336631
-
-""" # noqa
-import sqlalchemy as sa
-from alembic import op
-from geoalchemy2.types import Geometry
-from sqlalchemy.dialects.postgresql import JSONB
-
-# revision identifiers, used by Alembic.
-revision = "131aab4d9e49"
-down_revision = None
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
- """upgrade to this revision"""
- op.execute("CREATE SCHEMA data")
- op.execute("CREATE EXTENSION IF NOT EXISTS postgis")
-
- # Create collections table
- op.create_table(
- "collections",
- sa.Column("id", sa.VARCHAR(1024), nullable=False, primary_key=True),
- sa.Column("stac_version", sa.VARCHAR(300)),
- sa.Column("title", sa.VARCHAR(1024)),
- sa.Column("stac_extensions", sa.ARRAY(sa.VARCHAR(300)), nullable=True),
- sa.Column("description", sa.VARCHAR(1024), nullable=False),
- sa.Column("keywords", sa.ARRAY(sa.VARCHAR(300))),
- sa.Column("version", sa.VARCHAR(300)),
- sa.Column("license", sa.VARCHAR(300), nullable=False),
- sa.Column("providers", JSONB),
- sa.Column("summaries", JSONB, nullable=True),
- sa.Column("extent", JSONB),
- sa.Column("links", JSONB, nullable=True),
- schema="data",
- )
-
- # Create items table
- op.create_table(
- "items",
- sa.Column("id", sa.VARCHAR(1024), nullable=False, primary_key=True),
- sa.Column("stac_version", sa.VARCHAR(300)),
- sa.Column("stac_extensions", sa.ARRAY(sa.VARCHAR(300)), nullable=True),
- sa.Column("geometry", Geometry("POLYGON", srid=4326, spatial_index=True)),
- sa.Column("bbox", sa.ARRAY(sa.NUMERIC), nullable=False),
- sa.Column("properties", JSONB),
- sa.Column("assets", JSONB),
- sa.Column("collection_id", sa.VARCHAR(1024), nullable=False, index=True),
- # These are usually in properties but defined as their own fields for indexing
- sa.Column("datetime", sa.TIMESTAMP, nullable=False, index=True),
- sa.Column("links", JSONB, nullable=True),
- sa.ForeignKeyConstraint(["collection_id"], ["data.collections.id"]),
- schema="data",
- )
-
- # Create pagination token table
- op.create_table(
- "tokens",
- sa.Column("id", sa.VARCHAR(100), nullable=False, primary_key=True),
- sa.Column("keyset", sa.VARCHAR(1000), nullable=False),
- schema="data",
- )
-
-
-def downgrade():
- """downgrade to previous revision"""
- op.execute("DROP TABLE data.items")
- op.execute("DROP TABLE data.collections")
- op.execute("DROP TABLE data.tokens")
- op.execute("DROP SCHEMA data")
- op.execute("DROP EXTENSION IF EXISTS postgis")
diff --git a/stac_fastapi/sqlalchemy/alembic/versions/407037cb1636_add_stac_1_0_0_fields.py b/stac_fastapi/sqlalchemy/alembic/versions/407037cb1636_add_stac_1_0_0_fields.py
deleted file mode 100644
index fdf15cde6..000000000
--- a/stac_fastapi/sqlalchemy/alembic/versions/407037cb1636_add_stac_1_0_0_fields.py
+++ /dev/null
@@ -1,27 +0,0 @@
-"""add-stac-1.0.0-fields
-
-Revision ID: 407037cb1636
-Revises: 77c019af60bf
-Create Date: 2021-07-07 16:10:03.196942
-
-"""
-import sqlalchemy as sa
-from alembic import op
-
-# revision identifiers, used by Alembic.
-revision = "407037cb1636"
-down_revision = "77c019af60bf"
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
- op.add_column(
- "collections",
- sa.Column("type", sa.VARCHAR(300), default="collection", nullable=False),
- schema="data",
- )
-
-
-def downgrade():
- op.drop_column("collections", "type")
diff --git a/stac_fastapi/sqlalchemy/alembic/versions/5909bd10f2e6_change_item_geometry_column_type.py b/stac_fastapi/sqlalchemy/alembic/versions/5909bd10f2e6_change_item_geometry_column_type.py
deleted file mode 100644
index 2c1edd985..000000000
--- a/stac_fastapi/sqlalchemy/alembic/versions/5909bd10f2e6_change_item_geometry_column_type.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""change item geometry column type
-
-Revision ID: 5909bd10f2e6
-Revises: 821aa04011e8
-Create Date: 2021-11-23 10:14:17.974565
-
-"""
-from alembic import op
-
-from stac_fastapi.sqlalchemy.models.database import GeojsonGeometry
-
-# revision identifiers, used by Alembic.
-revision = "5909bd10f2e6"
-down_revision = "821aa04011e8"
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
- op.alter_column(
- schema="data",
- table_name="items",
- column_name="geometry",
- type_=GeojsonGeometry("Geometry", srid=4326, spatial_index=True),
- )
-
-
-def downgrade():
- op.alter_column(
- schema="data",
- table_name="items",
- column_name="geometry",
- type_=GeojsonGeometry("Polygon", srid=4326, spatial_index=True),
- )
diff --git a/stac_fastapi/sqlalchemy/alembic/versions/7016c1bf3fbf_make_item_geometry_and_bbox_nullable.py b/stac_fastapi/sqlalchemy/alembic/versions/7016c1bf3fbf_make_item_geometry_and_bbox_nullable.py
deleted file mode 100644
index 804361b02..000000000
--- a/stac_fastapi/sqlalchemy/alembic/versions/7016c1bf3fbf_make_item_geometry_and_bbox_nullable.py
+++ /dev/null
@@ -1,46 +0,0 @@
-"""Make item geometry and bbox nullable
-
-Revision ID: 7016c1bf3fbf
-Revises: 5909bd10f2e6
-Create Date: 2022-04-28 10:40:06.856826
-
-"""
-from alembic import op
-
-# revision identifiers, used by Alembic.
-revision = "7016c1bf3fbf"
-down_revision = "5909bd10f2e6"
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
- op.alter_column(
- schema="data",
- table_name="items",
- column_name="geometry",
- nullable=True,
- )
- op.alter_column(
- schema="data",
- table_name="items",
- column_name="bbox",
- nullable=True,
- )
-
-
-def downgrade():
- # Downgrading will require the user to update or remove all null geometry
- # cases from the DB, otherwise the downgrade migration will fail.
- op.alter_column(
- schema="data",
- table_name="items",
- column_name="geometry",
- nullable=False,
- )
- op.alter_column(
- schema="data",
- table_name="items",
- column_name="bbox",
- nullable=False,
- )
diff --git a/stac_fastapi/sqlalchemy/alembic/versions/77c019af60bf_use_timestamptz_rather_than_timestamp.py b/stac_fastapi/sqlalchemy/alembic/versions/77c019af60bf_use_timestamptz_rather_than_timestamp.py
deleted file mode 100644
index 0c6085fbd..000000000
--- a/stac_fastapi/sqlalchemy/alembic/versions/77c019af60bf_use_timestamptz_rather_than_timestamp.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""use timestamptz rather than timestamp
-
-Revision ID: 77c019af60bf
-Revises: 131aab4d9e49
-Create Date: 2021-03-02 11:51:43.539119
-
-"""
-from alembic import op
-
-# revision identifiers, used by Alembic.
-revision = "77c019af60bf"
-down_revision = "131aab4d9e49"
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
- """upgrade to this revision"""
- op.execute(
- """
- ALTER TABLE
- data.items
- ALTER COLUMN datetime
- TYPE timestamptz
- ;
- """
- )
-
-
-def downgrade():
- """downgrade from this revision"""
- op.execute(
- """
- ALTER TABLE
- data.items
- ALTER COLUMN datetime
- TYPE timestamp
- ;
- """
- )
diff --git a/stac_fastapi/sqlalchemy/alembic/versions/821aa04011e8_change_pri_key_for_item.py b/stac_fastapi/sqlalchemy/alembic/versions/821aa04011e8_change_pri_key_for_item.py
deleted file mode 100644
index 335b3e624..000000000
--- a/stac_fastapi/sqlalchemy/alembic/versions/821aa04011e8_change_pri_key_for_item.py
+++ /dev/null
@@ -1,24 +0,0 @@
-"""Change pri key for Item
-
-Revision ID: 821aa04011e8
-Revises: 407037cb1636
-Create Date: 2021-10-11 12:10:34.148098
-
-"""
-from alembic import op
-
-# revision identifiers, used by Alembic.
-revision = "821aa04011e8"
-down_revision = "407037cb1636"
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
- op.drop_constraint("items_pkey", "items", schema="data")
- op.create_primary_key("items_pkey", "items", ["id", "collection_id"], schema="data")
-
-
-def downgrade():
- op.drop_constraint("items_pkey", "items", schema="data")
- op.create_primary_key("items_pkey", "items", ["id"], schema="data")
diff --git a/stac_fastapi/sqlalchemy/pytest.ini b/stac_fastapi/sqlalchemy/pytest.ini
deleted file mode 100644
index f11bd4cec..000000000
--- a/stac_fastapi/sqlalchemy/pytest.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[pytest]
-testpaths = tests
-addopts = -sv
\ No newline at end of file
diff --git a/stac_fastapi/sqlalchemy/setup.cfg b/stac_fastapi/sqlalchemy/setup.cfg
deleted file mode 100644
index 46ac9c3b1..000000000
--- a/stac_fastapi/sqlalchemy/setup.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[metadata]
-version = attr: stac_fastapi.sqlalchemy.version.__version__
diff --git a/stac_fastapi/sqlalchemy/setup.py b/stac_fastapi/sqlalchemy/setup.py
deleted file mode 100644
index fd4f53027..000000000
--- a/stac_fastapi/sqlalchemy/setup.py
+++ /dev/null
@@ -1,62 +0,0 @@
-"""stac_fastapi: sqlalchemy module."""
-
-from setuptools import find_namespace_packages, setup
-
-with open("README.md") as f:
- desc = f.read()
-
-install_requires = [
- "attrs",
- "pydantic[dotenv]",
- "stac_pydantic>=2.0.3",
- "stac-fastapi.types",
- "stac-fastapi.api",
- "stac-fastapi.extensions",
- "sqlakeyset",
- "geoalchemy2<0.8.0",
- "sqlalchemy==1.3.23",
- "shapely",
- "psycopg2-binary",
- "alembic",
- "fastapi-utils",
-]
-
-extra_reqs = {
- "dev": [
- "pytest",
- "pytest-cov",
- "pre-commit",
- "requests",
- ],
- "docs": ["mkdocs", "mkdocs-material", "pdocs"],
- "server": ["uvicorn[standard]==0.19.0"],
-}
-
-
-setup(
- name="stac-fastapi.sqlalchemy",
- description="An implementation of STAC API based on the FastAPI framework.",
- long_description=desc,
- long_description_content_type="text/markdown",
- python_requires=">=3.8",
- classifiers=[
- "Intended Audience :: Developers",
- "Intended Audience :: Information Technology",
- "Intended Audience :: Science/Research",
- "Programming Language :: Python :: 3.8",
- "License :: OSI Approved :: MIT License",
- ],
- keywords="STAC FastAPI COG",
- author="Arturo Engineering",
- author_email="engineering@arturo.ai",
- url="https://github.com/stac-utils/stac-fastapi",
- license="MIT",
- packages=find_namespace_packages(exclude=["alembic", "tests", "scripts"]),
- zip_safe=False,
- install_requires=install_requires,
- tests_require=extra_reqs["dev"],
- extras_require=extra_reqs,
- entry_points={
- "console_scripts": ["stac-fastapi-sqlalchemy=stac_fastapi.sqlalchemy.app:run"]
- },
-)
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/__init__.py b/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/__init__.py
deleted file mode 100644
index ee2522f79..000000000
--- a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""sqlalchemy submodule."""
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/app.py b/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/app.py
deleted file mode 100644
index 038d92b80..000000000
--- a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/app.py
+++ /dev/null
@@ -1,80 +0,0 @@
-"""FastAPI application."""
-import os
-
-from stac_fastapi.api.app import StacApi
-from stac_fastapi.api.models import create_get_request_model, create_post_request_model
-from stac_fastapi.extensions.core import (
- ContextExtension,
- FieldsExtension,
- SortExtension,
- TokenPaginationExtension,
- TransactionExtension,
-)
-from stac_fastapi.extensions.third_party import BulkTransactionExtension
-from stac_fastapi.sqlalchemy.config import SqlalchemySettings
-from stac_fastapi.sqlalchemy.core import CoreCrudClient
-from stac_fastapi.sqlalchemy.extensions import QueryExtension
-from stac_fastapi.sqlalchemy.session import Session
-from stac_fastapi.sqlalchemy.transactions import (
- BulkTransactionsClient,
- TransactionsClient,
-)
-
-settings = SqlalchemySettings()
-session = Session.create_from_settings(settings)
-extensions = [
- TransactionExtension(client=TransactionsClient(session=session), settings=settings),
- BulkTransactionExtension(client=BulkTransactionsClient(session=session)),
- FieldsExtension(),
- QueryExtension(),
- SortExtension(),
- TokenPaginationExtension(),
- ContextExtension(),
-]
-
-post_request_model = create_post_request_model(extensions)
-
-api = StacApi(
- settings=settings,
- extensions=extensions,
- client=CoreCrudClient(
- session=session, extensions=extensions, post_request_model=post_request_model
- ),
- search_get_request_model=create_get_request_model(extensions),
- search_post_request_model=post_request_model,
-)
-app = api.app
-
-
-def run():
- """Run app from command line using uvicorn if available."""
- try:
- import uvicorn
-
- uvicorn.run(
- "stac_fastapi.sqlalchemy.app:app",
- host=settings.app_host,
- port=settings.app_port,
- log_level="info",
- reload=settings.reload,
- root_path=os.getenv("UVICORN_ROOT_PATH", ""),
- )
- except ImportError:
- raise RuntimeError("Uvicorn must be installed in order to use command")
-
-
-if __name__ == "__main__":
- run()
-
-
-def create_handler(app):
- """Create a handler to use with AWS Lambda if mangum available."""
- try:
- from mangum import Mangum
-
- return Mangum(app)
- except ImportError:
- return None
-
-
-handler = create_handler(app)
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/config.py b/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/config.py
deleted file mode 100644
index 340ef62b0..000000000
--- a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/config.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""Postgres API configuration."""
-from typing import Set
-
-from stac_fastapi.types.config import ApiSettings
-
-
-class SqlalchemySettings(ApiSettings):
- """Postgres-specific API settings.
-
- Attributes:
- postgres_user: postgres username.
- postgres_pass: postgres password.
- postgres_host_reader: hostname for the reader connection.
- postgres_host_writer: hostname for the writer connection.
- postgres_port: database port.
- postgres_dbname: database name.
- """
-
- postgres_user: str
- postgres_pass: str
- postgres_host_reader: str
- postgres_host_writer: str
- postgres_port: str
- postgres_dbname: str
-
- # Fields which are defined by STAC but not included in the database model
- forbidden_fields: Set[str] = {"type"}
-
- # Fields which are item properties but indexed as distinct fields in the database model
- indexed_fields: Set[str] = {"datetime"}
-
- @property
- def reader_connection_string(self):
- """Create reader psql connection string."""
- return f"postgresql://{self.postgres_user}:{self.postgres_pass}@{self.postgres_host_reader}:{self.postgres_port}/{self.postgres_dbname}"
-
- @property
- def writer_connection_string(self):
- """Create writer psql connection string."""
- return f"postgresql://{self.postgres_user}:{self.postgres_pass}@{self.postgres_host_writer}:{self.postgres_port}/{self.postgres_dbname}"
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/core.py b/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/core.py
deleted file mode 100644
index 62289d2a0..000000000
--- a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/core.py
+++ /dev/null
@@ -1,522 +0,0 @@
-"""Item crud client."""
-import json
-import logging
-import operator
-from datetime import datetime
-from typing import List, Optional, Set, Type, Union
-from urllib.parse import unquote_plus, urlencode, urljoin
-
-import attr
-import geoalchemy2 as ga
-import sqlalchemy as sa
-import stac_pydantic
-from fastapi import HTTPException
-from pydantic import ValidationError
-from shapely.geometry import Polygon as ShapelyPolygon
-from shapely.geometry import shape
-from sqlakeyset import get_page
-from sqlalchemy import func
-from sqlalchemy.orm import Session as SqlSession
-from stac_pydantic.links import Relations
-from stac_pydantic.shared import MimeTypes
-
-from stac_fastapi.sqlalchemy import serializers
-from stac_fastapi.sqlalchemy.extensions.query import Operator
-from stac_fastapi.sqlalchemy.models import database
-from stac_fastapi.sqlalchemy.session import Session
-from stac_fastapi.sqlalchemy.tokens import PaginationTokenClient
-from stac_fastapi.types.config import Settings
-from stac_fastapi.types.core import BaseCoreClient
-from stac_fastapi.types.errors import NotFoundError
-from stac_fastapi.types.search import BaseSearchPostRequest
-from stac_fastapi.types.stac import Collection, Collections, Item, ItemCollection
-
-logger = logging.getLogger(__name__)
-
-NumType = Union[float, int]
-
-
-@attr.s
-class CoreCrudClient(PaginationTokenClient, BaseCoreClient):
- """Client for core endpoints defined by stac."""
-
- session: Session = attr.ib(default=attr.Factory(Session.create_from_env))
- item_table: Type[database.Item] = attr.ib(default=database.Item)
- collection_table: Type[database.Collection] = attr.ib(default=database.Collection)
- item_serializer: Type[serializers.Serializer] = attr.ib(
- default=serializers.ItemSerializer
- )
- collection_serializer: Type[serializers.Serializer] = attr.ib(
- default=serializers.CollectionSerializer
- )
-
- @staticmethod
- def _lookup_id(
- id: str, table: Type[database.BaseModel], session: SqlSession
- ) -> Type[database.BaseModel]:
- """Lookup row by id."""
- row = session.query(table).filter(table.id == id).first()
- if not row:
- raise NotFoundError(f"{table.__name__} {id} not found")
- return row
-
- def all_collections(self, **kwargs) -> Collections:
- """Read all collections from the database."""
- base_url = str(kwargs["request"].base_url)
- with self.session.reader.context_session() as session:
- collections = session.query(self.collection_table).all()
- serialized_collections = [
- self.collection_serializer.db_to_stac(collection, base_url=base_url)
- for collection in collections
- ]
- links = [
- {
- "rel": Relations.root.value,
- "type": MimeTypes.json,
- "href": base_url,
- },
- {
- "rel": Relations.parent.value,
- "type": MimeTypes.json,
- "href": base_url,
- },
- {
- "rel": Relations.self.value,
- "type": MimeTypes.json,
- "href": urljoin(base_url, "collections"),
- },
- ]
- collection_list = Collections(
- collections=serialized_collections or [], links=links
- )
- return collection_list
-
- def get_collection(self, collection_id: str, **kwargs) -> Collection:
- """Get collection by id."""
- base_url = str(kwargs["request"].base_url)
- with self.session.reader.context_session() as session:
- collection = self._lookup_id(collection_id, self.collection_table, session)
- return self.collection_serializer.db_to_stac(collection, base_url)
-
- def item_collection(
- self,
- collection_id: str,
- bbox: Optional[List[NumType]] = None,
- datetime: Optional[str] = None,
- limit: int = 10,
- token: str = None,
- **kwargs,
- ) -> ItemCollection:
- """Read an item collection from the database."""
- base_url = str(kwargs["request"].base_url)
- with self.session.reader.context_session() as session:
- # Look up the collection first to get a 404 if it doesn't exist
- _ = self._lookup_id(collection_id, self.collection_table, session)
- query = (
- session.query(self.item_table)
- .join(self.collection_table)
- .filter(self.collection_table.id == collection_id)
- .order_by(self.item_table.datetime.desc(), self.item_table.id)
- )
- # Spatial query
- geom = None
- if bbox:
- bbox = [float(x) for x in bbox]
- if len(bbox) == 4:
- geom = ShapelyPolygon.from_bounds(*bbox)
- elif len(bbox) == 6:
- """Shapely doesn't support 3d bounding boxes so use the 2d portion"""
- bbox_2d = [bbox[0], bbox[1], bbox[3], bbox[4]]
- geom = ShapelyPolygon.from_bounds(*bbox_2d)
- if geom:
- filter_geom = ga.shape.from_shape(geom, srid=4326)
- query = query.filter(
- ga.func.ST_Intersects(self.item_table.geometry, filter_geom)
- )
-
- # Temporal query
- if datetime:
- # Two tailed query (between)
- dts = datetime.split("/")
- # Non-interval date ex. "2000-02-02T00:00:00.00Z"
- if len(dts) == 1:
- query = query.filter(self.item_table.datetime == dts[0])
- # is there a benefit to between instead of >= and <= ?
- elif dts[0] not in ["", ".."] and dts[1] not in ["", ".."]:
- query = query.filter(self.item_table.datetime.between(*dts))
- # All items after the start date
- elif dts[0] not in ["", ".."]:
- query = query.filter(self.item_table.datetime >= dts[0])
- # All items before the end date
- elif dts[1] not in ["", ".."]:
- query = query.filter(self.item_table.datetime <= dts[1])
-
- count = None
- if self.extension_is_enabled("ContextExtension"):
- count_query = query.statement.with_only_columns(
- [func.count()]
- ).order_by(None)
- count = query.session.execute(count_query).scalar()
- token = self.get_token(token) if token else token
- page = get_page(query, per_page=limit, page=(token or False))
- # Create dynamic attributes for each page
- page.next = (
- self.insert_token(keyset=page.paging.bookmark_next)
- if page.paging.has_next
- else None
- )
- page.previous = (
- self.insert_token(keyset=page.paging.bookmark_previous)
- if page.paging.has_previous
- else None
- )
-
- links = [
- {
- "rel": Relations.self.value,
- "type": "application/geo+json",
- "href": str(kwargs["request"].url),
- },
- {
- "rel": Relations.root.value,
- "type": "application/json",
- "href": str(kwargs["request"].base_url),
- },
- {
- "rel": Relations.parent.value,
- "type": "application/json",
- "href": str(kwargs["request"].base_url),
- },
- ]
- if page.next:
- links.append(
- {
- "rel": Relations.next.value,
- "type": "application/geo+json",
- "href": f"{kwargs['request'].base_url}collections/{collection_id}/items?token={page.next}&limit={limit}",
- "method": "GET",
- }
- )
- if page.previous:
- links.append(
- {
- "rel": Relations.previous.value,
- "type": "application/geo+json",
- "href": f"{kwargs['request'].base_url}collections/{collection_id}/items?token={page.previous}&limit={limit}",
- "method": "GET",
- }
- )
-
- response_features = []
- for item in page:
- response_features.append(
- self.item_serializer.db_to_stac(item, base_url=base_url)
- )
-
- context_obj = None
- if self.extension_is_enabled("ContextExtension"):
- context_obj = {
- "returned": len(page),
- "limit": limit,
- "matched": count,
- }
-
- return ItemCollection(
- type="FeatureCollection",
- features=response_features,
- links=links,
- context=context_obj,
- )
-
- def get_item(self, item_id: str, collection_id: str, **kwargs) -> Item:
- """Get item by id."""
- base_url = str(kwargs["request"].base_url)
- with self.session.reader.context_session() as session:
- db_query = session.query(self.item_table)
- db_query = db_query.filter(self.item_table.collection_id == collection_id)
- db_query = db_query.filter(self.item_table.id == item_id)
- item = db_query.first()
- if not item:
- raise NotFoundError(f"{self.item_table.__name__} {item_id} not found")
- return self.item_serializer.db_to_stac(item, base_url=base_url)
-
- def get_search(
- self,
- collections: Optional[List[str]] = None,
- ids: Optional[List[str]] = None,
- bbox: Optional[List[NumType]] = None,
- datetime: Optional[Union[str, datetime]] = None,
- limit: Optional[int] = 10,
- query: Optional[str] = None,
- token: Optional[str] = None,
- fields: Optional[List[str]] = None,
- sortby: Optional[str] = None,
- intersects: Optional[str] = None,
- **kwargs,
- ) -> ItemCollection:
- """GET search catalog."""
- # Parse request parameters
- base_args = {
- "collections": collections,
- "ids": ids,
- "bbox": bbox,
- "limit": limit,
- "token": token,
- "query": json.loads(unquote_plus(query)) if query else query,
- }
-
- if datetime:
- base_args["datetime"] = datetime
-
- if intersects:
- base_args["intersects"] = json.loads(unquote_plus(intersects))
-
- if sortby:
- # https://github.com/radiantearth/stac-spec/tree/master/api-spec/extensions/sort#http-get-or-post-form
- sort_param = []
- for sort in sortby:
- sort_param.append(
- {
- "field": sort[1:],
- "direction": "asc" if sort[0] == "+" else "desc",
- }
- )
- base_args["sortby"] = sort_param
-
- if fields:
- includes = set()
- excludes = set()
- for field in fields:
- if field[0] == "-":
- excludes.add(field[1:])
- elif field[0] == "+":
- includes.add(field[1:])
- else:
- includes.add(field)
- base_args["fields"] = {"include": includes, "exclude": excludes}
-
- # Do the request
- try:
- search_request = self.post_request_model(**base_args)
- except ValidationError:
- raise HTTPException(status_code=400, detail="Invalid parameters provided")
- resp = self.post_search(search_request, request=kwargs["request"])
-
- # Pagination
- page_links = []
- for link in resp["links"]:
- if link["rel"] == Relations.next or link["rel"] == Relations.previous:
- query_params = dict(kwargs["request"].query_params)
- if link["body"] and link["merge"]:
- query_params.update(link["body"])
- link["method"] = "GET"
- link["href"] = f"{link['body']}?{urlencode(query_params)}"
- link["body"] = None
- link["merge"] = False
- page_links.append(link)
- else:
- page_links.append(link)
- resp["links"] = page_links
- return resp
-
- def post_search(
- self, search_request: BaseSearchPostRequest, **kwargs
- ) -> ItemCollection:
- """POST search catalog."""
- base_url = str(kwargs["request"].base_url)
- with self.session.reader.context_session() as session:
- token = (
- self.get_token(search_request.token) if search_request.token else False
- )
- query = session.query(self.item_table)
-
- # Filter by collection
- count = None
- if search_request.collections:
- query = query.join(self.collection_table).filter(
- sa.or_(
- *[
- self.collection_table.id == col_id
- for col_id in search_request.collections
- ]
- )
- )
-
- # Sort
- if search_request.sortby:
- sort_fields = [
- getattr(
- self.item_table.get_field(sort.field),
- sort.direction.value,
- )()
- for sort in search_request.sortby
- ]
- sort_fields.append(self.item_table.id)
- query = query.order_by(*sort_fields)
- else:
- # Default sort is date
- query = query.order_by(
- self.item_table.datetime.desc(), self.item_table.id
- )
-
- # Ignore other parameters if ID is present
- if search_request.ids:
- id_filter = sa.or_(
- *[self.item_table.id == i for i in search_request.ids]
- )
- items = query.filter(id_filter).order_by(self.item_table.id)
- page = get_page(items, per_page=search_request.limit, page=token)
- if self.extension_is_enabled("ContextExtension"):
- count = len(search_request.ids)
- page.next = (
- self.insert_token(keyset=page.paging.bookmark_next)
- if page.paging.has_next
- else None
- )
- page.previous = (
- self.insert_token(keyset=page.paging.bookmark_previous)
- if page.paging.has_previous
- else None
- )
-
- else:
- # Spatial query
- geom = None
- if search_request.intersects is not None:
- geom = shape(search_request.intersects)
- elif search_request.bbox:
- if len(search_request.bbox) == 4:
- geom = ShapelyPolygon.from_bounds(*search_request.bbox)
- elif len(search_request.bbox) == 6:
- """Shapely doesn't support 3d bounding boxes we'll just use the 2d portion"""
- bbox_2d = [
- search_request.bbox[0],
- search_request.bbox[1],
- search_request.bbox[3],
- search_request.bbox[4],
- ]
- geom = ShapelyPolygon.from_bounds(*bbox_2d)
-
- if geom:
- filter_geom = ga.shape.from_shape(geom, srid=4326)
- query = query.filter(
- ga.func.ST_Intersects(self.item_table.geometry, filter_geom)
- )
-
- # Temporal query
- if search_request.datetime:
- # Two tailed query (between)
- dts = search_request.datetime.split("/")
- # Non-interval date ex. "2000-02-02T00:00:00.00Z"
- if len(dts) == 1:
- query = query.filter(self.item_table.datetime == dts[0])
- # is there a benefit to between instead of >= and <= ?
- elif dts[0] not in ["", ".."] and dts[1] not in ["", ".."]:
- query = query.filter(self.item_table.datetime.between(*dts))
- # All items after the start date
- elif dts[0] not in ["", ".."]:
- query = query.filter(self.item_table.datetime >= dts[0])
- # All items before the end date
- elif dts[1] not in ["", ".."]:
- query = query.filter(self.item_table.datetime <= dts[1])
-
- # Query fields
- if search_request.query:
- for (field_name, expr) in search_request.query.items():
- field = self.item_table.get_field(field_name)
- for (op, value) in expr.items():
- if op == Operator.gte:
- query = query.filter(operator.ge(field, value))
- elif op == Operator.lte:
- query = query.filter(operator.le(field, value))
- else:
- query = query.filter(op.operator(field, value))
-
- if self.extension_is_enabled("ContextExtension"):
- count_query = query.statement.with_only_columns(
- [func.count()]
- ).order_by(None)
- count = query.session.execute(count_query).scalar()
- page = get_page(query, per_page=search_request.limit, page=token)
- # Create dynamic attributes for each page
- page.next = (
- self.insert_token(keyset=page.paging.bookmark_next)
- if page.paging.has_next
- else None
- )
- page.previous = (
- self.insert_token(keyset=page.paging.bookmark_previous)
- if page.paging.has_previous
- else None
- )
-
- links = []
- if page.next:
- links.append(
- {
- "rel": Relations.next.value,
- "type": "application/geo+json",
- "href": f"{kwargs['request'].base_url}search",
- "method": "POST",
- "body": {"token": page.next},
- "merge": True,
- }
- )
- if page.previous:
- links.append(
- {
- "rel": Relations.previous.value,
- "type": "application/geo+json",
- "href": f"{kwargs['request'].base_url}search",
- "method": "POST",
- "body": {"token": page.previous},
- "merge": True,
- }
- )
-
- response_features = []
- filter_kwargs = {}
-
- for item in page:
- response_features.append(
- self.item_serializer.db_to_stac(item, base_url=base_url)
- )
-
- # Use pydantic includes/excludes syntax to implement fields extension
- if self.extension_is_enabled("FieldsExtension"):
- if search_request.query is not None:
- query_include: Set[str] = set(
- [
- k
- if k in Settings.get().indexed_fields
- else f"properties.{k}"
- for k in search_request.query.keys()
- ]
- )
- if not search_request.fields.include:
- search_request.fields.include = query_include
- else:
- search_request.fields.include.union(query_include)
-
- filter_kwargs = search_request.fields.filter_fields
- # Need to pass through `.json()` for proper serialization
- # of datetime
- response_features = [
- json.loads(stac_pydantic.Item(**feat).json(**filter_kwargs))
- for feat in response_features
- ]
-
- context_obj = None
- if self.extension_is_enabled("ContextExtension"):
- context_obj = {
- "returned": len(page),
- "limit": search_request.limit,
- "matched": count,
- }
-
- return ItemCollection(
- type="FeatureCollection",
- features=response_features,
- links=links,
- context=context_obj,
- )
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/extensions/__init__.py b/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/extensions/__init__.py
deleted file mode 100644
index d97a001cd..000000000
--- a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/extensions/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""sqlalchemy extensions modifications."""
-
-from .query import Operator, QueryableTypes, QueryExtension
-
-__all__ = ["Operator", "QueryableTypes", "QueryExtension"]
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/extensions/query.py b/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/extensions/query.py
deleted file mode 100644
index 17fc85ab9..000000000
--- a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/extensions/query.py
+++ /dev/null
@@ -1,133 +0,0 @@
-"""STAC SQLAlchemy specific query search model.
-
-# TODO: replace with stac-pydantic
-"""
-
-import logging
-import operator
-from dataclasses import dataclass
-from enum import auto
-from types import DynamicClassAttribute
-from typing import Any, Callable, Dict, Optional, Union
-
-import sqlalchemy as sa
-from pydantic import BaseModel, ValidationError, root_validator
-from pydantic.error_wrappers import ErrorWrapper
-from stac_pydantic.utils import AutoValueEnum
-
-from stac_fastapi.extensions.core.query import QueryExtension as QueryExtensionBase
-
-logger = logging.getLogger("uvicorn")
-logger.setLevel(logging.INFO)
-# Be careful: https://github.com/samuelcolvin/pydantic/issues/1423#issuecomment-642797287
-NumType = Union[float, int]
-
-
-class Operator(str, AutoValueEnum):
- """Defines the set of operators supported by the API."""
-
- eq = auto()
- ne = auto()
- lt = auto()
- lte = auto()
- gt = auto()
- gte = auto()
-
- # TODO: These are defined in the spec but aren't currently implemented by the api
- # startsWith = auto()
- # endsWith = auto()
- # contains = auto()
- # in = auto()
-
- @DynamicClassAttribute
- def operator(self) -> Callable[[Any, Any], bool]:
- """Return python operator."""
- return getattr(operator, self._value_)
-
-
-class Queryables(str, AutoValueEnum):
- """Queryable fields.
-
- Define an enum of queryable fields and their data type. Queryable fields are explicitly defined for two reasons:
- 1. So the caller knows which fields they can query by
- 2. Because JSONB queries with sqlalchemy ORM require casting the type of the field at runtime
- (see ``QueryableTypes``)
-
- # TODO: Let the user define these in a config file
- """
-
- orientation = auto()
- gsd = auto()
- epsg = "proj:epsg"
- height = auto()
- width = auto()
- minzoom = "cog:minzoom"
- maxzoom = "cog:maxzoom"
- dtype = "cog:dtype"
- foo = "foo"
-
- def __str__(self) -> str:
- """Return the Queryable's value as its __str__.
-
- Python 3.11 changed the default __str__ behavior for Enums, and since we
- can't use StrEnum (it was introduced in 3.11), we need to define our
- expected behavior explicitly.
- """
- return self.value
-
-
-@dataclass
-class QueryableTypes:
- """Defines a set of queryable fields.
-
- # TODO: Let the user define these in a config file
- # TODO: There is a much better way of defining this field <> type mapping than two enums with same keys
- """
-
- orientation = sa.String
- gsd = sa.Float
- epsg = sa.Integer
- height = sa.Integer
- width = sa.Integer
- minzoom = sa.Integer
- maxzoom = sa.Integer
- dtype = sa.String
-
-
-class QueryExtensionPostRequest(BaseModel):
- """Queryable validation.
-
- Add queryables validation to the POST request
- to raise errors for unsupported querys.
- """
-
- query: Optional[Dict[Queryables, Dict[Operator, Any]]]
-
- @root_validator(pre=True)
- def validate_query_fields(cls, values: Dict) -> Dict:
- """Validate query fields."""
- logger.debug(f"Validating SQLAlchemySTACSearch {cls} {values}")
- if "query" in values and values["query"]:
- queryable_fields = Queryables.__members__.values()
- for field_name in values["query"]:
- if field_name not in queryable_fields:
- raise ValidationError(
- [
- ErrorWrapper(
- ValueError(f"Cannot search on field: {field_name}"),
- "STACSearch",
- )
- ],
- QueryExtensionPostRequest,
- )
- return values
-
-
-class QueryExtension(QueryExtensionBase):
- """Query Extenson.
-
- Override the POST request model to add validation against
- supported fields
- """
-
- POST = QueryExtensionPostRequest
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/__init__.py b/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/__init__.py
deleted file mode 100644
index 67d205ef3..000000000
--- a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""stac_fastapi.postgres.models module."""
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/database.py b/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/database.py
deleted file mode 100644
index ed9d8cef0..000000000
--- a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/database.py
+++ /dev/null
@@ -1,99 +0,0 @@
-"""SQLAlchemy ORM models."""
-
-import json
-from typing import Optional
-
-import geoalchemy2 as ga
-import sqlalchemy as sa
-from sqlalchemy.dialects.postgresql import JSONB
-from sqlalchemy.ext.declarative import declarative_base
-
-from stac_fastapi.sqlalchemy.extensions.query import Queryables, QueryableTypes
-
-BaseModel = declarative_base()
-
-
-class GeojsonGeometry(ga.Geometry):
- """Custom geoalchemy type which returns GeoJSON."""
-
- from_text = "ST_GeomFromGeoJSON"
-
- def result_processor(self, dialect: str, coltype):
- """Override default processer to return GeoJSON."""
-
- def process(value: Optional[bytes]):
- if value is not None:
- geom = ga.shape.to_shape(
- ga.elements.WKBElement(
- value, srid=self.srid, extended=self.extended
- )
- )
- return json.loads(json.dumps(geom.__geo_interface__))
-
- return process
-
-
-class Collection(BaseModel): # type:ignore
- """Collection orm model."""
-
- __tablename__ = "collections"
- __table_args__ = {"schema": "data"}
-
- id = sa.Column(sa.VARCHAR(1024), nullable=False, primary_key=True)
- stac_version = sa.Column(sa.VARCHAR(300))
- stac_extensions = sa.Column(sa.ARRAY(sa.VARCHAR(300)), nullable=True)
- title = sa.Column(sa.VARCHAR(1024))
- description = sa.Column(sa.VARCHAR(1024), nullable=False)
- keywords = sa.Column(sa.ARRAY(sa.VARCHAR(300)))
- version = sa.Column(sa.VARCHAR(300))
- license = sa.Column(sa.VARCHAR(300), nullable=False)
- providers = sa.Column(JSONB)
- summaries = sa.Column(JSONB, nullable=True)
- extent = sa.Column(JSONB)
- links = sa.Column(JSONB)
- children = sa.orm.relationship("Item", lazy="dynamic")
- type = sa.Column(sa.VARCHAR(300), nullable=False)
-
-
-class Item(BaseModel): # type:ignore
- """Item orm model."""
-
- __tablename__ = "items"
- __table_args__ = {"schema": "data"}
-
- id = sa.Column(sa.VARCHAR(1024), nullable=False, primary_key=True)
- stac_version = sa.Column(sa.VARCHAR(300))
- stac_extensions = sa.Column(sa.ARRAY(sa.VARCHAR(300)), nullable=True)
- geometry = sa.Column(
- GeojsonGeometry("GEOMETRY", srid=4326, spatial_index=True), nullable=True
- )
- bbox = sa.Column(sa.ARRAY(sa.NUMERIC), nullable=True)
- properties = sa.Column(JSONB)
- assets = sa.Column(JSONB)
- collection_id = sa.Column(
- sa.VARCHAR(1024), sa.ForeignKey(Collection.id), nullable=False, primary_key=True
- )
- parent_collection = sa.orm.relationship("Collection", back_populates="children")
- datetime = sa.Column(sa.TIMESTAMP(timezone=True), nullable=False)
- links = sa.Column(JSONB)
-
- @classmethod
- def get_field(cls, field_name):
- """Get a model field."""
- try:
- return getattr(cls, field_name)
- except AttributeError:
- # Use a JSONB field
- return cls.properties[(field_name)].cast(
- getattr(QueryableTypes, Queryables(field_name).name)
- )
-
-
-class PaginationToken(BaseModel): # type:ignore
- """Pagination orm model."""
-
- __tablename__ = "tokens"
- __table_args__ = {"schema": "data"}
-
- id = sa.Column(sa.VARCHAR(100), nullable=False, primary_key=True)
- keyset = sa.Column(sa.VARCHAR(1000), nullable=False)
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/search.py b/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/search.py
deleted file mode 100644
index f87f7a8b8..000000000
--- a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/search.py
+++ /dev/null
@@ -1,23 +0,0 @@
-"""Queryable data types for sqlalchemy backend."""
-
-from dataclasses import dataclass
-
-import sqlalchemy as sa
-
-
-@dataclass
-class QueryableTypes:
- """Defines a set of queryable fields.
-
- # TODO: Let the user define these in a config file
- # TODO: There is a much better way of defining this field <> type mapping than two enums with same keys
- """
-
- orientation = sa.String
- gsd = sa.Float
- epsg = sa.Integer
- height = sa.Integer
- width = sa.Integer
- minzoom = sa.Integer
- maxzoom = sa.Integer
- dtype = sa.String
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/serializers.py b/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/serializers.py
deleted file mode 100644
index 5a272634e..000000000
--- a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/serializers.py
+++ /dev/null
@@ -1,177 +0,0 @@
-"""Serializers."""
-import abc
-import json
-from typing import TypedDict
-
-import attr
-import geoalchemy2 as ga
-from pystac.utils import datetime_to_str
-
-from stac_fastapi.sqlalchemy.models import database
-from stac_fastapi.types import stac as stac_types
-from stac_fastapi.types.config import Settings
-from stac_fastapi.types.links import CollectionLinks, ItemLinks, resolve_links
-from stac_fastapi.types.rfc3339 import now_to_rfc3339_str, rfc3339_str_to_datetime
-
-
-@attr.s # type:ignore
-class Serializer(abc.ABC):
- """Defines serialization methods between the API and the data model."""
-
- @classmethod
- @abc.abstractmethod
- def db_to_stac(cls, db_model: database.BaseModel, base_url: str) -> TypedDict:
- """Transform database model to stac."""
- ...
-
- @classmethod
- @abc.abstractmethod
- def stac_to_db(
- cls, stac_data: TypedDict, exclude_geometry: bool = False
- ) -> database.BaseModel:
- """Transform stac to database model."""
- ...
-
- @classmethod
- def row_to_dict(cls, db_model: database.BaseModel):
- """Transform a database model to it's dictionary representation."""
- d = {}
- for column in db_model.__table__.columns:
- value = getattr(db_model, column.name)
- if value:
- d[column.name] = value
- return d
-
-
-class ItemSerializer(Serializer):
- """Serialization methods for STAC items."""
-
- @classmethod
- def db_to_stac(cls, db_model: database.Item, base_url: str) -> stac_types.Item:
- """Transform database model to stac item."""
- properties = db_model.properties.copy()
- indexed_fields = Settings.get().indexed_fields
- for field in indexed_fields:
- # Use getattr to accommodate extension namespaces
- field_value = getattr(db_model, field.split(":")[-1])
- if field == "datetime":
- field_value = datetime_to_str(field_value)
- properties[field] = field_value
- item_id = db_model.id
- collection_id = db_model.collection_id
- item_links = ItemLinks(
- collection_id=collection_id, item_id=item_id, base_url=base_url
- ).create_links()
-
- db_links = db_model.links
- if db_links:
- item_links += resolve_links(db_links, base_url)
-
- stac_extensions = db_model.stac_extensions or []
-
- # The custom geometry we are using emits geojson if the geometry is bound to the database
- # Otherwise it will return a geoalchemy2 WKBElement
- # TODO: It's probably best to just remove the custom geometry type
- geometry = db_model.geometry
- if isinstance(geometry, ga.elements.WKBElement):
- geometry = ga.shape.to_shape(geometry).__geo_interface__
- if isinstance(geometry, str):
- geometry = json.loads(geometry)
-
- bbox = db_model.bbox
- if bbox is not None:
- bbox = [float(x) for x in db_model.bbox]
-
- return stac_types.Item(
- type="Feature",
- stac_version=db_model.stac_version,
- stac_extensions=stac_extensions,
- id=db_model.id,
- collection=db_model.collection_id,
- geometry=geometry,
- bbox=bbox,
- properties=properties,
- links=item_links,
- assets=db_model.assets,
- )
-
- @classmethod
- def stac_to_db(
- cls, stac_data: TypedDict, exclude_geometry: bool = False
- ) -> database.Item:
- """Transform stac item to database model."""
- indexed_fields = {}
- for field in Settings.get().indexed_fields:
- # Use getattr to accommodate extension namespaces
- field_value = stac_data["properties"][field]
- if field == "datetime":
- field_value = rfc3339_str_to_datetime(field_value)
- indexed_fields[field.split(":")[-1]] = field_value
-
- # TODO: Exclude indexed fields from the properties jsonb field to prevent duplication
-
- now = now_to_rfc3339_str()
- if "created" not in stac_data["properties"]:
- stac_data["properties"]["created"] = now
- stac_data["properties"]["updated"] = now
-
- geometry = stac_data["geometry"]
- if geometry is not None:
- geometry = json.dumps(geometry)
-
- return database.Item(
- id=stac_data["id"],
- collection_id=stac_data["collection"],
- stac_version=stac_data["stac_version"],
- stac_extensions=stac_data.get("stac_extensions"),
- geometry=geometry,
- bbox=stac_data.get("bbox"),
- properties=stac_data["properties"],
- assets=stac_data["assets"],
- **indexed_fields,
- )
-
-
-class CollectionSerializer(Serializer):
- """Serialization methods for STAC collections."""
-
- @classmethod
- def db_to_stac(cls, db_model: database.Collection, base_url: str) -> TypedDict:
- """Transform database model to stac collection."""
- collection_links = CollectionLinks(
- collection_id=db_model.id, base_url=base_url
- ).create_links()
-
- db_links = db_model.links
- if db_links:
- collection_links += resolve_links(db_links, base_url)
-
- collection = stac_types.Collection(
- type="Collection",
- id=db_model.id,
- stac_version=db_model.stac_version,
- description=db_model.description,
- license=db_model.license,
- extent=db_model.extent,
- links=collection_links,
- )
- # We need to manually include optional values to ensure they are
- # excluded if we're not using response models.
- if db_model.stac_extensions:
- collection["stac_extensions"] = db_model.stac_extensions
- if db_model.title:
- collection["title"] = db_model.title
- if db_model.keywords:
- collection["keywords"] = db_model.keywords
- if db_model.providers:
- collection["providers"] = db_model.providers
- if db_model.summaries:
- collection["summaries"] = db_model.summaries
- return collection
-
- @classmethod
- def stac_to_db(
- cls, stac_data: TypedDict, exclude_geometry: bool = False
- ) -> database.Collection:
- """Transform stac collection to database model."""
- return database.Collection(**dict(stac_data))
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/session.py b/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/session.py
deleted file mode 100644
index 79119c4a1..000000000
--- a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/session.py
+++ /dev/null
@@ -1,62 +0,0 @@
-"""database session management."""
-import logging
-import os
-from contextlib import contextmanager
-from typing import Iterator
-
-import attr
-import psycopg2
-import sqlalchemy as sa
-from fastapi_utils.session import FastAPISessionMaker as _FastAPISessionMaker
-from sqlalchemy.orm import Session as SqlSession
-
-from stac_fastapi.sqlalchemy.config import SqlalchemySettings
-from stac_fastapi.types import errors
-
-logger = logging.getLogger(__name__)
-
-
-class FastAPISessionMaker(_FastAPISessionMaker):
- """FastAPISessionMaker."""
-
- @contextmanager
- def context_session(self) -> Iterator[SqlSession]:
- """Override base method to include exception handling."""
- try:
- yield from self.get_db()
- except sa.exc.StatementError as e:
- if isinstance(e.orig, psycopg2.errors.UniqueViolation):
- raise errors.ConflictError("resource already exists") from e
- elif isinstance(e.orig, psycopg2.errors.ForeignKeyViolation):
- raise errors.ForeignKeyError("collection does not exist") from e
- logger.error(e, exc_info=True)
- raise errors.DatabaseError("unhandled database error")
-
-
-@attr.s
-class Session:
- """Database session management."""
-
- reader_conn_string: str = attr.ib()
- writer_conn_string: str = attr.ib()
-
- @classmethod
- def create_from_env(cls):
- """Create from environment."""
- return cls(
- reader_conn_string=os.environ["READER_CONN_STRING"],
- writer_conn_string=os.environ["WRITER_CONN_STRING"],
- )
-
- @classmethod
- def create_from_settings(cls, settings: SqlalchemySettings) -> "Session":
- """Create a Session object from settings."""
- return cls(
- reader_conn_string=settings.reader_connection_string,
- writer_conn_string=settings.writer_connection_string,
- )
-
- def __attrs_post_init__(self):
- """Post init handler."""
- self.reader: FastAPISessionMaker = FastAPISessionMaker(self.reader_conn_string)
- self.writer: FastAPISessionMaker = FastAPISessionMaker(self.writer_conn_string)
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/tokens.py b/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/tokens.py
deleted file mode 100644
index 19920ab9d..000000000
--- a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/tokens.py
+++ /dev/null
@@ -1,55 +0,0 @@
-"""Pagination token client."""
-import abc
-import logging
-import os
-from base64 import urlsafe_b64encode
-from typing import Type
-
-import attr
-from sqlalchemy.orm import Session as SqlSession
-
-from stac_fastapi.sqlalchemy.models import database
-from stac_fastapi.sqlalchemy.session import Session
-from stac_fastapi.types.errors import DatabaseError
-
-logger = logging.getLogger(__name__)
-
-
-@attr.s
-class PaginationTokenClient(abc.ABC):
- """Pagination token specific CRUD operations."""
-
- session: Session = attr.ib(default=attr.Factory(Session.create_from_env))
- token_table: Type[database.PaginationToken] = attr.ib(
- default=database.PaginationToken
- )
-
- @staticmethod
- @abc.abstractmethod
- def _lookup_id(
- id: str, table: Type[database.BaseModel], session: SqlSession
- ) -> Type[database.BaseModel]:
- """Lookup row by id."""
- ...
-
- def insert_token(self, keyset: str, tries: int = 0) -> str: # type:ignore
- """Insert a keyset into the database."""
- # uid has collision chance of 1e-7 percent
- uid = urlsafe_b64encode(os.urandom(6)).decode()
- with self.session.writer.context_session() as session:
- try:
- token = database.PaginationToken(id=uid, keyset=keyset)
- session.add(token)
- return uid
- except DatabaseError:
- # Try again if uid already exists in the database
- # TODO: Explicitely check for ConflictError (if insert fails for other reasons it should be raised)
- if tries > 5:
- raise
- self.insert_token(keyset, tries=tries + 1)
-
- def get_token(self, token_id: str) -> str:
- """Retrieve a keyset from the database."""
- with self.session.reader.context_session() as session:
- token = self._lookup_id(token_id, self.token_table, session)
- return token.keyset
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/transactions.py b/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/transactions.py
deleted file mode 100644
index 644b82f2d..000000000
--- a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/transactions.py
+++ /dev/null
@@ -1,201 +0,0 @@
-"""transactions extension client."""
-
-import logging
-from typing import Optional, Type, Union
-
-import attr
-from fastapi import HTTPException
-from starlette.responses import Response
-
-from stac_fastapi.extensions.third_party.bulk_transactions import (
- BaseBulkTransactionsClient,
- Items,
-)
-from stac_fastapi.sqlalchemy import serializers
-from stac_fastapi.sqlalchemy.models import database
-from stac_fastapi.sqlalchemy.session import Session
-from stac_fastapi.types import stac as stac_types
-from stac_fastapi.types.core import BaseTransactionsClient
-from stac_fastapi.types.errors import NotFoundError
-
-logger = logging.getLogger(__name__)
-
-
-@attr.s
-class TransactionsClient(BaseTransactionsClient):
- """Transactions extension specific CRUD operations."""
-
- session: Session = attr.ib(default=attr.Factory(Session.create_from_env))
- collection_table: Type[database.Collection] = attr.ib(default=database.Collection)
- item_table: Type[database.Item] = attr.ib(default=database.Item)
- item_serializer: Type[serializers.Serializer] = attr.ib(
- default=serializers.ItemSerializer
- )
- collection_serializer: Type[serializers.Serializer] = attr.ib(
- default=serializers.CollectionSerializer
- )
-
- def create_item(
- self,
- collection_id: str,
- item: Union[stac_types.Item, stac_types.ItemCollection],
- **kwargs,
- ) -> Optional[stac_types.Item]:
- """Create item."""
- base_url = str(kwargs["request"].base_url)
-
- # If a feature collection is posted
- if item["type"] == "FeatureCollection":
- bulk_client = BulkTransactionsClient(session=self.session)
- bulk_client.bulk_item_insert(items=item["features"])
- return None
-
- # Otherwise a single item has been posted
- body_collection_id = item.get("collection")
- if body_collection_id is not None and collection_id != body_collection_id:
- raise HTTPException(
- status_code=400,
- detail=f"Collection ID from path parameter ({collection_id}) does not match Collection ID from Item ({body_collection_id})",
- )
- item["collection"] = collection_id
- data = self.item_serializer.stac_to_db(item)
- with self.session.writer.context_session() as session:
- session.add(data)
- return self.item_serializer.db_to_stac(data, base_url)
-
- def create_collection(
- self, collection: stac_types.Collection, **kwargs
- ) -> Optional[Union[stac_types.Collection, Response]]:
- """Create collection."""
- base_url = str(kwargs["request"].base_url)
- data = self.collection_serializer.stac_to_db(collection)
- with self.session.writer.context_session() as session:
- session.add(data)
- return self.collection_serializer.db_to_stac(data, base_url=base_url)
-
- def update_item(
- self, collection_id: str, item_id: str, item: stac_types.Item, **kwargs
- ) -> Optional[Union[stac_types.Item, Response]]:
- """Update item."""
- body_collection_id = item.get("collection")
- if body_collection_id is not None and collection_id != body_collection_id:
- raise HTTPException(
- status_code=400,
- detail=f"Collection ID from path parameter ({collection_id}) does not match Collection ID from Item ({body_collection_id})",
- )
- item["collection"] = collection_id
- body_item_id = item["id"]
- if body_item_id != item_id:
- raise HTTPException(
- status_code=400,
- detail=f"Item ID from path parameter ({item_id}) does not match Item ID from Item ({body_item_id})",
- )
- base_url = str(kwargs["request"].base_url)
- with self.session.reader.context_session() as session:
- query = session.query(self.item_table).filter(
- self.item_table.id == item["id"]
- )
- query = query.filter(self.item_table.collection_id == item["collection"])
- if not query.scalar():
- raise NotFoundError(
- f"Item {item['id']} in collection {item['collection']}"
- )
- # SQLAlchemy orm updates don't seem to like geoalchemy types
- db_model = self.item_serializer.stac_to_db(item)
- query.update(self.item_serializer.row_to_dict(db_model))
- stac_item = self.item_serializer.db_to_stac(db_model, base_url)
-
- return stac_item
-
- def update_collection(
- self, collection: stac_types.Collection, **kwargs
- ) -> Optional[Union[stac_types.Collection, Response]]:
- """Update collection."""
- base_url = str(kwargs["request"].base_url)
- with self.session.reader.context_session() as session:
- query = session.query(self.collection_table).filter(
- self.collection_table.id == collection["id"]
- )
- if not query.scalar():
- raise NotFoundError(f"Item {collection['id']} not found")
-
- # SQLAlchemy orm updates don't seem to like geoalchemy types
- db_model = self.collection_serializer.stac_to_db(collection)
- query.update(self.collection_serializer.row_to_dict(db_model))
-
- return self.collection_serializer.db_to_stac(db_model, base_url)
-
- def delete_item(
- self, item_id: str, collection_id: str, **kwargs
- ) -> Optional[Union[stac_types.Item, Response]]:
- """Delete item."""
- base_url = str(kwargs["request"].base_url)
- with self.session.writer.context_session() as session:
- query = session.query(self.item_table).filter(
- self.item_table.collection_id == collection_id
- )
- query = query.filter(self.item_table.id == item_id)
- data = query.first()
- if not data:
- raise NotFoundError(
- f"Item {item_id} not found in collection {collection_id}"
- )
- query.delete()
- return self.item_serializer.db_to_stac(data, base_url=base_url)
-
- def delete_collection(
- self, collection_id: str, **kwargs
- ) -> Optional[Union[stac_types.Collection, Response]]:
- """Delete collection."""
- base_url = str(kwargs["request"].base_url)
- with self.session.writer.context_session() as session:
- query = session.query(self.collection_table).filter(
- self.collection_table.id == collection_id
- )
- data = query.first()
- if not data:
- raise NotFoundError(f"Collection {collection_id} not found")
- query.delete()
- return self.collection_serializer.db_to_stac(data, base_url=base_url)
-
-
-@attr.s
-class BulkTransactionsClient(BaseBulkTransactionsClient):
- """Postgres bulk transactions."""
-
- session: Session = attr.ib(default=attr.Factory(Session.create_from_env))
- debug: bool = attr.ib(default=False)
- item_table: Type[database.Item] = attr.ib(default=database.Item)
- item_serializer: Type[serializers.Serializer] = attr.ib(
- default=serializers.ItemSerializer
- )
-
- def __attrs_post_init__(self):
- """Create sqlalchemy engine."""
- self.engine = self.session.writer.cached_engine
-
- def _preprocess_item(self, item: stac_types.Item) -> stac_types.Item:
- """Preprocess items to match data model.
-
- # TODO: dedup with GetterDict logic (ref #58)
- """
- db_model = self.item_serializer.stac_to_db(item)
- return self.item_serializer.row_to_dict(db_model)
-
- def bulk_item_insert(
- self, items: Items, chunk_size: Optional[int] = None, **kwargs
- ) -> str:
- """Bulk item insertion using sqlalchemy core.
-
- https://docs.sqlalchemy.org/en/13/faq/performance.html#i-m-inserting-400-000-rows-with-the-orm-and-it-s-really-slow
- """
- # Use items.items because schemas.Items is a model with an items key
- processed_items = [self._preprocess_item(item) for item in items]
- return_msg = f"Successfully added {len(processed_items)} items."
- if chunk_size:
- for chunk in self._chunks(processed_items, chunk_size):
- self.engine.execute(self.item_table.__table__.insert(), chunk)
- return return_msg
-
- self.engine.execute(self.item_table.__table__.insert(), processed_items)
- return return_msg
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/version.py b/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/version.py
deleted file mode 100644
index cdb2b2835..000000000
--- a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/version.py
+++ /dev/null
@@ -1,2 +0,0 @@
-"""library version."""
-__version__ = "2.4.5"
diff --git a/stac_fastapi/sqlalchemy/tests/__init__.py b/stac_fastapi/sqlalchemy/tests/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/stac_fastapi/sqlalchemy/tests/api/__init__.py b/stac_fastapi/sqlalchemy/tests/api/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/stac_fastapi/sqlalchemy/tests/api/test_api.py b/stac_fastapi/sqlalchemy/tests/api/test_api.py
deleted file mode 100644
index 6fdbb6ed8..000000000
--- a/stac_fastapi/sqlalchemy/tests/api/test_api.py
+++ /dev/null
@@ -1,529 +0,0 @@
-from datetime import datetime, timedelta
-from urllib.parse import quote_plus
-
-import orjson
-
-from ..conftest import MockStarletteRequest
-
-STAC_CORE_ROUTES = [
- "GET /",
- "GET /collections",
- "GET /collections/{collection_id}",
- "GET /collections/{collection_id}/items",
- "GET /collections/{collection_id}/items/{item_id}",
- "GET /conformance",
- "GET /search",
- "POST /search",
-]
-
-STAC_TRANSACTION_ROUTES = [
- "DELETE /collections/{collection_id}",
- "DELETE /collections/{collection_id}/items/{item_id}",
- "POST /collections",
- "POST /collections/{collection_id}/items",
- "PUT /collections",
- "PUT /collections/{collection_id}/items/{item_id}",
-]
-
-
-def test_post_search_content_type(app_client):
- params = {"limit": 1}
- resp = app_client.post("search", json=params)
- assert resp.headers["content-type"] == "application/geo+json"
-
-
-def test_get_search_content_type(app_client):
- resp = app_client.get("search")
- assert resp.headers["content-type"] == "application/geo+json"
-
-
-def test_api_headers(app_client):
- resp = app_client.get("/api")
- assert (
- resp.headers["content-type"] == "application/vnd.oai.openapi+json;version=3.0"
- )
- assert resp.status_code == 200
-
-
-def test_core_router(api_client):
- core_routes = set(STAC_CORE_ROUTES)
- api_routes = set(
- [f"{list(route.methods)[0]} {route.path}" for route in api_client.app.routes]
- )
- assert not core_routes - api_routes
-
-
-def test_landing_page_stac_extensions(app_client):
- resp = app_client.get("/")
- assert resp.status_code == 200
- resp_json = resp.json()
- assert not resp_json["stac_extensions"]
-
-
-def test_transactions_router(api_client):
- transaction_routes = set(STAC_TRANSACTION_ROUTES)
- api_routes = set(
- [f"{list(route.methods)[0]} {route.path}" for route in api_client.app.routes]
- )
- assert not transaction_routes - api_routes
-
-
-def test_app_transaction_extension(app_client, load_test_data):
- item = load_test_data("test_item.json")
- resp = app_client.post(f"/collections/{item['collection']}/items", json=item)
- assert resp.status_code == 200
-
-
-def test_app_search_response(load_test_data, app_client, postgres_transactions):
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- resp = app_client.get("/search", params={"collections": ["test-collection"]})
- assert resp.status_code == 200
- resp_json = resp.json()
-
- assert resp_json.get("type") == "FeatureCollection"
- # stac_version and stac_extensions were removed in v1.0.0-beta.3
- assert resp_json.get("stac_version") is None
- assert resp_json.get("stac_extensions") is None
-
-
-def test_app_search_response_multipolygon(
- load_test_data, app_client, postgres_transactions
-):
- item = load_test_data("test_item_multipolygon.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- resp = app_client.get("/search", params={"collections": ["test-collection"]})
- assert resp.status_code == 200
- resp_json = resp.json()
-
- assert resp_json.get("type") == "FeatureCollection"
- assert resp_json.get("features")[0]["geometry"]["type"] == "MultiPolygon"
-
-
-def test_app_search_response_geometry_null(
- load_test_data, app_client, postgres_transactions
-):
- item = load_test_data("test_item_geometry_null.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- resp = app_client.get("/search", params={"collections": ["test-collection"]})
- assert resp.status_code == 200
- resp_json = resp.json()
-
- assert resp_json.get("type") == "FeatureCollection"
- assert resp_json.get("features")[0]["geometry"] is None
- assert resp_json.get("features")[0]["bbox"] is None
-
-
-def test_app_context_extension(load_test_data, app_client, postgres_transactions):
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- resp = app_client.get("/search", params={"collections": ["test-collection"]})
- assert resp.status_code == 200
- resp_json = resp.json()
- assert "context" in resp_json
- assert resp_json["context"]["returned"] == resp_json["context"]["matched"] == 1
-
-
-def test_app_fields_extension(load_test_data, app_client, postgres_transactions):
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- resp = app_client.get("/search", params={"collections": ["test-collection"]})
- assert resp.status_code == 200
- resp_json = resp.json()
- assert list(resp_json["features"][0]["properties"]) == ["datetime"]
-
-
-def test_app_query_extension_gt(load_test_data, app_client, postgres_transactions):
- test_item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- test_item["collection"], test_item, request=MockStarletteRequest
- )
-
- params = {"query": {"proj:epsg": {"gt": test_item["properties"]["proj:epsg"]}}}
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 0
-
- params["query"] = quote_plus(orjson.dumps(params["query"]))
- resp = app_client.get("/search", params=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 0
-
-
-def test_app_query_extension_gte(load_test_data, app_client, postgres_transactions):
- test_item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- test_item["collection"], test_item, request=MockStarletteRequest
- )
-
- params = {"query": {"proj:epsg": {"gte": test_item["properties"]["proj:epsg"]}}}
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
-
-def test_app_query_extension_limit_eq0(app_client):
- params = {"limit": 0}
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 400
-
-
-def test_app_query_extension_limit_lt0(
- load_test_data, app_client, postgres_transactions
-):
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- params = {"limit": -1}
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 400
-
-
-def test_app_query_extension_limit_gt10000(
- load_test_data, app_client, postgres_transactions
-):
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- params = {"limit": 10001}
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 200
-
-
-def test_app_query_extension_limit_10000(
- load_test_data, app_client, postgres_transactions
-):
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- params = {"limit": 10000}
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 200
-
-
-def test_app_sort_extension(load_test_data, app_client, postgres_transactions):
- first_item = load_test_data("test_item.json")
- item_date = datetime.strptime(
- first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%SZ"
- )
- postgres_transactions.create_item(
- first_item["collection"], first_item, request=MockStarletteRequest
- )
-
- second_item = load_test_data("test_item.json")
- second_item["id"] = "another-item"
- another_item_date = item_date - timedelta(days=1)
- second_item["properties"]["datetime"] = another_item_date.strftime(
- "%Y-%m-%dT%H:%M:%SZ"
- )
- postgres_transactions.create_item(
- second_item["collection"], second_item, request=MockStarletteRequest
- )
-
- params = {
- "collections": [first_item["collection"]],
- "sortby": [{"field": "datetime", "direction": "desc"}],
- }
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert resp_json["features"][0]["id"] == first_item["id"]
- assert resp_json["features"][1]["id"] == second_item["id"]
-
-
-def test_search_invalid_date(load_test_data, app_client, postgres_transactions):
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- params = {
- "datetime": "2020-XX-01/2020-10-30",
- "collections": [item["collection"]],
- }
-
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 400
-
-
-def test_search_point_intersects(load_test_data, app_client, postgres_transactions):
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- new_coordinates = list()
- for coordinate in item["geometry"]["coordinates"][0]:
- new_coordinates.append([coordinate[0] * -1, coordinate[1] * -1])
- item["id"] = "test-item-other-hemispheres"
- item["geometry"]["coordinates"] = [new_coordinates]
- item["bbox"] = list(value * -1 for value in item["bbox"])
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- point = [150.04, -33.14]
- intersects = {"type": "Point", "coordinates": point}
-
- params = {
- "intersects": intersects,
- "collections": [item["collection"]],
- }
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
- params["intersects"] = orjson.dumps(params["intersects"]).decode("utf-8")
- resp = app_client.get("/search", params=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
-
-def test_datetime_non_interval(load_test_data, app_client, postgres_transactions):
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
- alternate_formats = [
- "2020-02-12T12:30:22+00:00",
- "2020-02-12T12:30:22.00Z",
- "2020-02-12T12:30:22Z",
- "2020-02-12T12:30:22.00+00:00",
- ]
- for date in alternate_formats:
- params = {
- "datetime": date,
- "collections": [item["collection"]],
- }
-
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- # datetime is returned in this format "2020-02-12T12:30:22+00:00"
- assert resp_json["features"][0]["properties"]["datetime"][0:19] == date[0:19]
-
-
-def test_bbox_3d(load_test_data, app_client, postgres_transactions):
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- australia_bbox = [106.343365, -47.199523, 0.1, 168.218365, -19.437288, 0.1]
- params = {
- "bbox": australia_bbox,
- "collections": [item["collection"]],
- }
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
-
-def test_search_line_string_intersects(
- load_test_data, app_client, postgres_transactions
-):
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- line = [[150.04, -33.14], [150.22, -33.89]]
- intersects = {"type": "LineString", "coordinates": line}
-
- params = {
- "intersects": intersects,
- "collections": [item["collection"]],
- }
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
-
-def test_app_fields_extension_return_all_properties(
- load_test_data, app_client, postgres_transactions
-):
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- resp = app_client.get(
- "/search", params={"collections": ["test-collection"], "fields": "properties"}
- )
- assert resp.status_code == 200
- resp_json = resp.json()
- feature = resp_json["features"][0]
- assert len(feature["properties"]) >= len(item["properties"])
- for expected_prop, expected_value in item["properties"].items():
- if expected_prop in ("datetime", "created", "updated"):
- assert feature["properties"][expected_prop][0:19] == expected_value[0:19]
- else:
- assert feature["properties"][expected_prop] == expected_value
-
-
-def test_landing_forwarded_header(load_test_data, app_client, postgres_transactions):
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- response = app_client.get(
- "/",
- headers={
- "Forwarded": "proto=https;host=test:1234",
- "X-Forwarded-Proto": "http",
- "X-Forwarded-Port": "4321",
- },
- ).json()
- for link in response["links"]:
- assert link["href"].startswith("https://test:1234/")
-
-
-def test_app_search_response_forwarded_header(
- load_test_data, app_client, postgres_transactions
-):
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- resp = app_client.get(
- "/search",
- params={"collections": ["test-collection"]},
- headers={"Forwarded": "proto=https;host=testserver:1234"},
- )
- for feature in resp.json()["features"]:
- for link in feature["links"]:
- assert link["href"].startswith("https://testserver:1234/")
-
-
-def test_app_search_response_x_forwarded_headers(
- load_test_data, app_client, postgres_transactions
-):
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- resp = app_client.get(
- "/search",
- params={"collections": ["test-collection"]},
- headers={
- "X-Forwarded-Port": "1234",
- "X-Forwarded-Proto": "https",
- },
- )
- for feature in resp.json()["features"]:
- for link in feature["links"]:
- assert link["href"].startswith("https://testserver:1234/")
-
-
-def test_app_search_response_duplicate_forwarded_headers(
- load_test_data, app_client, postgres_transactions
-):
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- resp = app_client.get(
- "/search",
- params={"collections": ["test-collection"]},
- headers={
- "Forwarded": "proto=https;host=testserver:1234",
- "X-Forwarded-Port": "4321",
- "X-Forwarded-Proto": "http",
- },
- )
- for feature in resp.json()["features"]:
- for link in feature["links"]:
- assert link["href"].startswith("https://testserver:1234/")
-
-
-def test_get_features_content_type(app_client, load_test_data):
- item = load_test_data("test_item.json")
- resp = app_client.get(f"collections/{item['collection']}/items")
- assert resp.headers["content-type"] == "application/geo+json"
-
-
-def test_get_feature_content_type(app_client, load_test_data, postgres_transactions):
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
- resp = app_client.get(f"collections/{item['collection']}/items/{item['id']}")
- assert resp.headers["content-type"] == "application/geo+json"
-
-
-def test_item_collection_filter_bbox(load_test_data, app_client, postgres_transactions):
- item = load_test_data("test_item.json")
- collection = item["collection"]
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- bbox = "100,-50,170,-20"
- resp = app_client.get(f"/collections/{collection}/items", params={"bbox": bbox})
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
- bbox = "1,2,3,4"
- resp = app_client.get(f"/collections/{collection}/items", params={"bbox": bbox})
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 0
-
-
-def test_item_collection_filter_datetime(
- load_test_data, app_client, postgres_transactions
-):
- item = load_test_data("test_item.json")
- collection = item["collection"]
- postgres_transactions.create_item(
- item["collection"], item, request=MockStarletteRequest
- )
-
- datetime_range = "2020-01-01T00:00:00.00Z/.."
- resp = app_client.get(
- f"/collections/{collection}/items", params={"datetime": datetime_range}
- )
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 1
-
- datetime_range = "2018-01-01T00:00:00.00Z/2019-01-01T00:00:00.00Z"
- resp = app_client.get(
- f"/collections/{collection}/items", params={"datetime": datetime_range}
- )
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 0
diff --git a/stac_fastapi/sqlalchemy/tests/clients/__init__.py b/stac_fastapi/sqlalchemy/tests/clients/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/stac_fastapi/sqlalchemy/tests/clients/test_postgres.py b/stac_fastapi/sqlalchemy/tests/clients/test_postgres.py
deleted file mode 100644
index da69c78bb..000000000
--- a/stac_fastapi/sqlalchemy/tests/clients/test_postgres.py
+++ /dev/null
@@ -1,376 +0,0 @@
-import uuid
-from copy import deepcopy
-from typing import Callable
-
-import pytest
-from stac_pydantic import Collection, Item
-from tests.conftest import MockStarletteRequest
-
-from stac_fastapi.api.app import StacApi
-from stac_fastapi.extensions.third_party.bulk_transactions import Items
-from stac_fastapi.sqlalchemy.core import CoreCrudClient
-from stac_fastapi.sqlalchemy.transactions import (
- BulkTransactionsClient,
- TransactionsClient,
-)
-from stac_fastapi.types.errors import ConflictError, NotFoundError
-
-
-def test_create_collection(
- postgres_core: CoreCrudClient,
- postgres_transactions: TransactionsClient,
- load_test_data: Callable,
-):
- data = load_test_data("test_collection.json")
- resp = postgres_transactions.create_collection(data, request=MockStarletteRequest)
- assert Collection(**data).dict(exclude={"links"}) == Collection(**resp).dict(
- exclude={"links"}
- )
- coll = postgres_core.get_collection(data["id"], request=MockStarletteRequest)
- assert coll["id"] == data["id"]
-
-
-def test_create_collection_already_exists(
- postgres_transactions: TransactionsClient,
- load_test_data: Callable,
-):
- data = load_test_data("test_collection.json")
- postgres_transactions.create_collection(data, request=MockStarletteRequest)
-
- with pytest.raises(ConflictError):
- postgres_transactions.create_collection(data, request=MockStarletteRequest)
-
-
-def test_update_collection(
- postgres_core: CoreCrudClient,
- postgres_transactions: TransactionsClient,
- load_test_data: Callable,
-):
- data = load_test_data("test_collection.json")
- postgres_transactions.create_collection(data, request=MockStarletteRequest)
-
- data["keywords"].append("new keyword")
- postgres_transactions.update_collection(data, request=MockStarletteRequest)
-
- coll = postgres_core.get_collection(data["id"], request=MockStarletteRequest)
- assert "new keyword" in coll["keywords"]
-
-
-def test_delete_collection(
- postgres_core: CoreCrudClient,
- postgres_transactions: TransactionsClient,
- load_test_data: Callable,
-):
- data = load_test_data("test_collection.json")
- postgres_transactions.create_collection(data, request=MockStarletteRequest)
-
- deleted = postgres_transactions.delete_collection(
- data["id"], request=MockStarletteRequest
- )
-
- with pytest.raises(NotFoundError):
- postgres_core.get_collection(deleted["id"], request=MockStarletteRequest)
-
-
-def test_get_collection(
- postgres_core: CoreCrudClient,
- postgres_transactions: TransactionsClient,
- load_test_data: Callable,
-):
- data = load_test_data("test_collection.json")
- postgres_transactions.create_collection(data, request=MockStarletteRequest)
- coll = postgres_core.get_collection(data["id"], request=MockStarletteRequest)
- assert Collection(**data).dict(exclude={"links"}) == Collection(**coll).dict(
- exclude={"links"}
- )
- assert coll["id"] == data["id"]
-
-
-def test_get_item(
- postgres_core: CoreCrudClient,
- postgres_transactions: TransactionsClient,
- load_test_data: Callable,
-):
- collection_data = load_test_data("test_collection.json")
- postgres_transactions.create_collection(
- collection_data, request=MockStarletteRequest
- )
- data = load_test_data("test_item.json")
- postgres_transactions.create_item(
- collection_data["id"], data, request=MockStarletteRequest
- )
- coll = postgres_core.get_item(
- item_id=data["id"],
- collection_id=data["collection"],
- request=MockStarletteRequest,
- )
- assert coll["id"] == data["id"]
- assert coll["collection"] == data["collection"]
-
-
-def test_get_collection_items(
- postgres_core: CoreCrudClient,
- postgres_transactions: TransactionsClient,
- load_test_data: Callable,
-):
- coll = load_test_data("test_collection.json")
- postgres_transactions.create_collection(coll, request=MockStarletteRequest)
-
- item = load_test_data("test_item.json")
-
- for _ in range(5):
- item["id"] = str(uuid.uuid4())
- postgres_transactions.create_item(
- coll["id"], item, request=MockStarletteRequest
- )
-
- fc = postgres_core.item_collection(coll["id"], request=MockStarletteRequest)
- assert len(fc["features"]) == 5
-
- for item in fc["features"]:
- assert item["collection"] == coll["id"]
-
-
-def test_create_item(
- postgres_core: CoreCrudClient,
- postgres_transactions: TransactionsClient,
- load_test_data: Callable,
-):
- coll = load_test_data("test_collection.json")
- postgres_transactions.create_collection(coll, request=MockStarletteRequest)
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(coll["id"], item, request=MockStarletteRequest)
- resp = postgres_core.get_item(
- item["id"], item["collection"], request=MockStarletteRequest
- )
- assert Item(**item).dict(
- exclude={"links": ..., "properties": {"created", "updated"}}
- ) == Item(**resp).dict(exclude={"links": ..., "properties": {"created", "updated"}})
-
-
-def test_create_item_already_exists(
- postgres_transactions: TransactionsClient,
- load_test_data: Callable,
-):
- coll = load_test_data("test_collection.json")
- postgres_transactions.create_collection(coll, request=MockStarletteRequest)
-
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(coll["id"], item, request=MockStarletteRequest)
-
- with pytest.raises(ConflictError):
- postgres_transactions.create_item(
- coll["id"], item, request=MockStarletteRequest
- )
-
-
-def test_create_duplicate_item_different_collections(
- postgres_core: CoreCrudClient,
- postgres_transactions: TransactionsClient,
- load_test_data: Callable,
-):
- # create test-collection
- coll = load_test_data("test_collection.json")
- postgres_transactions.create_collection(coll, request=MockStarletteRequest)
-
- # create test-collection-2
- coll["id"] = "test-collection-2"
- postgres_transactions.create_collection(coll, request=MockStarletteRequest)
-
- # add item to test-collection
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(
- "test-collection", item, request=MockStarletteRequest
- )
-
- # get item from test-collection
- resp = postgres_core.get_item(
- item["id"], item["collection"], request=MockStarletteRequest
- )
- assert Item(**item).dict(
- exclude={"links": ..., "properties": {"created", "updated"}}
- ) == Item(**resp).dict(exclude={"links": ..., "properties": {"created", "updated"}})
-
- # add item to test-collection-2
- item["collection"] = "test-collection-2"
- postgres_transactions.create_item(
- "test-collection-2", item, request=MockStarletteRequest
- )
-
- # get item with same id from test-collection-2
- resp = postgres_core.get_item(
- item["id"], item["collection"], request=MockStarletteRequest
- )
- assert Item(**item).dict(
- exclude={"links": ..., "properties": {"created", "updated"}}
- ) == Item(**resp).dict(exclude={"links": ..., "properties": {"created", "updated"}})
-
-
-def test_update_item(
- postgres_core: CoreCrudClient,
- postgres_transactions: TransactionsClient,
- load_test_data: Callable,
-):
- coll = load_test_data("test_collection.json")
- postgres_transactions.create_collection(coll, request=MockStarletteRequest)
-
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(coll["id"], item, request=MockStarletteRequest)
-
- item["properties"]["foo"] = "bar"
- postgres_transactions.update_item(
- coll["id"], item["id"], item, request=MockStarletteRequest
- )
-
- updated_item = postgres_core.get_item(
- item["id"], item["collection"], request=MockStarletteRequest
- )
- assert updated_item["properties"]["foo"] == "bar"
-
-
-def test_update_geometry(
- postgres_core: CoreCrudClient,
- postgres_transactions: TransactionsClient,
- load_test_data: Callable,
-):
- coll = load_test_data("test_collection.json")
- postgres_transactions.create_collection(coll, request=MockStarletteRequest)
-
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(coll["id"], item, request=MockStarletteRequest)
-
- item["geometry"]["coordinates"] = [[[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]]]
- postgres_transactions.update_item(
- coll["id"], item["id"], item, request=MockStarletteRequest
- )
-
- updated_item = postgres_core.get_item(
- item["id"], item["collection"], request=MockStarletteRequest
- )
- assert updated_item["geometry"]["coordinates"] == item["geometry"]["coordinates"]
-
-
-def test_delete_item(
- postgres_core: CoreCrudClient,
- postgres_transactions: TransactionsClient,
- load_test_data: Callable,
-):
- coll = load_test_data("test_collection.json")
- postgres_transactions.create_collection(coll, request=MockStarletteRequest)
-
- item = load_test_data("test_item.json")
- postgres_transactions.create_item(coll["id"], item, request=MockStarletteRequest)
-
- postgres_transactions.delete_item(
- item["id"], item["collection"], request=MockStarletteRequest
- )
-
- with pytest.raises(NotFoundError):
- postgres_core.get_item(
- item["id"], item["collection"], request=MockStarletteRequest
- )
-
-
-def test_bulk_item_insert(
- postgres_core: CoreCrudClient,
- postgres_transactions: TransactionsClient,
- postgres_bulk_transactions: BulkTransactionsClient,
- load_test_data: Callable,
-):
- coll = load_test_data("test_collection.json")
- postgres_transactions.create_collection(coll, request=MockStarletteRequest)
-
- item = load_test_data("test_item.json")
-
- items = {}
- for _ in range(10):
- _item = deepcopy(item)
- _item["id"] = str(uuid.uuid4())
- items[_item["id"]] = _item
-
- fc = postgres_core.item_collection(coll["id"], request=MockStarletteRequest)
- assert len(fc["features"]) == 0
-
- postgres_bulk_transactions.bulk_item_insert(Items(items=items))
-
- fc = postgres_core.item_collection(coll["id"], request=MockStarletteRequest)
- assert len(fc["features"]) == 10
-
- for item in items.values():
- postgres_transactions.delete_item(
- item["id"], item["collection"], request=MockStarletteRequest
- )
-
-
-def test_bulk_item_insert_chunked(
- postgres_transactions: TransactionsClient,
- postgres_bulk_transactions: BulkTransactionsClient,
- load_test_data: Callable,
-):
- coll = load_test_data("test_collection.json")
- postgres_transactions.create_collection(coll, request=MockStarletteRequest)
-
- item = load_test_data("test_item.json")
-
- items = []
- for _ in range(10):
- _item = deepcopy(item)
- _item["id"] = str(uuid.uuid4())
- items.append(_item)
-
- postgres_bulk_transactions.bulk_item_insert(items=items, chunk_size=2)
-
- for item in items:
- postgres_transactions.delete_item(
- item["id"], item["collection"], request=MockStarletteRequest
- )
-
-
-def test_feature_collection_insert(
- postgres_core: CoreCrudClient,
- postgres_transactions: TransactionsClient,
- load_test_data: Callable,
-):
- coll = load_test_data("test_collection.json")
- postgres_transactions.create_collection(coll, request=MockStarletteRequest)
-
- item = load_test_data("test_item.json")
-
- features = []
- for _ in range(10):
- _item = deepcopy(item)
- _item["id"] = str(uuid.uuid4())
- features.append(_item)
-
- feature_collection = {"type": "FeatureCollection", "features": features}
-
- postgres_transactions.create_item(
- coll["id"], feature_collection, request=MockStarletteRequest
- )
-
- fc = postgres_core.item_collection(coll["id"], request=MockStarletteRequest)
- assert len(fc["features"]) >= 10
-
- for item in features:
- postgres_transactions.delete_item(
- item["id"], item["collection"], request=MockStarletteRequest
- )
-
-
-def test_landing_page_no_collection_title(
- postgres_core: CoreCrudClient,
- postgres_transactions: TransactionsClient,
- load_test_data: Callable,
- api_client: StacApi,
-):
- class MockStarletteRequestWithApp(MockStarletteRequest):
- app = api_client.app
-
- coll = load_test_data("test_collection.json")
- del coll["title"]
- postgres_transactions.create_collection(coll, request=MockStarletteRequest)
-
- landing_page = postgres_core.landing_page(request=MockStarletteRequestWithApp)
- for link in landing_page["links"]:
- if link["href"].split("/")[-1] == coll["id"]:
- assert link["title"]
diff --git a/stac_fastapi/sqlalchemy/tests/conftest.py b/stac_fastapi/sqlalchemy/tests/conftest.py
deleted file mode 100644
index 86984a12f..000000000
--- a/stac_fastapi/sqlalchemy/tests/conftest.py
+++ /dev/null
@@ -1,155 +0,0 @@
-import json
-import os
-from typing import Callable, Dict
-
-import pytest
-from starlette.testclient import TestClient
-
-from stac_fastapi.api.app import StacApi
-from stac_fastapi.api.models import create_request_model
-from stac_fastapi.extensions.core import (
- ContextExtension,
- FieldsExtension,
- SortExtension,
- TokenPaginationExtension,
- TransactionExtension,
-)
-from stac_fastapi.sqlalchemy.config import SqlalchemySettings
-from stac_fastapi.sqlalchemy.core import CoreCrudClient
-from stac_fastapi.sqlalchemy.extensions import QueryExtension
-from stac_fastapi.sqlalchemy.models import database
-from stac_fastapi.sqlalchemy.session import Session
-from stac_fastapi.sqlalchemy.transactions import (
- BulkTransactionsClient,
- TransactionsClient,
-)
-from stac_fastapi.types.config import Settings
-from stac_fastapi.types.search import BaseSearchGetRequest, BaseSearchPostRequest
-
-DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
-
-
-class TestSettings(SqlalchemySettings):
- class Config:
- env_file = ".env.test"
-
-
-settings = TestSettings()
-Settings.set(settings)
-
-
-@pytest.fixture(autouse=True)
-def cleanup(postgres_core: CoreCrudClient, postgres_transactions: TransactionsClient):
- yield
- collections = postgres_core.all_collections(request=MockStarletteRequest)
- for coll in collections["collections"]:
- if coll["id"].split("-")[0] == "test":
- # Delete the items
- items = postgres_core.item_collection(
- coll["id"], limit=100, request=MockStarletteRequest
- )
- for feat in items["features"]:
- postgres_transactions.delete_item(
- feat["id"], feat["collection"], request=MockStarletteRequest
- )
-
- # Delete the collection
- postgres_transactions.delete_collection(
- coll["id"], request=MockStarletteRequest
- )
-
-
-@pytest.fixture
-def load_test_data() -> Callable[[str], Dict]:
- def load_file(filename: str) -> Dict:
- with open(os.path.join(DATA_DIR, filename)) as file:
- return json.load(file)
-
- return load_file
-
-
-class MockStarletteRequest:
- base_url = "http://test-server"
- url = "http://test-server/some/endpoint"
-
-
-@pytest.fixture
-def db_session() -> Session:
- return Session(
- reader_conn_string=settings.reader_connection_string,
- writer_conn_string=settings.writer_connection_string,
- )
-
-
-@pytest.fixture
-def postgres_core(db_session):
- return CoreCrudClient(
- session=db_session,
- item_table=database.Item,
- collection_table=database.Collection,
- token_table=database.PaginationToken,
- )
-
-
-@pytest.fixture
-def postgres_transactions(db_session):
- return TransactionsClient(
- session=db_session,
- item_table=database.Item,
- collection_table=database.Collection,
- )
-
-
-@pytest.fixture
-def postgres_bulk_transactions(db_session):
- return BulkTransactionsClient(session=db_session)
-
-
-@pytest.fixture
-def api_client(db_session):
- settings = SqlalchemySettings()
- extensions = [
- TransactionExtension(
- client=TransactionsClient(session=db_session), settings=settings
- ),
- ContextExtension(),
- SortExtension(),
- FieldsExtension(),
- QueryExtension(),
- TokenPaginationExtension(),
- ]
-
- get_request_model = create_request_model(
- "SearchGetRequest",
- base_model=BaseSearchGetRequest,
- extensions=extensions,
- request_type="GET",
- )
-
- post_request_model = create_request_model(
- "SearchPostRequest",
- base_model=BaseSearchPostRequest,
- extensions=extensions,
- request_type="POST",
- )
-
- return StacApi(
- settings=settings,
- client=CoreCrudClient(
- session=db_session,
- extensions=extensions,
- post_request_model=post_request_model,
- ),
- extensions=extensions,
- search_get_request_model=get_request_model,
- search_post_request_model=post_request_model,
- )
-
-
-@pytest.fixture
-def app_client(api_client, load_test_data, postgres_transactions):
- coll = load_test_data("test_collection.json")
- postgres_transactions.create_collection(coll, request=MockStarletteRequest)
-
- with TestClient(api_client.app) as test_app:
- yield test_app
diff --git a/stac_fastapi/sqlalchemy/tests/data/test_collection.json b/stac_fastapi/sqlalchemy/tests/data/test_collection.json
deleted file mode 100644
index 5028bfeae..000000000
--- a/stac_fastapi/sqlalchemy/tests/data/test_collection.json
+++ /dev/null
@@ -1,167 +0,0 @@
-{
- "id": "test-collection",
- "stac_extensions": ["https://stac-extensions.github.io/eo/v1.0.0/schema.json"],
- "type": "Collection",
- "description": "Landat 8 imagery radiometrically calibrated and orthorectified using gound points and Digital Elevation Model (DEM) data to correct relief displacement.",
- "stac_version": "1.0.0",
- "license": "PDDL-1.0",
- "summaries": {
- "platform": ["landsat-8"],
- "instruments": ["oli", "tirs"],
- "gsd": [30],
- "eo:bands": [
- {
- "name": "B1",
- "common_name": "coastal",
- "center_wavelength": 0.44,
- "full_width_half_max": 0.02
- },
- {
- "name": "B2",
- "common_name": "blue",
- "center_wavelength": 0.48,
- "full_width_half_max": 0.06
- },
- {
- "name": "B3",
- "common_name": "green",
- "center_wavelength": 0.56,
- "full_width_half_max": 0.06
- },
- {
- "name": "B4",
- "common_name": "red",
- "center_wavelength": 0.65,
- "full_width_half_max": 0.04
- },
- {
- "name": "B5",
- "common_name": "nir",
- "center_wavelength": 0.86,
- "full_width_half_max": 0.03
- },
- {
- "name": "B6",
- "common_name": "swir16",
- "center_wavelength": 1.6,
- "full_width_half_max": 0.08
- },
- {
- "name": "B7",
- "common_name": "swir22",
- "center_wavelength": 2.2,
- "full_width_half_max": 0.2
- },
- {
- "name": "B8",
- "common_name": "pan",
- "center_wavelength": 0.59,
- "full_width_half_max": 0.18
- },
- {
- "name": "B9",
- "common_name": "cirrus",
- "center_wavelength": 1.37,
- "full_width_half_max": 0.02
- },
- {
- "name": "B10",
- "common_name": "lwir11",
- "center_wavelength": 10.9,
- "full_width_half_max": 0.8
- },
- {
- "name": "B11",
- "common_name": "lwir12",
- "center_wavelength": 12,
- "full_width_half_max": 1
- }
- ]
- },
- "extent": {
- "spatial": {
- "bbox": [
- [
- -180.0,
- -90.0,
- 180.0,
- 90.0
- ]
- ]
- },
- "temporal": {
- "interval": [
- [
- "2013-06-01",
- null
- ]
- ]
- }
- },
- "links": [
- {
- "href": "http://localhost:8081/collections/landsat-8-l1",
- "rel": "self",
- "type": "application/json"
- },
- {
- "href": "http://localhost:8081/",
- "rel": "parent",
- "type": "application/json"
- },
- {
- "href": "http://localhost:8081/collections/landsat-8-l1/items",
- "rel": "item",
- "type": "application/geo+json"
- },
- {
- "href": "http://localhost:8081/",
- "rel": "root",
- "type": "application/json"
- }
- ],
- "title": "Landsat 8 L1",
- "keywords": [
- "landsat",
- "earth observation",
- "usgs"
- ],
- "providers": [
- {
- "name": "USGS",
- "roles": [
- "producer"
- ],
- "url": "https://landsat.usgs.gov/"
- },
- {
- "name": "Planet Labs",
- "roles": [
- "processor"
- ],
- "url": "https://github.com/landsat-pds/landsat_ingestor"
- },
- {
- "name": "AWS",
- "roles": [
- "host"
- ],
- "url": "https://landsatonaws.com/"
- },
- {
- "name": "Development Seed",
- "roles": [
- "processor"
- ],
- "url": "https://github.com/sat-utils/sat-api"
- },
- {
- "name": "Earth Search by Element84",
- "description": "API of Earth on AWS datasets",
- "roles": [
- "host"
- ],
- "url": "https://element84.com"
- }
- ]
-}
\ No newline at end of file
diff --git a/stac_fastapi/sqlalchemy/tests/data/test_item.json b/stac_fastapi/sqlalchemy/tests/data/test_item.json
deleted file mode 100644
index 2b7fdd860..000000000
--- a/stac_fastapi/sqlalchemy/tests/data/test_item.json
+++ /dev/null
@@ -1,505 +0,0 @@
-{
- "type": "Feature",
- "id": "test-item",
- "stac_version": "1.0.0",
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "geometry": {
- "coordinates": [
- [
- [
- 152.15052873427666,
- -33.82243006904891
- ],
- [
- 150.1000346138806,
- -34.257132625788756
- ],
- [
- 149.5776607193635,
- -32.514709769700254
- ],
- [
- 151.6262528041627,
- -32.08081674221862
- ],
- [
- 152.15052873427666,
- -33.82243006904891
- ]
- ]
- ],
- "type": "Polygon"
- },
- "properties": {
- "datetime": "2020-02-12T12:30:22Z",
- "landsat:scene_id": "LC82081612020043LGN00",
- "landsat:row": "161",
- "gsd": 15,
- "eo:bands": [
- {
- "gsd": 30,
- "name": "B1",
- "common_name": "coastal",
- "center_wavelength": 0.44,
- "full_width_half_max": 0.02
- },
- {
- "gsd": 30,
- "name": "B2",
- "common_name": "blue",
- "center_wavelength": 0.48,
- "full_width_half_max": 0.06
- },
- {
- "gsd": 30,
- "name": "B3",
- "common_name": "green",
- "center_wavelength": 0.56,
- "full_width_half_max": 0.06
- },
- {
- "gsd": 30,
- "name": "B4",
- "common_name": "red",
- "center_wavelength": 0.65,
- "full_width_half_max": 0.04
- },
- {
- "gsd": 30,
- "name": "B5",
- "common_name": "nir",
- "center_wavelength": 0.86,
- "full_width_half_max": 0.03
- },
- {
- "gsd": 30,
- "name": "B6",
- "common_name": "swir16",
- "center_wavelength": 1.6,
- "full_width_half_max": 0.08
- },
- {
- "gsd": 30,
- "name": "B7",
- "common_name": "swir22",
- "center_wavelength": 2.2,
- "full_width_half_max": 0.2
- },
- {
- "gsd": 15,
- "name": "B8",
- "common_name": "pan",
- "center_wavelength": 0.59,
- "full_width_half_max": 0.18
- },
- {
- "gsd": 30,
- "name": "B9",
- "common_name": "cirrus",
- "center_wavelength": 1.37,
- "full_width_half_max": 0.02
- },
- {
- "gsd": 100,
- "name": "B10",
- "common_name": "lwir11",
- "center_wavelength": 10.9,
- "full_width_half_max": 0.8
- },
- {
- "gsd": 100,
- "name": "B11",
- "common_name": "lwir12",
- "center_wavelength": 12,
- "full_width_half_max": 1
- }
- ],
- "landsat:revision": "00",
- "view:sun_azimuth": -148.83296771,
- "instrument": "OLI_TIRS",
- "landsat:product_id": "LC08_L1GT_208161_20200212_20200212_01_RT",
- "eo:cloud_cover": 0,
- "landsat:tier": "RT",
- "landsat:processing_level": "L1GT",
- "landsat:column": "208",
- "platform": "landsat-8",
- "proj:epsg": 32756,
- "view:sun_elevation": -37.30791534,
- "view:off_nadir": 0,
- "height": 2500,
- "width": 2500
- },
- "bbox": [
- 149.57574,
- -34.25796,
- 152.15194,
- -32.07915
- ],
- "collection": "test-collection",
- "assets": {
- "ANG": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_ANG.txt",
- "type": "text/plain",
- "title": "Angle Coefficients File",
- "description": "Collection 2 Level-1 Angle Coefficients File (ANG)"
- },
- "SR_B1": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_SR_B1.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Coastal/Aerosol Band (B1)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B1",
- "common_name": "coastal",
- "center_wavelength": 0.44,
- "full_width_half_max": 0.02
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Collection 2 Level-2 Coastal/Aerosol Band (B1) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "SR_B2": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_SR_B2.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Blue Band (B2)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B2",
- "common_name": "blue",
- "center_wavelength": 0.48,
- "full_width_half_max": 0.06
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Collection 2 Level-2 Blue Band (B2) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "SR_B3": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_SR_B3.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Green Band (B3)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B3",
- "common_name": "green",
- "center_wavelength": 0.56,
- "full_width_half_max": 0.06
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Collection 2 Level-2 Green Band (B3) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "SR_B4": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_SR_B4.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Red Band (B4)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B4",
- "common_name": "red",
- "center_wavelength": 0.65,
- "full_width_half_max": 0.04
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Collection 2 Level-2 Red Band (B4) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "SR_B5": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_SR_B5.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Near Infrared Band 0.8 (B5)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B5",
- "common_name": "nir08",
- "center_wavelength": 0.86,
- "full_width_half_max": 0.03
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Collection 2 Level-2 Near Infrared Band 0.8 (B5) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "SR_B6": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_SR_B6.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Short-wave Infrared Band 1.6 (B6)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B6",
- "common_name": "swir16",
- "center_wavelength": 1.6,
- "full_width_half_max": 0.08
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Collection 2 Level-2 Short-wave Infrared Band 1.6 (B6) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "SR_B7": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_SR_B7.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Short-wave Infrared Band 2.2 (B7)",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "SR_B7",
- "common_name": "swir22",
- "center_wavelength": 2.2,
- "full_width_half_max": 0.2
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Collection 2 Level-2 Short-wave Infrared Band 2.2 (B7) Surface Reflectance",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "ST_QA": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_ST_QA.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Surface Temperature Quality Assessment Band",
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Landsat Collection 2 Level-2 Surface Temperature Band Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "ST_B10": {
- "gsd": 100,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_ST_B10.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Surface Temperature Band (B10)",
- "eo:bands": [
- {
- "gsd": 100,
- "name": "ST_B10",
- "common_name": "lwir11",
- "center_wavelength": 10.9,
- "full_width_half_max": 0.8
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Landsat Collection 2 Level-2 Surface Temperature Band (B10) Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "MTL.txt": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_MTL.txt",
- "type": "text/plain",
- "title": "Product Metadata File",
- "description": "Collection 2 Level-1 Product Metadata File (MTL)"
- },
- "MTL.xml": {
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_MTL.xml",
- "type": "application/xml",
- "title": "Product Metadata File (xml)",
- "description": "Collection 2 Level-1 Product Metadata File (xml)"
- },
- "ST_DRAD": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_ST_DRAD.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Downwelled Radiance Band",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "ST_DRAD",
- "description": "downwelled radiance"
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Landsat Collection 2 Level-2 Downwelled Radiance Band Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "ST_EMIS": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_ST_EMIS.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Emissivity Band",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "ST_EMIS",
- "description": "emissivity"
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Landsat Collection 2 Level-2 Emissivity Band Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- },
- "ST_EMSD": {
- "gsd": 30,
- "href": "https://landsateuwest.blob.core.windows.net/landsat-c2/level-2/standard/oli-tirs/2021/108/066/LC08_L2SP_108066_20210712_20210720_02_T1/LC08_L2SP_108066_20210712_20210720_02_T1_ST_EMSD.TIF",
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "title": "Emissivity Standard Deviation Band",
- "eo:bands": [
- {
- "gsd": 30,
- "name": "ST_EMSD",
- "description": "emissivity standard deviation"
- }
- ],
- "proj:shape": [
- 7731,
- 7591
- ],
- "description": "Landsat Collection 2 Level-2 Emissivity Standard Deviation Band Surface Temperature Product",
- "proj:transform": [
- 30,
- 0,
- 304185,
- 0,
- -30,
- -843585
- ]
- }
- },
- "links": [
- {
- "href": "http://localhost:8081/collections/landsat-8-l1/items/LC82081612020043",
- "rel": "self",
- "type": "application/geo+json"
- },
- {
- "href": "http://localhost:8081/collections/landsat-8-l1",
- "rel": "parent",
- "type": "application/json"
- },
- {
- "href": "http://localhost:8081/collections/landsat-8-l1",
- "rel": "collection",
- "type": "application/json"
- },
- {
- "href": "http://localhost:8081/",
- "rel": "root",
- "type": "application/json"
- }
- ]
-}
\ No newline at end of file
diff --git a/stac_fastapi/sqlalchemy/tests/data/test_item_geometry_null.json b/stac_fastapi/sqlalchemy/tests/data/test_item_geometry_null.json
deleted file mode 100644
index 27ef327a7..000000000
--- a/stac_fastapi/sqlalchemy/tests/data/test_item_geometry_null.json
+++ /dev/null
@@ -1,169 +0,0 @@
-{
- "type": "Feature",
- "stac_version": "1.0.0",
- "stac_extensions": [
- "https://landsat.usgs.gov/stac/landsat-ard-extension/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json",
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/alternate-assets/v1.1.0/schema.json",
- "https://stac-extensions.github.io/storage/v1.0.0/schema.json"
- ],
- "id": "LE07_CU_002012_20150101_20210502_02_BA",
- "description": "Landsat Collection 2 Level-3 Burned Area Product",
- "geometry": null,
- "properties": {
- "datetime": "2015-01-01T18:39:12.4885358Z",
- "platform": "LANDSAT_7",
- "instruments": [
- "ETM"
- ],
- "landsat:grid_horizontal": "02",
- "landsat:grid_vertical": "12",
- "landsat:grid_region": "CU",
- "landsat:scene_count": 1,
- "eo:cloud_cover": 0.0759,
- "landsat:cloud_shadow_cover": 0.1394,
- "landsat:snow_ice_cover": 0,
- "landsat:fill": 95.4286,
- "proj:epsg": null,
- "proj:shape": [
- 5000,
- 5000
- ],
- "proj:transform": [
- 30,
- 0,
- -2265585,
- 0,
- -30,
- 1514805
- ],
- "created": "2022-02-08T20:07:38.885Z",
- "updated": "2022-02-08T20:07:38.885Z"
- },
- "assets": {
- "index": {
- "title": "HTML index page",
- "type": "text/html",
- "roles": [
- "metadata"
- ],
- "href": "https://landsatlook.usgs.gov/stac-browser/collection02/BA/2015/CU/002/012/LE07_CU_002012_20150101_20210502_02_BA/LE07_CU_002012_20150101_20210502_02"
- },
- "bp": {
- "title": "Burn Probability",
- "description": "Collection 2 Level-3 Albers Burn Probability Burned Area",
- "type": "image/vnd.stac.geotiff; cloud-optimized=true",
- "roles": [
- "data"
- ],
- "href": "https://landsatlook.usgs.gov/level-3/collection02/BA/2015/CU/002/012/LE07_CU_002012_20150101_20210502_02_BA/LE07_CU_002012_20150101_20210502_02_BP.TIF",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat-level-3/collection02/BA/2015/CU/002/012/LE07_CU_002012_20150101_20210502_02_BA/LE07_CU_002012_20150101_20210502_02_BP.TIF"
- }
- }
- },
- "bc": {
- "title": "Burn Classification",
- "description": "Collection 2 Level-3 Albers Burn Classification Burned Area",
- "type": "image/vnd.stac.geotiff; cloud-optimized=true",
- "roles": [
- "data"
- ],
- "href": "https://landsatlook.usgs.gov/level-3/collection02/BA/2015/CU/002/012/LE07_CU_002012_20150101_20210502_02_BA/LE07_CU_002012_20150101_20210502_02_BC.TIF",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat-level-3/collection02/BA/2015/CU/002/012/LE07_CU_002012_20150101_20210502_02_BA/LE07_CU_002012_20150101_20210502_02_BC.TIF"
- }
- }
- },
- "quick_look": {
- "title": "Quick Look File",
- "description": "Collection 2 Level-3 Albers Quick Look File Burned Area",
- "type": "image/png",
- "roles": [
- "data"
- ],
- "href": "https://landsatlook.usgs.gov/level-3/collection02/BA/2015/CU/002/012/LE07_CU_002012_20150101_20210502_02_BA/LE07_CU_002012_20150101_20210502_02_QuickLook.png",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat-level-3/collection02/BA/2015/CU/002/012/LE07_CU_002012_20150101_20210502_02_BA/LE07_CU_002012_20150101_20210502_02_QuickLook.png"
- }
- }
- },
- "xml": {
- "title": "Extensible Metadata File",
- "description": "Collection 2 Level-3 Albers Extensible Metadata File Burned Area",
- "type": "application/xml",
- "roles": [
- "metadata"
- ],
- "href": "https://landsatlook.usgs.gov/level-3/collection02/BA/2015/CU/002/012/LE07_CU_002012_20150101_20210502_02_BA/LE07_CU_002012_20150101_20210502_02.xml",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat-level-3/collection02/BA/2015/CU/002/012/LE07_CU_002012_20150101_20210502_02_BA/LE07_CU_002012_20150101_20210502_02.xml"
- }
- }
- },
- "json": {
- "title": "Extensible Metadata File (json)",
- "description": "Collection 2 Level-3 Albers Extensible Metadata File (json) Burned Area",
- "type": "application/json",
- "roles": [
- "metadata"
- ],
- "href": "https://landsatlook.usgs.gov/level-3/collection02/BA/2015/CU/002/012/LE07_CU_002012_20150101_20210502_02_BA/LE07_CU_002012_20150101_20210502_02.json",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat-level-3/collection02/BA/2015/CU/002/012/LE07_CU_002012_20150101_20210502_02_BA/LE07_CU_002012_20150101_20210502_02.json"
- }
- }
- }
- },
- "links": [
- {
- "rel": "self",
- "href": "https://landsatlook.usgs.gov/stac-server/collections/landsat-c2l3-ba/items/LE07_CU_002012_20150101_20210502_02_BA"
- },
- {
- "rel": "derived_from",
- "href": "https://landsatlook.usgs.gov/stac-server/collections/landsat-c2ard-sr/items/LE07_CU_002012_20150101_20210502_02_SR"
- },
- {
- "rel": "derived_from",
- "href": "https://landsatlook.usgs.gov/stac-server/collections/landsat-c2ard-st/items/LE07_CU_002012_20150101_20210502_02_ST"
- },
- {
- "rel": "derived_from",
- "href": "https://landsatlook.usgs.gov/stac-server/collections/landsat-c2ard-ta/items/LE07_CU_002012_20150101_20210502_02_TOA"
- },
- {
- "rel": "derived_from",
- "href": "https://landsatlook.usgs.gov/stac-server/collections/landsat-c2ard-bt/items/LE07_CU_002012_20150101_20210502_02_BT"
- },
- {
- "rel": "parent",
- "href": "https://landsatlook.usgs.gov/stac-server/collections/landsat-c2l3-ba"
- },
- {
- "rel": "collection",
- "href": "https://landsatlook.usgs.gov/stac-server/collections/landsat-c2l3-ba"
- },
- {
- "rel": "root",
- "href": "https://landsatlook.usgs.gov/stac-server/"
- }
- ],
- "collection": "test-collection"
- }
\ No newline at end of file
diff --git a/stac_fastapi/sqlalchemy/tests/data/test_item_multipolygon.json b/stac_fastapi/sqlalchemy/tests/data/test_item_multipolygon.json
deleted file mode 100644
index f5701c3a9..000000000
--- a/stac_fastapi/sqlalchemy/tests/data/test_item_multipolygon.json
+++ /dev/null
@@ -1,454 +0,0 @@
-{
- "type": "Feature",
- "stac_version": "1.0.0",
- "stac_extensions": [
- "https://landsat.usgs.gov/stac/landsat-extension/v1.1.1/schema.json",
- "https://stac-extensions.github.io/view/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json",
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/alternate-assets/v1.1.0/schema.json",
- "https://stac-extensions.github.io/storage/v1.0.0/schema.json"
- ],
- "id": "LE07_L2SP_092013_20211007_20211104_02_T2_SR",
- "description": "Landsat Collection 2 Level-2 Surface Reflectance Product",
- "bbox": [
- 175.93215804933186,
- 65.93036549677463,
- -178.26673562596073,
- 68.07019813171695
- ],
- "geometry": {
- "type": "MultiPolygon",
- "coordinates": [
- [
- [
- [
- 180.0,
- 67.67956138964027
- ],
- [
- 177.4008122028755,
- 68.07019813171695
- ],
- [
- 175.93215804933186,
- 66.54096344674578
- ],
- [
- 180.0,
- 65.93733582837588
- ],
- [
- 180.0,
- 67.67956138964027
- ]
- ]
- ],
- [
- [
- [
- -180.0,
- 65.93733582837588
- ],
- [
- -179.95302698810534,
- 65.93036549677463
- ],
- [
- -178.3207049853914,
- 67.36419976494292
- ],
- [
- -178.26673562596073,
- 67.41036545485302
- ],
- [
- -178.27732165481333,
- 67.42065687448587
- ],
- [
- -180.0,
- 67.67956138964027
- ],
- [
- -180.0,
- 65.93733582837588
- ]
- ]
- ]
- ]
- },
- "properties": {
- "datetime": "2021-10-07T22:29:48Z",
- "eo:cloud_cover": 50.0,
- "view:sun_azimuth": 158.59868248,
- "view:sun_elevation": 15.64343101,
- "platform": "LANDSAT_7",
- "instruments": [
- "ETM"
- ],
- "view:off_nadir": 0,
- "landsat:cloud_cover_land": 0.0,
- "landsat:wrs_type": "2",
- "landsat:wrs_path": "092",
- "landsat:wrs_row": "013",
- "landsat:scene_id": "LE70920132021280ASN00",
- "landsat:collection_category": "T2",
- "landsat:collection_number": "02",
- "landsat:correction": "L2SP",
- "proj:epsg": 32660,
- "proj:shape": [
- 8011,
- 8731
- ],
- "proj:transform": [
- 30.0,
- 0.0,
- 446085.0,
- 0.0,
- -30.0,
- 7553415.0
- ]
- },
- "assets": {
- "thumbnail": {
- "title": "Thumbnail image",
- "type": "image/jpeg",
- "roles": [
- "thumbnail"
- ],
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_thumb_small.jpeg",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_thumb_small.jpeg"
- }
- }
- },
- "reduced_resolution_browse": {
- "title": "Reduced resolution browse image",
- "type": "image/jpeg",
- "roles": [
- "overview"
- ],
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_thumb_large.jpeg",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_thumb_large.jpeg"
- }
- }
- },
- "index": {
- "title": "HTML index page",
- "type": "text/html",
- "roles": [
- "metadata"
- ],
- "href": "https://landsatlook.usgs.gov/stac-browser/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2"
- },
- "blue": {
- "title": "Blue Band (B1)",
- "description": "Collection 2 Level-2 Blue Band (B1) Surface Reflectance",
- "type": "image/vnd.stac.geotiff; cloud-optimized=true",
- "roles": [
- "data"
- ],
- "eo:bands": [
- {
- "name": "B1",
- "common_name": "blue",
- "gsd": 30,
- "center_wavelength": 0.48
- }
- ],
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_SR_B1.TIF",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_SR_B1.TIF"
- }
- }
- },
- "green": {
- "title": "Green Band (B2)",
- "description": "Collection 2 Level-2 Green Band (B2) Surface Reflectance",
- "type": "image/vnd.stac.geotiff; cloud-optimized=true",
- "roles": [
- "data"
- ],
- "eo:bands": [
- {
- "name": "B2",
- "common_name": "green",
- "gsd": 30,
- "center_wavelength": 0.56
- }
- ],
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_SR_B2.TIF",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_SR_B2.TIF"
- }
- }
- },
- "red": {
- "title": "Red Band (B3)",
- "description": "Collection 2 Level-2 Red Band (B3) Surface Reflectance",
- "type": "image/vnd.stac.geotiff; cloud-optimized=true",
- "roles": [
- "data"
- ],
- "eo:bands": [
- {
- "name": "B3",
- "common_name": "red",
- "gsd": 30,
- "center_wavelength": 0.65
- }
- ],
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_SR_B3.TIF",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_SR_B3.TIF"
- }
- }
- },
- "nir08": {
- "title": "Near Infrared Band 0.8 (B4)",
- "description": "Collection 2 Level-2 Near Infrared Band 0.8 (B4) Surface Reflectance",
- "type": "image/vnd.stac.geotiff; cloud-optimized=true",
- "roles": [
- "data",
- "reflectance"
- ],
- "eo:bands": [
- {
- "name": "B4",
- "common_name": "nir08",
- "gsd": 30,
- "center_wavelength": 0.86
- }
- ],
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_SR_B4.TIF",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_SR_B4.TIF"
- }
- }
- },
- "swir16": {
- "title": "Short-wave Infrared Band 1.6 (B5)",
- "description": "Collection 2 Level-2 Short-wave Infrared Band 1.6 (B6) Surface Reflectance",
- "type": "image/vnd.stac.geotiff; cloud-optimized=true",
- "roles": [
- "data",
- "reflectance"
- ],
- "eo:bands": [
- {
- "name": "B5",
- "common_name": "swir16",
- "gsd": 30,
- "center_wavelength": 1.6
- }
- ],
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_SR_B5.TIF",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_SR_B5.TIF"
- }
- }
- },
- "swir22": {
- "title": "Short-wave Infrared Band 2.2 (B7)",
- "description": "Collection 2 Level-2 Short-wave Infrared Band 2.2 (B7) Surface Reflectance",
- "type": "image/vnd.stac.geotiff; cloud-optimized=true",
- "roles": [
- "data",
- "reflectance"
- ],
- "eo:bands": [
- {
- "name": "B7",
- "common_name": "swir22",
- "gsd": 30,
- "center_wavelength": 2.2
- }
- ],
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_SR_B7.TIF",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_SR_B7.TIF"
- }
- }
- },
- "atmos_opacity": {
- "title": "Atmospheric Opacity Band",
- "description": "Collection 2 Level-2 Atmospheric Opacity Band Surface Reflectance",
- "type": "image/vnd.stac.geotiff; cloud-optimized=true",
- "roles": [
- "data"
- ],
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_SR_ATMOS_OPACITY.TIF",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_SR_ATMOS_OPACITY.TIF"
- }
- }
- },
- "cloud_qa": {
- "title": "Cloud Quality Analysis Band",
- "description": "Collection 2 Level-2 Cloud Quality Opacity Band Surface Reflectance",
- "type": "image/vnd.stac.geotiff; cloud-optimized=true",
- "roles": [
- "metadata",
- "cloud",
- "cloud-shadow",
- "snow-ice",
- "water-mask"
- ],
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_SR_CLOUD_QA.TIF",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_SR_CLOUD_QA.TIF"
- }
- }
- },
- "ANG.txt": {
- "title": "Angle Coefficients File",
- "description": "Collection 2 Level-2 Angle Coefficients File (ANG)",
- "type": "text/plain",
- "roles": [
- "metadata"
- ],
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_ANG.txt",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_ANG.txt"
- }
- }
- },
- "MTL.txt": {
- "title": "Product Metadata File",
- "description": "Collection 2 Level-2 Product Metadata File (MTL)",
- "type": "text/plain",
- "roles": [
- "metadata"
- ],
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_MTL.txt",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_MTL.txt"
- }
- }
- },
- "MTL.xml": {
- "title": "Product Metadata File (xml)",
- "description": "Collection 2 Level-1 Product Metadata File (xml)",
- "type": "application/xml",
- "roles": [
- "metadata"
- ],
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_MTL.xml",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_MTL.xml"
- }
- }
- },
- "MTL.json": {
- "title": "Product Metadata File (json)",
- "description": "Collection 2 Level-2 Product Metadata File (json)",
- "type": "application/json",
- "roles": [
- "metadata"
- ],
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_MTL.json",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_MTL.json"
- }
- }
- },
- "qa_pixel": {
- "title": "Pixel Quality Assessment Band",
- "description": "Collection 2 Level-2 Pixel Quality Assessment Band Surface Reflectance",
- "type": "image/vnd.stac.geotiff; cloud-optimized=true",
- "roles": [
- "cloud",
- "cloud-shadow",
- "snow-ice",
- "water-mask"
- ],
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_QA_PIXEL.TIF",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_QA_PIXEL.TIF"
- }
- }
- },
- "qa_radsat": {
- "title": "Radiometric Saturation Quality Assessment Band",
- "description": "Collection 2 Level-2 Radiometric Saturation Quality Assessment Band Surface Reflectance",
- "type": "image/vnd.stac.geotiff; cloud-optimized=true",
- "roles": [
- "saturation"
- ],
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_QA_RADSAT.TIF",
- "alternate": {
- "s3": {
- "storage:platform": "AWS",
- "storage:requester_pays": true,
- "href": "s3://usgs-landsat/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_QA_RADSAT.TIF"
- }
- }
- }
- },
- "links": [
- {
- "rel": "root",
- "href": "https://landsatlook.usgs.gov/data/catalog.json"
- },
- {
- "rel": "parent",
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/catalog.json"
- },
- {
- "rel": "collection",
- "href": "https://landsatlook.usgs.gov/data/collection02/landsat-c2l2-sr.json"
- },
- {
- "rel": "self",
- "href": "https://landsatlook.usgs.gov/data/collection02/level-2/standard/etm/2021/092/013/LE07_L2SP_092013_20211007_20211104_02_T2/LE07_L2SP_092013_20211007_20211104_02_T2_SR_stac.json"
- }
- ],
- "collection": "test-collection"
-}
diff --git a/stac_fastapi/sqlalchemy/tests/features/__init__.py b/stac_fastapi/sqlalchemy/tests/features/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/stac_fastapi/sqlalchemy/tests/features/test_custom_models.py b/stac_fastapi/sqlalchemy/tests/features/test_custom_models.py
deleted file mode 100644
index 400c14ec8..000000000
--- a/stac_fastapi/sqlalchemy/tests/features/test_custom_models.py
+++ /dev/null
@@ -1,75 +0,0 @@
-# from typing import Type
-#
-# import sqlalchemy as sa
-# from starlette.testclient import TestClient
-#
-# # TODO: move these
-# from stac_api.models.database import Item
-# from stac_api.models.schemas import Collection
-#
-# from stac_fastapi.api.app import StacApi
-# from stac_fastapi.extensions.core import TransactionExtension
-# from stac_fastapi.postgres.core import CoreCrudClient, Session
-# from stac_fastapi.postgres.transactions import TransactionsClient
-# from stac_fastapi.postgres.config import PostgresSettings
-#
-#
-# from ..conftest import MockStarletteRequest
-#
-#
-# class CustomItem(Item):
-# foo = sa.Column(sa.VARCHAR(10))
-#
-#
-# def create_app(item_model: Type[Item], db_session: Session) -> StacApi:
-# """Create application with a custom sqlalchemy item"""
-# api = StacApi(
-# settings=PostgresSettings(indexed_fields={"datetime", "foo"}),
-# extensions=[
-# TransactionExtension(
-# client=TransactionsClient(item_table=item_model, session=db_session)
-# )
-# ],
-# client=CoreCrudClient(item_table=item_model, session=db_session),
-# )
-# return api
-#
-#
-# def test_custom_item(load_test_data, postgres_transactions, db_session):
-# api = create_app(CustomItem, db_session)
-# transactions = TransactionsClient(item_table=CustomItem, session=db_session)
-#
-# with TestClient(api.app) as test_client:
-# # Ingest a collection
-# coll = Collection.parse_obj(load_test_data("test_collection.json"))
-# transactions.create_collection(coll, request=MockStarletteRequest)
-#
-# # Modify the table to match our custom item
-# # This would typically be done with alembic
-# db_session.writer.cached_engine.execute(
-# "ALTER TABLE data.items ADD COLUMN foo VARCHAR(10)"
-# )
-#
-# # Post an item
-# test_item = load_test_data("test_item.json")
-# test_item["properties"]["foo"] = "hello"
-# resp = test_client.post(
-# f"/collections/{test_item['collection']}/items", json=test_item
-# )
-# assert resp.status_code == 200
-# assert resp.json()["properties"]["foo"] == "hello"
-#
-# # Search for the item
-# body = {"query": {"foo": {"eq": "hello"}}}
-# resp = test_client.post("/search", json=body)
-# assert resp.status_code == 200
-# resp_json = resp.json()
-# assert len(resp_json["features"]) == 1
-# assert resp_json["features"][0]["properties"]["foo"] == "hello"
-#
-# # Cleanup
-# transactions.delete_item(test_item["id"], request=MockStarletteRequest)
-# transactions.delete_collection(coll.id, request=MockStarletteRequest)
-# db_session.writer.cached_engine.execute(
-# "ALTER TABLE data.items DROP COLUMN foo"
-# )
diff --git a/stac_fastapi/sqlalchemy/tests/resources/__init__.py b/stac_fastapi/sqlalchemy/tests/resources/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/stac_fastapi/sqlalchemy/tests/resources/test_collection.py b/stac_fastapi/sqlalchemy/tests/resources/test_collection.py
deleted file mode 100644
index 275b2684f..000000000
--- a/stac_fastapi/sqlalchemy/tests/resources/test_collection.py
+++ /dev/null
@@ -1,118 +0,0 @@
-import pystac
-
-
-def test_create_and_delete_collection(app_client, load_test_data):
- """Test creation and deletion of a collection"""
- test_collection = load_test_data("test_collection.json")
- test_collection["id"] = "test"
-
- resp = app_client.post("/collections", json=test_collection)
- assert resp.status_code == 200
-
- resp = app_client.delete(f"/collections/{test_collection['id']}")
- assert resp.status_code == 200
-
-
-def test_create_collection_conflict(app_client, load_test_data):
- """Test creation of a collection which already exists"""
- # This collection ID is created in the fixture, so this should be a conflict
- test_collection = load_test_data("test_collection.json")
- resp = app_client.post("/collections", json=test_collection)
- assert resp.status_code == 409
-
-
-def test_delete_missing_collection(app_client):
- """Test deletion of a collection which does not exist"""
- resp = app_client.delete("/collections/missing-collection")
- assert resp.status_code == 404
-
-
-def test_update_collection_already_exists(app_client, load_test_data):
- """Test updating a collection which already exists"""
- test_collection = load_test_data("test_collection.json")
- test_collection["keywords"].append("test")
- resp = app_client.put("/collections", json=test_collection)
- assert resp.status_code == 200
-
- resp = app_client.get(f"/collections/{test_collection['id']}")
- assert resp.status_code == 200
- resp_json = resp.json()
- assert "test" in resp_json["keywords"]
-
-
-def test_update_new_collection(app_client, load_test_data):
- """Test updating a collection which does not exist (same as creation)"""
- test_collection = load_test_data("test_collection.json")
- test_collection["id"] = "new-test-collection"
-
- resp = app_client.put("/collections", json=test_collection)
- assert resp.status_code == 404
-
-
-def test_collection_not_found(app_client):
- """Test read a collection which does not exist"""
- resp = app_client.get("/collections/does-not-exist")
- assert resp.status_code == 404
-
-
-def test_returns_valid_collection(app_client, load_test_data):
- """Test validates fetched collection with jsonschema"""
- test_collection = load_test_data("test_collection.json")
- resp = app_client.put("/collections", json=test_collection)
- assert resp.status_code == 200
-
- resp = app_client.get(f"/collections/{test_collection['id']}")
- assert resp.status_code == 200
- resp_json = resp.json()
-
- # Mock root to allow validation
- mock_root = pystac.Catalog(
- id="test", description="test desc", href="https://example.com"
- )
- collection = pystac.Collection.from_dict(
- resp_json, root=mock_root, preserve_dict=False
- )
- collection.validate()
-
-
-def test_get_collection_forwarded_header(app_client, load_test_data):
- test_collection = load_test_data("test_collection.json")
- app_client.put("/collections", json=test_collection)
-
- resp = app_client.get(
- f"/collections/{test_collection['id']}",
- headers={"Forwarded": "proto=https;host=testserver:1234"},
- )
- for link in resp.json()["links"]:
- assert link["href"].startswith("https://testserver:1234/")
-
-
-def test_get_collection_x_forwarded_headers(app_client, load_test_data):
- test_collection = load_test_data("test_collection.json")
- app_client.put("/collections", json=test_collection)
-
- resp = app_client.get(
- f"/collections/{test_collection['id']}",
- headers={
- "X-Forwarded-Port": "1234",
- "X-Forwarded-Proto": "https",
- },
- )
- for link in resp.json()["links"]:
- assert link["href"].startswith("https://testserver:1234/")
-
-
-def test_get_collection_duplicate_forwarded_headers(app_client, load_test_data):
- test_collection = load_test_data("test_collection.json")
- app_client.put("/collections", json=test_collection)
-
- resp = app_client.get(
- f"/collections/{test_collection['id']}",
- headers={
- "Forwarded": "proto=https;host=testserver:1234",
- "X-Forwarded-Port": "4321",
- "X-Forwarded-Proto": "http",
- },
- )
- for link in resp.json()["links"]:
- assert link["href"].startswith("https://testserver:1234/")
diff --git a/stac_fastapi/sqlalchemy/tests/resources/test_conformance.py b/stac_fastapi/sqlalchemy/tests/resources/test_conformance.py
deleted file mode 100644
index cb85c7449..000000000
--- a/stac_fastapi/sqlalchemy/tests/resources/test_conformance.py
+++ /dev/null
@@ -1,68 +0,0 @@
-import urllib.parse
-
-import pytest
-
-
-@pytest.fixture
-def response(app_client):
- return app_client.get("/")
-
-
-@pytest.fixture
-def response_json(response):
- return response.json()
-
-
-def get_link(landing_page, rel_type):
- return next(
- filter(lambda link: link["rel"] == rel_type, landing_page["links"]), None
- )
-
-
-def test_landing_page_health(response):
- """Test landing page"""
- assert response.status_code == 200
- assert response.headers["content-type"] == "application/json"
-
-
-# Parameters for test_landing_page_links test below.
-# Each tuple has the following values (in this order):
-# - Rel type of link to test
-# - Expected MIME/Media Type
-# - Expected relative path
-link_tests = [
- ("root", "application/json", "/"),
- ("conformance", "application/json", "/conformance"),
- ("service-doc", "text/html", "/api.html"),
- ("service-desc", "application/vnd.oai.openapi+json;version=3.0", "/api"),
-]
-
-
-@pytest.mark.parametrize("rel_type,expected_media_type,expected_path", link_tests)
-def test_landing_page_links(
- response_json, app_client, rel_type, expected_media_type, expected_path
-):
- link = get_link(response_json, rel_type)
-
- assert link is not None, f"Missing {rel_type} link in landing page"
- assert link.get("type") == expected_media_type
-
- link_path = urllib.parse.urlsplit(link.get("href")).path
- assert link_path == expected_path
-
- resp = app_client.get(link_path)
- assert resp.status_code == 200
-
-
-# This endpoint currently returns a 404 for empty result sets, but testing for this response
-# code here seems meaningless since it would be the same as if the endpoint did not exist. Once
-# https://github.com/stac-utils/stac-fastapi/pull/227 has been merged we can add this to the
-# parameterized tests above.
-def test_search_link(response_json):
- search_link = get_link(response_json, "search")
-
- assert search_link is not None
- assert search_link.get("type") == "application/geo+json"
-
- search_path = urllib.parse.urlsplit(search_link.get("href")).path
- assert search_path == "/search"
diff --git a/stac_fastapi/sqlalchemy/tests/resources/test_item.py b/stac_fastapi/sqlalchemy/tests/resources/test_item.py
deleted file mode 100644
index 5ecf51952..000000000
--- a/stac_fastapi/sqlalchemy/tests/resources/test_item.py
+++ /dev/null
@@ -1,992 +0,0 @@
-import json
-import os
-import time
-import uuid
-from copy import deepcopy
-from datetime import datetime, timedelta, timezone
-from random import randint
-from urllib.parse import parse_qs, urlparse, urlsplit
-
-import pystac
-from pydantic.datetime_parse import parse_datetime
-from pystac.utils import datetime_to_str
-from shapely.geometry import Polygon
-
-from stac_fastapi.sqlalchemy.core import CoreCrudClient
-from stac_fastapi.types.core import LandingPageMixin
-from stac_fastapi.types.rfc3339 import rfc3339_str_to_datetime
-
-
-def test_create_and_delete_item(app_client, load_test_data):
- """Test creation and deletion of a single item (transactions extension)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- resp = app_client.delete(
- f"/collections/{test_item['collection']}/items/{resp.json()['id']}"
- )
- assert resp.status_code == 200
-
-
-def test_create_item_conflict(app_client, load_test_data):
- """Test creation of an item which already exists (transactions extension)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 409
-
-
-def test_create_item_duplicate(app_client, load_test_data):
- """Test creation of an item id which already exists but in a different collection(transactions extension)"""
-
- # add test_item to test-collection
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # add test_item to test-collection again, resource already exists
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 409
-
- # create "test-collection-2"
- collection_2 = load_test_data("test_collection.json")
- collection_2["id"] = "test-collection-2"
- resp = app_client.post("/collections", json=collection_2)
- assert resp.status_code == 200
-
- # add test_item to test-collection-2, posts successfully
- test_item["collection"] = "test-collection-2"
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
-
-def test_delete_item_duplicate(app_client, load_test_data):
- """Test creation of an item id which already exists but in a different collection(transactions extension)"""
-
- # add test_item to test-collection
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # create "test-collection-2"
- collection_2 = load_test_data("test_collection.json")
- collection_2["id"] = "test-collection-2"
- resp = app_client.post("/collections", json=collection_2)
- assert resp.status_code == 200
-
- # add test_item to test-collection-2
- test_item["collection"] = "test-collection-2"
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # delete test_item from test-collection
- test_item["collection"] = "test-collection"
- resp = app_client.delete(
- f"/collections/{test_item['collection']}/items/{test_item['id']}"
- )
- assert resp.status_code == 200
-
- # test-item in test-collection has already been deleted
- resp = app_client.delete(
- f"/collections/{test_item['collection']}/items/{test_item['id']}"
- )
- assert resp.status_code == 404
-
- # test-item in test-collection-2 still exists, was not deleted
- test_item["collection"] = "test-collection-2"
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 409
-
-
-def test_update_item_duplicate(app_client, load_test_data):
- """Test creation of an item id which already exists but in a different collection(transactions extension)"""
-
- # add test_item to test-collection
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # create "test-collection-2"
- collection_2 = load_test_data("test_collection.json")
- collection_2["id"] = "test-collection-2"
- resp = app_client.post("/collections", json=collection_2)
- assert resp.status_code == 200
-
- # add test_item to test-collection-2
- test_item["collection"] = "test-collection-2"
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # update gsd in test_item, test-collection-2
- test_item["properties"]["gsd"] = 16
- resp = app_client.put(
- f"/collections/{test_item['collection']}/items/{test_item['id']}",
- json=test_item,
- )
- assert resp.status_code == 200
- updated_item = resp.json()
- assert updated_item["properties"]["gsd"] == 16
-
- # update gsd in test_item, test-collection
- test_item["collection"] = "test-collection"
- test_item["properties"]["gsd"] = 17
- resp = app_client.put(
- f"/collections/{test_item['collection']}/items/{test_item['id']}",
- json=test_item,
- )
- assert resp.status_code == 200
- updated_item = resp.json()
- assert updated_item["properties"]["gsd"] == 17
-
- # test_item in test-collection, updated gsd = 17
- resp = app_client.get(
- f"/collections/{test_item['collection']}/items/{test_item['id']}"
- )
- assert resp.status_code == 200
- item = resp.json()
- assert item["properties"]["gsd"] == 17
-
- # test_item in test-collection-2, updated gsd = 16
- test_item["collection"] = "test-collection-2"
- resp = app_client.get(
- f"/collections/{test_item['collection']}/items/{test_item['id']}"
- )
- assert resp.status_code == 200
- item = resp.json()
- assert item["properties"]["gsd"] == 16
-
-
-def test_delete_missing_item(app_client, load_test_data):
- """Test deletion of an item which does not exist (transactions extension)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.delete(f"/collections/{test_item['collection']}/items/hijosh")
- assert resp.status_code == 404
-
-
-def test_create_item_missing_collection(app_client, load_test_data):
- """Test creation of an item without a parent collection (transactions extension)"""
- test_item = load_test_data("test_item.json")
- test_item["collection"] = "stac is cool"
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 424
-
-
-def test_update_item_already_exists(app_client, load_test_data):
- """Test updating an item which already exists (transactions extension)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- assert test_item["properties"]["gsd"] != 16
- test_item["properties"]["gsd"] = 16
- resp = app_client.put(
- f"/collections/{test_item['collection']}/items/{test_item['id']}",
- json=test_item,
- )
- updated_item = resp.json()
- assert updated_item["properties"]["gsd"] == 16
-
-
-def test_update_new_item(app_client, load_test_data):
- """Test updating an item which does not exist (transactions extension)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.put(
- f"/collections/{test_item['collection']}/items/{test_item['id']}",
- json=test_item,
- )
- assert resp.status_code == 404
-
-
-def test_update_item_missing_collection(app_client, load_test_data):
- """Test updating an item without a parent collection (transactions extension)"""
- test_item = load_test_data("test_item.json")
-
- # Create the item
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # Try to update collection of the item
- test_item["collection"] = "stac is cool"
- resp = app_client.put(
- f"/collections/{test_item['collection']}/items/{test_item['id']}",
- json=test_item,
- )
- assert resp.status_code == 404
-
-
-def test_update_item_geometry(app_client, load_test_data):
- test_item = load_test_data("test_item.json")
-
- # Create the item
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # Update the geometry of the item
- test_item["geometry"]["coordinates"] = [[[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]]]
- resp = app_client.put(
- f"/collections/{test_item['collection']}/items/{test_item['id']}",
- json=test_item,
- )
- assert resp.status_code == 200
-
- # Fetch the updated item
- resp = app_client.get(
- f"/collections/{test_item['collection']}/items/{test_item['id']}"
- )
- assert resp.status_code == 200
- assert resp.json()["geometry"]["coordinates"] == [
- [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]]
- ]
-
-
-def test_get_item(app_client, load_test_data):
- """Test read an item by id (core)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- get_item = app_client.get(
- f"/collections/{test_item['collection']}/items/{test_item['id']}"
- )
- assert get_item.status_code == 200
-
-
-def test_returns_valid_item(app_client, load_test_data):
- """Test validates fetched item with jsonschema"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- get_item = app_client.get(
- f"/collections/{test_item['collection']}/items/{test_item['id']}"
- )
- assert get_item.status_code == 200
- item_dict = get_item.json()
- # Mock root to allow validation
- mock_root = pystac.Catalog(
- id="test", description="test desc", href="https://example.com"
- )
- item = pystac.Item.from_dict(item_dict, preserve_dict=False, root=mock_root)
- item.validate()
-
-
-def test_get_item_collection(app_client, load_test_data):
- """Test read an item collection (core)"""
- item_count = randint(1, 4)
- test_item = load_test_data("test_item.json")
-
- for idx in range(item_count):
- _test_item = deepcopy(test_item)
- _test_item["id"] = test_item["id"] + str(idx)
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=_test_item
- )
- assert resp.status_code == 200
-
- resp = app_client.get(f"/collections/{test_item['collection']}/items")
- assert resp.status_code == 200
-
- item_collection = resp.json()
- assert item_collection["context"]["matched"] == len(range(item_count))
-
-
-def test_pagination(app_client, load_test_data):
- """Test item collection pagination (paging extension)"""
- item_count = 10
- test_item = load_test_data("test_item.json")
-
- for idx in range(item_count):
- _test_item = deepcopy(test_item)
- _test_item["id"] = test_item["id"] + str(idx)
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=_test_item
- )
- assert resp.status_code == 200
-
- resp = app_client.get(
- f"/collections/{test_item['collection']}/items", params={"limit": 3}
- )
- assert resp.status_code == 200
- first_page = resp.json()
- assert first_page["context"]["returned"] == 3
-
- url_components = urlsplit(first_page["links"][0]["href"])
- resp = app_client.get(f"{url_components.path}?{url_components.query}")
- assert resp.status_code == 200
- second_page = resp.json()
- assert second_page["context"]["returned"] == 3
-
-
-def test_item_timestamps(app_client, load_test_data):
- """Test created and updated timestamps (common metadata)"""
- test_item = load_test_data("test_item.json")
- start_time = datetime.now(timezone.utc)
- time.sleep(2)
- # Confirm `created` timestamp
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- item = resp.json()
- created_dt = parse_datetime(item["properties"]["created"])
- assert resp.status_code == 200
- assert start_time < created_dt < datetime.now(timezone.utc)
-
- time.sleep(2)
- # Confirm `updated` timestamp
- item["properties"]["proj:epsg"] = 4326
- resp = app_client.put(
- f"/collections/{test_item['collection']}/items/{item['id']}", json=item
- )
- assert resp.status_code == 200
- updated_item = resp.json()
-
- # Created shouldn't change on update
- assert item["properties"]["created"] == updated_item["properties"]["created"]
- assert parse_datetime(updated_item["properties"]["updated"]) > created_dt
-
-
-def test_item_search_by_id_post(app_client, load_test_data):
- """Test POST search by item id (core)"""
- ids = ["test1", "test2", "test3"]
- for id in ids:
- test_item = load_test_data("test_item.json")
- test_item["id"] = id
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- params = {"collections": [test_item["collection"]], "ids": ids}
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == len(ids)
- assert set([feat["id"] for feat in resp_json["features"]]) == set(ids)
-
-
-def test_item_search_spatial_query_post(app_client, load_test_data):
- """Test POST search with spatial query (core)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- params = {
- "collections": [test_item["collection"]],
- "intersects": test_item["geometry"],
- }
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert resp_json["features"][0]["id"] == test_item["id"]
-
-
-def test_item_search_temporal_query_post(app_client, load_test_data):
- """Test POST search with single-tailed spatio-temporal query (core)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- item_date = rfc3339_str_to_datetime(test_item["properties"]["datetime"])
- item_date = item_date + timedelta(seconds=1)
-
- params = {
- "collections": [test_item["collection"]],
- "intersects": test_item["geometry"],
- "datetime": f"../{datetime_to_str(item_date)}",
- }
- resp = app_client.post("/search", json=params)
- resp_json = resp.json()
- assert resp_json["features"][0]["id"] == test_item["id"]
-
-
-def test_item_search_temporal_window_post(app_client, load_test_data):
- """Test POST search with two-tailed spatio-temporal query (core)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- item_date = rfc3339_str_to_datetime(test_item["properties"]["datetime"])
- item_date_before = item_date - timedelta(seconds=1)
- item_date_after = item_date + timedelta(seconds=1)
-
- params = {
- "collections": [test_item["collection"]],
- "intersects": test_item["geometry"],
- "datetime": f"{datetime_to_str(item_date_before)}/{datetime_to_str(item_date_after)}",
- }
- resp = app_client.post("/search", json=params)
- resp_json = resp.json()
- assert resp_json["features"][0]["id"] == test_item["id"]
-
-
-def test_item_search_temporal_open_window(app_client, load_test_data):
- """Test POST search with open spatio-temporal query (core)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- for dt in ["/", "../", "/..", "../.."]:
- resp = app_client.post("/search", json={"datetime": dt})
- assert resp.status_code == 400
-
-
-def test_item_search_sort_post(app_client, load_test_data):
- """Test POST search with sorting (sort extension)"""
- first_item = load_test_data("test_item.json")
- item_date = rfc3339_str_to_datetime(first_item["properties"]["datetime"])
- resp = app_client.post(
- f"/collections/{first_item['collection']}/items", json=first_item
- )
- assert resp.status_code == 200
-
- second_item = load_test_data("test_item.json")
- second_item["id"] = "another-item"
- another_item_date = item_date - timedelta(days=1)
- second_item["properties"]["datetime"] = datetime_to_str(another_item_date)
- resp = app_client.post(
- f"/collections/{second_item['collection']}/items", json=second_item
- )
- assert resp.status_code == 200
-
- params = {
- "collections": [first_item["collection"]],
- "sortby": [{"field": "datetime", "direction": "desc"}],
- }
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert resp_json["features"][0]["id"] == first_item["id"]
- assert resp_json["features"][1]["id"] == second_item["id"]
-
-
-def test_item_search_by_id_get(app_client, load_test_data):
- """Test GET search by item id (core)"""
- ids = ["test1", "test2", "test3"]
- for id in ids:
- test_item = load_test_data("test_item.json")
- test_item["id"] = id
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- params = {"collections": test_item["collection"], "ids": ",".join(ids)}
- resp = app_client.get("/search", params=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == len(ids)
- assert set([feat["id"] for feat in resp_json["features"]]) == set(ids)
-
-
-def test_item_search_bbox_get(app_client, load_test_data):
- """Test GET search with spatial query (core)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- params = {
- "collections": test_item["collection"],
- "bbox": ",".join([str(coord) for coord in test_item["bbox"]]),
- }
- resp = app_client.get("/search", params=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert resp_json["features"][0]["id"] == test_item["id"]
-
-
-def test_item_search_get_without_collections(app_client, load_test_data):
- """Test GET search without specifying collections"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- params = {
- "bbox": ",".join([str(coord) for coord in test_item["bbox"]]),
- }
- resp = app_client.get("/search", params=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert resp_json["features"][0]["id"] == test_item["id"]
-
-
-def test_item_search_temporal_window_get(app_client, load_test_data):
- """Test GET search with spatio-temporal query (core)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- item_date = rfc3339_str_to_datetime(test_item["properties"]["datetime"])
- item_date_before = item_date - timedelta(seconds=1)
- item_date_after = item_date + timedelta(seconds=1)
-
- params = {
- "collections": test_item["collection"],
- "bbox": ",".join([str(coord) for coord in test_item["bbox"]]),
- "datetime": f"{datetime_to_str(item_date_before)}/{datetime_to_str(item_date_after)}",
- }
- resp = app_client.get("/search", params=params)
- resp_json = resp.json()
- assert resp_json["features"][0]["id"] == test_item["id"]
-
-
-def test_item_search_sort_get(app_client, load_test_data):
- """Test GET search with sorting (sort extension)"""
- first_item = load_test_data("test_item.json")
- item_date = rfc3339_str_to_datetime(first_item["properties"]["datetime"])
- resp = app_client.post(
- f"/collections/{first_item['collection']}/items", json=first_item
- )
- assert resp.status_code == 200
-
- second_item = load_test_data("test_item.json")
- second_item["id"] = "another-item"
- another_item_date = item_date - timedelta(days=1)
- second_item["properties"]["datetime"] = datetime_to_str(another_item_date)
- resp = app_client.post(
- f"/collections/{second_item['collection']}/items", json=second_item
- )
- assert resp.status_code == 200
- params = {"collections": [first_item["collection"]], "sortby": "-datetime"}
- resp = app_client.get("/search", params=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert resp_json["features"][0]["id"] == first_item["id"]
- assert resp_json["features"][1]["id"] == second_item["id"]
-
-
-def test_item_search_post_without_collection(app_client, load_test_data):
- """Test POST search without specifying a collection"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- params = {
- "bbox": test_item["bbox"],
- }
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert resp_json["features"][0]["id"] == test_item["id"]
-
-
-def test_item_search_properties_jsonb(app_client, load_test_data):
- """Test POST search with JSONB query (query extension)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # EPSG is a JSONB key
- params = {"query": {"proj:epsg": {"gt": test_item["properties"]["proj:epsg"] + 1}}}
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 0
-
-
-def test_item_search_properties_field(app_client, load_test_data):
- """Test POST search indexed field with query (query extension)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # Orientation is an indexed field
- params = {"query": {"orientation": {"eq": "south"}}}
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 200
- resp_json = resp.json()
- assert len(resp_json["features"]) == 0
-
-
-def test_item_search_get_query_extension(app_client, load_test_data):
- """Test GET search with JSONB query (query extension)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- # EPSG is a JSONB key
- params = {
- "collections": [test_item["collection"]],
- "query": json.dumps(
- {"proj:epsg": {"gt": test_item["properties"]["proj:epsg"] + 1}}
- ),
- }
- resp = app_client.get("/search", params=params)
- assert resp.json()["context"]["returned"] == 0
-
- params["query"] = json.dumps(
- {"proj:epsg": {"eq": test_item["properties"]["proj:epsg"]}}
- )
- resp = app_client.get("/search", params=params)
- resp_json = resp.json()
- assert resp_json["context"]["returned"] == 1
- assert (
- resp_json["features"][0]["properties"]["proj:epsg"]
- == test_item["properties"]["proj:epsg"]
- )
-
-
-def test_get_missing_item_collection(app_client):
- """Test reading a collection which does not exist"""
- resp = app_client.get("/collections/invalid-collection/items")
- assert resp.status_code == 404
-
-
-def test_pagination_item_collection(app_client, load_test_data):
- """Test item collection pagination links (paging extension)"""
- test_item = load_test_data("test_item.json")
- ids = []
-
- # Ingest 5 items
- for idx in range(5):
- uid = str(uuid.uuid4())
- test_item["id"] = uid
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
- ids.append(uid)
-
- # Paginate through all 5 items with a limit of 1 (expecting 5 requests)
- page = app_client.get(
- f"/collections/{test_item['collection']}/items", params={"limit": 1}
- )
- idx = 0
- item_ids = []
- while True:
- idx += 1
- page_data = page.json()
- item_ids.append(page_data["features"][0]["id"])
- next_link = list(filter(lambda link: link["rel"] == "next", page_data["links"]))
- if not next_link:
- break
- query_params = parse_qs(urlparse(next_link[0]["href"]).query)
- page = app_client.get(
- f"/collections/{test_item['collection']}/items",
- params=query_params,
- )
-
- # Our limit is 1 so we expect len(ids) number of requests before we run out of pages
- assert idx == len(ids)
-
- # Confirm we have paginated through all items
- assert not set(item_ids) - set(ids)
-
-
-def test_pagination_post(app_client, load_test_data):
- """Test POST pagination (paging extension)"""
- test_item = load_test_data("test_item.json")
- ids = []
-
- # Ingest 5 items
- for idx in range(5):
- uid = str(uuid.uuid4())
- test_item["id"] = uid
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
- ids.append(uid)
-
- # Paginate through all 5 items with a limit of 1 (expecting 5 requests)
- request_body = {"ids": ids, "limit": 1}
- page = app_client.post("/search", json=request_body)
- idx = 0
- item_ids = []
- while True:
- idx += 1
- page_data = page.json()
- item_ids.append(page_data["features"][0]["id"])
- next_link = list(filter(lambda link: link["rel"] == "next", page_data["links"]))
- if not next_link:
- break
- # Merge request bodies
- request_body.update(next_link[0]["body"])
- page = app_client.post("/search", json=request_body)
-
- # Our limit is 1 so we expect len(ids) number of requests before we run out of pages
- assert idx == len(ids)
-
- # Confirm we have paginated through all items
- assert not set(item_ids) - set(ids)
-
-
-def test_pagination_token_idempotent(app_client, load_test_data):
- """Test that pagination tokens are idempotent (paging extension)"""
- test_item = load_test_data("test_item.json")
- ids = []
-
- # Ingest 5 items
- for idx in range(5):
- uid = str(uuid.uuid4())
- test_item["id"] = uid
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
- ids.append(uid)
-
- page = app_client.get("/search", params={"ids": ",".join(ids), "limit": 3})
- page_data = page.json()
- next_link = list(filter(lambda link: link["rel"] == "next", page_data["links"]))
-
- # Confirm token is idempotent
- resp1 = app_client.get(
- "/search", params=parse_qs(urlparse(next_link[0]["href"]).query)
- )
- resp2 = app_client.get(
- "/search", params=parse_qs(urlparse(next_link[0]["href"]).query)
- )
- resp1_data = resp1.json()
- resp2_data = resp2.json()
-
- # Two different requests with the same pagination token should return the same items
- assert [item["id"] for item in resp1_data["features"]] == [
- item["id"] for item in resp2_data["features"]
- ]
-
-
-def test_field_extension_get(app_client, load_test_data):
- """Test GET search with included fields (fields extension)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- params = {"fields": "+properties.proj:epsg,+properties.gsd"}
- resp = app_client.get("/search", params=params)
- feat_properties = resp.json()["features"][0]["properties"]
- assert not set(feat_properties) - {"proj:epsg", "gsd", "datetime"}
-
-
-def test_field_extension_post(app_client, load_test_data):
- """Test POST search with included and excluded fields (fields extension)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- body = {
- "fields": {
- "exclude": ["assets.B1"],
- "include": ["properties.eo:cloud_cover", "properties.orientation"],
- }
- }
-
- resp = app_client.post("/search", json=body)
- resp_json = resp.json()
- assert "B1" not in resp_json["features"][0]["assets"].keys()
- assert not set(resp_json["features"][0]["properties"]) - {
- "orientation",
- "eo:cloud_cover",
- "datetime",
- }
-
-
-def test_field_extension_exclude_and_include(app_client, load_test_data):
- """Test POST search including/excluding same field (fields extension)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- body = {
- "fields": {
- "exclude": ["properties.eo:cloud_cover"],
- "include": ["properties.eo:cloud_cover"],
- }
- }
-
- resp = app_client.post("/search", json=body)
- resp_json = resp.json()
- assert "eo:cloud_cover" not in resp_json["features"][0]["properties"]
-
-
-def test_field_extension_exclude_default_includes(app_client, load_test_data):
- """Test POST search excluding a forbidden field (fields extension)"""
- test_item = load_test_data("test_item.json")
- resp = app_client.post(
- f"/collections/{test_item['collection']}/items", json=test_item
- )
- assert resp.status_code == 200
-
- body = {"fields": {"exclude": ["geometry"]}}
-
- resp = app_client.post("/search", json=body)
- resp_json = resp.json()
- assert "geometry" not in resp_json["features"][0]
-
-
-def test_search_intersects_and_bbox(app_client):
- """Test POST search intersects and bbox are mutually exclusive (core)"""
- bbox = [-118, 34, -117, 35]
- geoj = Polygon.from_bounds(*bbox).__geo_interface__
- params = {"bbox": bbox, "intersects": geoj}
- resp = app_client.post("/search", json=params)
- assert resp.status_code == 400
-
-
-def test_get_missing_item(app_client, load_test_data):
- """Test read item which does not exist (transactions extension)"""
- test_coll = load_test_data("test_collection.json")
- resp = app_client.get(f"/collections/{test_coll['id']}/items/invalid-item")
- assert resp.status_code == 404
-
-
-def test_search_invalid_query_field(app_client):
- body = {"query": {"gsd": {"lt": 100}, "invalid-field": {"eq": 50}}}
- resp = app_client.post("/search", json=body)
- assert resp.status_code == 400
-
-
-def test_search_bbox_errors(app_client):
- body = {"query": {"bbox": [0]}}
- resp = app_client.post("/search", json=body)
- assert resp.status_code == 400
-
- body = {"query": {"bbox": [100.0, 0.0, 0.0, 105.0, 1.0, 1.0]}}
- resp = app_client.post("/search", json=body)
- assert resp.status_code == 400
-
- params = {"bbox": "100.0,0.0,0.0,105.0"}
- resp = app_client.get("/search", params=params)
- assert resp.status_code == 400
-
-
-def test_conformance_classes_configurable():
- """Test conformance class configurability"""
- landing = LandingPageMixin()
- landing_page = landing._landing_page(
- base_url="http://test/test",
- conformance_classes=["this is a test"],
- extension_schemas=[],
- )
- assert landing_page["conformsTo"][0] == "this is a test"
-
- # Update environment to avoid key error on client instantiation
- os.environ["READER_CONN_STRING"] = "testing"
- os.environ["WRITER_CONN_STRING"] = "testing"
- client = CoreCrudClient(base_conformance_classes=["this is a test"])
- assert client.conformance_classes()[0] == "this is a test"
-
-
-def test_search_datetime_validation_errors(app_client):
- bad_datetimes = [
- "37-01-01T12:00:27.87Z",
- "1985-13-12T23:20:50.52Z",
- "1985-12-32T23:20:50.52Z",
- "1985-12-01T25:20:50.52Z",
- "1985-12-01T00:60:50.52Z",
- "1985-12-01T00:06:61.52Z",
- "1990-12-31T23:59:61Z",
- "1986-04-12T23:20:50.52Z/1985-04-12T23:20:50.52Z",
- ]
- for dt in bad_datetimes:
- body = {"query": {"datetime": dt}}
- resp = app_client.post("/search", json=body)
- assert resp.status_code == 400
-
- resp = app_client.get("/search?datetime={}".format(dt))
- assert resp.status_code == 400
-
-
-def test_get_item_forwarded_header(app_client, load_test_data):
- test_item = load_test_data("test_item.json")
- app_client.post(f"/collections/{test_item['collection']}/items", json=test_item)
- get_item = app_client.get(
- f"/collections/{test_item['collection']}/items/{test_item['id']}",
- headers={"Forwarded": "proto=https;host=testserver:1234"},
- )
- for link in get_item.json()["links"]:
- assert link["href"].startswith("https://testserver:1234/")
-
-
-def test_get_item_x_forwarded_headers(app_client, load_test_data):
- test_item = load_test_data("test_item.json")
- app_client.post(f"/collections/{test_item['collection']}/items", json=test_item)
- get_item = app_client.get(
- f"/collections/{test_item['collection']}/items/{test_item['id']}",
- headers={
- "X-Forwarded-Port": "1234",
- "X-Forwarded-Proto": "https",
- },
- )
- for link in get_item.json()["links"]:
- assert link["href"].startswith("https://testserver:1234/")
-
-
-def test_get_item_duplicate_forwarded_headers(app_client, load_test_data):
- test_item = load_test_data("test_item.json")
- app_client.post(f"/collections/{test_item['collection']}/items", json=test_item)
- get_item = app_client.get(
- f"/collections/{test_item['collection']}/items/{test_item['id']}",
- headers={
- "Forwarded": "proto=https;host=testserver:1234",
- "X-Forwarded-Port": "4321",
- "X-Forwarded-Proto": "http",
- },
- )
- for link in get_item.json()["links"]:
- assert link["href"].startswith("https://testserver:1234/")
diff --git a/stac_fastapi/sqlalchemy/tests/resources/test_mgmt.py b/stac_fastapi/sqlalchemy/tests/resources/test_mgmt.py
deleted file mode 100644
index 0a11e38e8..000000000
--- a/stac_fastapi/sqlalchemy/tests/resources/test_mgmt.py
+++ /dev/null
@@ -1,9 +0,0 @@
-def test_ping_no_param(app_client):
- """
- Test ping endpoint with a mocked client.
- Args:
- app_client (TestClient): mocked client fixture
- """
- res = app_client.get("/_mgmt/ping")
- assert res.status_code == 200
- assert res.json() == {"message": "PONG"}
diff --git a/stac_fastapi/testdata/joplin/collection.json b/stac_fastapi/testdata/joplin/collection.json
deleted file mode 100644
index 992e64b9a..000000000
--- a/stac_fastapi/testdata/joplin/collection.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "id": "joplin",
- "description": "This imagery was acquired by the NOAA Remote Sensing Division to support NOAA national security and emergency response requirements. In addition, it will be used for ongoing research efforts for testing and developing standards for airborne digital imagery. Individual images have been combined into a larger mosaic and tiled for distribution. The approximate ground sample distance (GSD) for each pixel is 35 cm (1.14 feet).",
- "stac_version": "1.0.0",
- "license": "public-domain",
- "links": [
- {
- "rel": "license",
- "href": "https://creativecommons.org/licenses/publicdomain/",
- "title": "public domain"
- }
- ],
- "type": "Collection",
- "extent": {
- "spatial": {
- "bbox": [
- [
- -94.6911621,
- 37.0332547,
- -94.402771,
- 37.1077651
- ]
- ]
- },
- "temporal": {
- "interval": [
- [
- "2000-02-01T00:00:00Z",
- "2000-02-12T00:00:00Z"
- ]
- ]
- }
- }
-}
diff --git a/stac_fastapi/testdata/joplin/feature.geojson b/stac_fastapi/testdata/joplin/feature.geojson
deleted file mode 100644
index 47db3190d..000000000
--- a/stac_fastapi/testdata/joplin/feature.geojson
+++ /dev/null
@@ -1,59 +0,0 @@
-{
- "id": "f2cca2a3-288b-4518-8a3e-a4492bb60b08",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6884155,
- 37.0595608
- ],
- [
- -94.6884155,
- 37.0332547
- ],
- [
- -94.6554565,
- 37.0332547
- ],
- [
- -94.6554565,
- 37.0595608
- ],
- [
- -94.6884155,
- 37.0595608
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C350000e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6884155,
- 37.0332547,
- -94.6554565,
- 37.0595608
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
-}
\ No newline at end of file
diff --git a/stac_fastapi/testdata/joplin/index.geojson b/stac_fastapi/testdata/joplin/index.geojson
deleted file mode 100644
index 1bc8dde5c..000000000
--- a/stac_fastapi/testdata/joplin/index.geojson
+++ /dev/null
@@ -1,1775 +0,0 @@
-{
- "type": "FeatureCollection",
- "features": [
- {
- "id": "f2cca2a3-288b-4518-8a3e-a4492bb60b08",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6884155,
- 37.0595608
- ],
- [
- -94.6884155,
- 37.0332547
- ],
- [
- -94.6554565,
- 37.0332547
- ],
- [
- -94.6554565,
- 37.0595608
- ],
- [
- -94.6884155,
- 37.0595608
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C350000e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6884155,
- 37.0332547,
- -94.6554565,
- 37.0595608
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "a7e125ba-565d-4aa2-bbf3-c57a9087c2e3",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6884155,
- 37.0814756
- ],
- [
- -94.6884155,
- 37.0551771
- ],
- [
- -94.6582031,
- 37.0551771
- ],
- [
- -94.6582031,
- 37.0814756
- ],
- [
- -94.6884155,
- 37.0814756
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C350000e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6884155,
- 37.0551771,
- -94.6582031,
- 37.0814756
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "f7f164c9-cfdf-436d-a3f0-69864c38ba2a",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6911621,
- 37.1033841
- ],
- [
- -94.6911621,
- 37.0770932
- ],
- [
- -94.6582031,
- 37.0770932
- ],
- [
- -94.6582031,
- 37.1033841
- ],
- [
- -94.6911621,
- 37.1033841
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C350000e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6911621,
- 37.0770932,
- -94.6582031,
- 37.1033841
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "ea0fddf4-56f9-4a16-8a0b-f6b0b123b7cf",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6609497,
- 37.0595608
- ],
- [
- -94.6609497,
- 37.0332547
- ],
- [
- -94.6279907,
- 37.0332547
- ],
- [
- -94.6279907,
- 37.0595608
- ],
- [
- -94.6609497,
- 37.0595608
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C352500e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6609497,
- 37.0332547,
- -94.6279907,
- 37.0595608
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "c811e716-ab07-4d80-ac95-6670f8713bc4",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6609497,
- 37.0814756
- ],
- [
- -94.6609497,
- 37.0551771
- ],
- [
- -94.6279907,
- 37.0551771
- ],
- [
- -94.6279907,
- 37.0814756
- ],
- [
- -94.6609497,
- 37.0814756
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C352500e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6609497,
- 37.0551771,
- -94.6279907,
- 37.0814756
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "d4eccfa2-7d77-4624-9e2a-3f59102285bb",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6609497,
- 37.1033841
- ],
- [
- -94.6609497,
- 37.0770932
- ],
- [
- -94.6279907,
- 37.0770932
- ],
- [
- -94.6279907,
- 37.1033841
- ],
- [
- -94.6609497,
- 37.1033841
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C352500e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6609497,
- 37.0770932,
- -94.6279907,
- 37.1033841
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "fe916452-ba6f-4631-9154-c249924a122d",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6334839,
- 37.0595608
- ],
- [
- -94.6334839,
- 37.0332547
- ],
- [
- -94.6005249,
- 37.0332547
- ],
- [
- -94.6005249,
- 37.0595608
- ],
- [
- -94.6334839,
- 37.0595608
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C355000e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6334839,
- 37.0332547,
- -94.6005249,
- 37.0595608
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "85f923a5-a81f-4acd-bc7f-96c7c915f357",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6334839,
- 37.0814756
- ],
- [
- -94.6334839,
- 37.0551771
- ],
- [
- -94.6005249,
- 37.0551771
- ],
- [
- -94.6005249,
- 37.0814756
- ],
- [
- -94.6334839,
- 37.0814756
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C355000e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6334839,
- 37.0551771,
- -94.6005249,
- 37.0814756
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "29c53e17-d7d1-4394-a80f-36763c8f42dc",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6334839,
- 37.1055746
- ],
- [
- -94.6334839,
- 37.0792845
- ],
- [
- -94.6005249,
- 37.0792845
- ],
- [
- -94.6005249,
- 37.1055746
- ],
- [
- -94.6334839,
- 37.1055746
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C355000e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6334839,
- 37.0792845,
- -94.6005249,
- 37.1055746
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "e0a02e4e-aa0c-412e-8f63-6f5344f829df",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6060181,
- 37.0595608
- ],
- [
- -94.6060181,
- 37.0332547
- ],
- [
- -94.5730591,
- 37.0332547
- ],
- [
- -94.5730591,
- 37.0595608
- ],
- [
- -94.6060181,
- 37.0595608
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C357500e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6060181,
- 37.0332547,
- -94.5730591,
- 37.0595608
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "047ab5f0-dce1-4166-a00d-425a3dbefe02",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6060181,
- 37.0814756
- ],
- [
- -94.6060181,
- 37.057369
- ],
- [
- -94.5730591,
- 37.057369
- ],
- [
- -94.5730591,
- 37.0814756
- ],
- [
- -94.6060181,
- 37.0814756
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C357500e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6060181,
- 37.057369,
- -94.5730591,
- 37.0814756
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "57f88dd2-e4e0-48e6-a2b6-7282d4ab8ea4",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.6060181,
- 37.1055746
- ],
- [
- -94.6060181,
- 37.0792845
- ],
- [
- -94.5730591,
- 37.0792845
- ],
- [
- -94.5730591,
- 37.1055746
- ],
- [
- -94.6060181,
- 37.1055746
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C357500e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.6060181,
- 37.0792845,
- -94.5730591,
- 37.1055746
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "68f2c2b2-4bce-4c40-9a0d-782c1be1f4f2",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.5758057,
- 37.0595608
- ],
- [
- -94.5758057,
- 37.0332547
- ],
- [
- -94.5428467,
- 37.0332547
- ],
- [
- -94.5428467,
- 37.0595608
- ],
- [
- -94.5758057,
- 37.0595608
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C360000e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.5758057,
- 37.0332547,
- -94.5428467,
- 37.0595608
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "d8461d8c-3d2b-4e4e-a931-7ae61ca06dbf",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.5758057,
- 37.0836668
- ],
- [
- -94.5758057,
- 37.057369
- ],
- [
- -94.5455933,
- 37.057369
- ],
- [
- -94.5455933,
- 37.0836668
- ],
- [
- -94.5758057,
- 37.0836668
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C360000e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.5758057,
- 37.057369,
- -94.5455933,
- 37.0836668
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "aeedef30-cbdd-4364-8781-dbb42d148c99",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.5785522,
- 37.1055746
- ],
- [
- -94.5785522,
- 37.0792845
- ],
- [
- -94.5455933,
- 37.0792845
- ],
- [
- -94.5455933,
- 37.1055746
- ],
- [
- -94.5785522,
- 37.1055746
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C360000e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.5785522,
- 37.0792845,
- -94.5455933,
- 37.1055746
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "9ef4279f-386c-40c7-ad71-8de5d9543aa4",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.5483398,
- 37.0595608
- ],
- [
- -94.5483398,
- 37.0354472
- ],
- [
- -94.5153809,
- 37.0354472
- ],
- [
- -94.5153809,
- 37.0595608
- ],
- [
- -94.5483398,
- 37.0595608
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C362500e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.5483398,
- 37.0354472,
- -94.5153809,
- 37.0595608
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "70cc6c05-9fe0-436a-a264-a52515f3f242",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.5483398,
- 37.0836668
- ],
- [
- -94.5483398,
- 37.057369
- ],
- [
- -94.5153809,
- 37.057369
- ],
- [
- -94.5153809,
- 37.0836668
- ],
- [
- -94.5483398,
- 37.0836668
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C362500e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.5483398,
- 37.057369,
- -94.5153809,
- 37.0836668
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "d191a6fd-7881-4421-805c-e246371e5cc4",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.5483398,
- 37.1055746
- ],
- [
- -94.5483398,
- 37.0792845
- ],
- [
- -94.5181274,
- 37.0792845
- ],
- [
- -94.5181274,
- 37.1055746
- ],
- [
- -94.5483398,
- 37.1055746
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C362500e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.5483398,
- 37.0792845,
- -94.5181274,
- 37.1055746
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "d144adde-df4a-45e8-bed9-f085f91486a2",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.520874,
- 37.0617526
- ],
- [
- -94.520874,
- 37.0354472
- ],
- [
- -94.487915,
- 37.0354472
- ],
- [
- -94.487915,
- 37.0617526
- ],
- [
- -94.520874,
- 37.0617526
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C365000e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.520874,
- 37.0354472,
- -94.487915,
- 37.0617526
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "a4c32abd-9791-422b-87ab-b0f3fa36f053",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.520874,
- 37.0836668
- ],
- [
- -94.520874,
- 37.057369
- ],
- [
- -94.487915,
- 37.057369
- ],
- [
- -94.487915,
- 37.0836668
- ],
- [
- -94.520874,
- 37.0836668
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C365000e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.520874,
- 37.057369,
- -94.487915,
- 37.0836668
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "4610c58e-39f4-4d9d-94ba-ceddbf9ac570",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.520874,
- 37.1055746
- ],
- [
- -94.520874,
- 37.0792845
- ],
- [
- -94.487915,
- 37.0792845
- ],
- [
- -94.487915,
- 37.1055746
- ],
- [
- -94.520874,
- 37.1055746
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C365000e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.520874,
- 37.0792845,
- -94.487915,
- 37.1055746
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "145fa700-16d4-4d34-98e0-7540d5c0885f",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.4934082,
- 37.0617526
- ],
- [
- -94.4934082,
- 37.0354472
- ],
- [
- -94.4604492,
- 37.0354472
- ],
- [
- -94.4604492,
- 37.0617526
- ],
- [
- -94.4934082,
- 37.0617526
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C367500e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.4934082,
- 37.0354472,
- -94.4604492,
- 37.0617526
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "a89dc7b8-a580-435b-8176-d8e4386d620c",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.4934082,
- 37.0836668
- ],
- [
- -94.4934082,
- 37.057369
- ],
- [
- -94.4604492,
- 37.057369
- ],
- [
- -94.4604492,
- 37.0836668
- ],
- [
- -94.4934082,
- 37.0836668
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C367500e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.4934082,
- 37.057369,
- -94.4604492,
- 37.0836668
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "386dfa13-c2b4-4ce6-8e6f-fcac73f4e64e",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.4934082,
- 37.1055746
- ],
- [
- -94.4934082,
- 37.0792845
- ],
- [
- -94.4604492,
- 37.0792845
- ],
- [
- -94.4604492,
- 37.1055746
- ],
- [
- -94.4934082,
- 37.1055746
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C367500e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.4934082,
- 37.0792845,
- -94.4604492,
- 37.1055746
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "4d8a8e40-d089-4ca7-92c8-27d810ee07bf",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.4631958,
- 37.0617526
- ],
- [
- -94.4631958,
- 37.0354472
- ],
- [
- -94.4329834,
- 37.0354472
- ],
- [
- -94.4329834,
- 37.0617526
- ],
- [
- -94.4631958,
- 37.0617526
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C370000e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.4631958,
- 37.0354472,
- -94.4329834,
- 37.0617526
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "f734401c-2df0-4694-a353-cdd3ea760cdc",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.4631958,
- 37.0836668
- ],
- [
- -94.4631958,
- 37.057369
- ],
- [
- -94.4329834,
- 37.057369
- ],
- [
- -94.4329834,
- 37.0836668
- ],
- [
- -94.4631958,
- 37.0836668
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C370000e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.4631958,
- 37.057369,
- -94.4329834,
- 37.0836668
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "da6ef938-c58f-4bab-9d4e-89f6ae667da2",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.4659424,
- 37.1077651
- ],
- [
- -94.4659424,
- 37.0814756
- ],
- [
- -94.4329834,
- 37.0814756
- ],
- [
- -94.4329834,
- 37.1077651
- ],
- [
- -94.4659424,
- 37.1077651
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C370000e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.4659424,
- 37.0814756,
- -94.4329834,
- 37.1077651
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "ad420ced-b005-472b-a6df-3838c2b74504",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.43573,
- 37.0617526
- ],
- [
- -94.43573,
- 37.0354472
- ],
- [
- -94.402771,
- 37.0354472
- ],
- [
- -94.402771,
- 37.0617526
- ],
- [
- -94.43573,
- 37.0617526
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C372500e4102500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.43573,
- 37.0354472,
- -94.402771,
- 37.0617526
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "f490b7af-0019-45e2-854b-3854d07fd063",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.43573,
- 37.0836668
- ],
- [
- -94.43573,
- 37.0595608
- ],
- [
- -94.402771,
- 37.0595608
- ],
- [
- -94.402771,
- 37.0836668
- ],
- [
- -94.43573,
- 37.0836668
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C372500e4105000n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.43573,
- 37.0595608,
- -94.402771,
- 37.0836668
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- },
- {
- "id": "b853f353-4b72-44d5-aa44-c07dfd307138",
- "type": "Feature",
- "collection": "joplin",
- "links": [],
- "geometry": {
- "type": "Polygon",
- "coordinates": [
- [
- [
- -94.43573,
- 37.1077651
- ],
- [
- -94.43573,
- 37.0814756
- ],
- [
- -94.4055176,
- 37.0814756
- ],
- [
- -94.4055176,
- 37.1077651
- ],
- [
- -94.43573,
- 37.1077651
- ]
- ]
- ]
- },
- "properties": {
- "proj:epsg": 3857,
- "orientation": "nadir",
- "height": 2500,
- "width": 2500,
- "datetime": "2000-02-02T00:00:00Z",
- "gsd": 0.5971642834779395
- },
- "assets": {
- "COG": {
- "type": "image/tiff; application=geotiff; profile=cloud-optimized",
- "href": "https://arturo-stac-api-test-data.s3.amazonaws.com/joplin/images/may24C372500e4107500n.tif",
- "title": "NOAA STORM COG"
- }
- },
- "bbox": [
- -94.43573,
- 37.0814756,
- -94.4055176,
- 37.1077651
- ],
- "stac_extensions": [
- "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
- "https://stac-extensions.github.io/projection/v1.0.0/schema.json"
- ],
- "stac_version": "1.0.0"
- }
- ]
-}
\ No newline at end of file