From 04d22a5ebad49c520106e19bb54110041256da9e Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Wed, 10 Aug 2022 13:54:02 -0600
Subject: [PATCH 01/21] remove other modules
---
stac_fastapi/api/README.md | 0
stac_fastapi/api/setup.cfg | 2 -
stac_fastapi/api/setup.py | 52 -
stac_fastapi/api/stac_fastapi/api/__init__.py | 1 -
stac_fastapi/api/stac_fastapi/api/app.py | 416 ----
stac_fastapi/api/stac_fastapi/api/config.py | 24 -
stac_fastapi/api/stac_fastapi/api/errors.py | 95 -
.../api/stac_fastapi/api/middleware.py | 134 --
stac_fastapi/api/stac_fastapi/api/models.py | 179 --
stac_fastapi/api/stac_fastapi/api/openapi.py | 68 -
stac_fastapi/api/stac_fastapi/api/routes.py | 145 --
stac_fastapi/api/stac_fastapi/api/version.py | 2 -
stac_fastapi/api/tests/test_api.py | 131 --
stac_fastapi/api/tests/test_middleware.py | 159 --
stac_fastapi/extensions/README.md | 0
stac_fastapi/extensions/setup.cfg | 2 -
stac_fastapi/extensions/setup.py | 51 -
.../stac_fastapi/extensions/__init__.py | 1 -
.../stac_fastapi/extensions/core/__init__.py | 19 -
.../stac_fastapi/extensions/core/context.py | 36 -
.../extensions/core/fields/__init__.py | 6 -
.../extensions/core/fields/fields.py | 59 -
.../extensions/core/fields/request.py | 72 -
.../extensions/core/filter/__init__.py | 6 -
.../extensions/core/filter/filter.py | 123 --
.../extensions/core/filter/request.py | 40 -
.../extensions/core/pagination/__init__.py | 6 -
.../extensions/core/pagination/pagination.py | 37 -
.../core/pagination/token_pagination.py | 37 -
.../extensions/core/query/__init__.py | 5 -
.../extensions/core/query/query.py | 39 -
.../extensions/core/query/request.py | 21 -
.../extensions/core/sort/__init__.py | 5 -
.../extensions/core/sort/request.py | 23 -
.../stac_fastapi/extensions/core/sort/sort.py | 39 -
.../extensions/core/transaction.py | 184 --
.../extensions/third_party/__init__.py | 4 -
.../third_party/bulk_transactions.py | 131 --
.../stac_fastapi/extensions/version.py | 2 -
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 | 91 -
.../pgstac/stac_fastapi/pgstac/config.py | 55 -
.../pgstac/stac_fastapi/pgstac/core.py | 411 ----
stac_fastapi/pgstac/stac_fastapi/pgstac/db.py | 115 --
.../pgstac/extensions/__init__.py | 5 -
.../stac_fastapi/pgstac/extensions/query.py | 48 -
.../stac_fastapi/pgstac/models/__init__.py | 1 -
.../stac_fastapi/pgstac/models/links.py | 229 ---
.../stac_fastapi/pgstac/transactions.py | 131 --
.../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 | 393 ----
stac_fastapi/pgstac/tests/clients/__init__.py | 0
.../pgstac/tests/clients/test_postgres.py | 172 --
stac_fastapi/pgstac/tests/conftest.py | 234 ---
.../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/types/README.md | 0
stac_fastapi/types/setup.cfg | 2 -
stac_fastapi/types/setup.py | 52 -
.../types/stac_fastapi/types/__init__.py | 1 -
.../types/stac_fastapi/types/config.py | 54 -
.../types/stac_fastapi/types/conformance.py | 30 -
stac_fastapi/types/stac_fastapi/types/core.py | 755 -------
.../types/stac_fastapi/types/errors.py | 41 -
.../types/stac_fastapi/types/extension.py | 37 -
.../types/stac_fastapi/types/links.py | 110 -
.../types/stac_fastapi/types/requests.py | 14 -
.../types/stac_fastapi/types/rfc3339.py | 86 -
.../types/stac_fastapi/types/search.py | 201 --
stac_fastapi/types/stac_fastapi/types/stac.py | 89 -
.../types/stac_fastapi/types/version.py | 2 -
stac_fastapi/types/tests/test_rfc3339.py | 105 -
91 files changed, 11636 deletions(-)
delete mode 100644 stac_fastapi/api/README.md
delete mode 100644 stac_fastapi/api/setup.cfg
delete mode 100644 stac_fastapi/api/setup.py
delete mode 100644 stac_fastapi/api/stac_fastapi/api/__init__.py
delete mode 100644 stac_fastapi/api/stac_fastapi/api/app.py
delete mode 100644 stac_fastapi/api/stac_fastapi/api/config.py
delete mode 100644 stac_fastapi/api/stac_fastapi/api/errors.py
delete mode 100644 stac_fastapi/api/stac_fastapi/api/middleware.py
delete mode 100644 stac_fastapi/api/stac_fastapi/api/models.py
delete mode 100644 stac_fastapi/api/stac_fastapi/api/openapi.py
delete mode 100644 stac_fastapi/api/stac_fastapi/api/routes.py
delete mode 100644 stac_fastapi/api/stac_fastapi/api/version.py
delete mode 100644 stac_fastapi/api/tests/test_api.py
delete mode 100644 stac_fastapi/api/tests/test_middleware.py
delete mode 100644 stac_fastapi/extensions/README.md
delete mode 100644 stac_fastapi/extensions/setup.cfg
delete mode 100644 stac_fastapi/extensions/setup.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/__init__.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/context.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/fields/__init__.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/fields/request.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/filter/__init__.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/filter/request.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/__init__.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/pagination.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/token_pagination.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/query/__init__.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/query/request.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/sort/__init__.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/sort/request.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/third_party/__init__.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/third_party/bulk_transactions.py
delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/version.py
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/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/types/README.md
delete mode 100644 stac_fastapi/types/setup.cfg
delete mode 100644 stac_fastapi/types/setup.py
delete mode 100644 stac_fastapi/types/stac_fastapi/types/__init__.py
delete mode 100644 stac_fastapi/types/stac_fastapi/types/config.py
delete mode 100644 stac_fastapi/types/stac_fastapi/types/conformance.py
delete mode 100644 stac_fastapi/types/stac_fastapi/types/core.py
delete mode 100644 stac_fastapi/types/stac_fastapi/types/errors.py
delete mode 100644 stac_fastapi/types/stac_fastapi/types/extension.py
delete mode 100644 stac_fastapi/types/stac_fastapi/types/links.py
delete mode 100644 stac_fastapi/types/stac_fastapi/types/requests.py
delete mode 100644 stac_fastapi/types/stac_fastapi/types/rfc3339.py
delete mode 100644 stac_fastapi/types/stac_fastapi/types/search.py
delete mode 100644 stac_fastapi/types/stac_fastapi/types/stac.py
delete mode 100644 stac_fastapi/types/stac_fastapi/types/version.py
delete mode 100644 stac_fastapi/types/tests/test_rfc3339.py
diff --git a/stac_fastapi/api/README.md b/stac_fastapi/api/README.md
deleted file mode 100644
index e69de29..0000000
diff --git a/stac_fastapi/api/setup.cfg b/stac_fastapi/api/setup.cfg
deleted file mode 100644
index d6e3762..0000000
--- a/stac_fastapi/api/setup.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[metadata]
-version = attr: stac_fastapi.api.version.__version__
diff --git a/stac_fastapi/api/setup.py b/stac_fastapi/api/setup.py
deleted file mode 100644
index 2b70d42..0000000
--- a/stac_fastapi/api/setup.py
+++ /dev/null
@@ -1,52 +0,0 @@
-"""stac_fastapi: api 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.*",
- "brotli_asgi",
- "stac-fastapi.types",
-]
-
-extra_reqs = {
- "dev": [
- "pytest",
- "pytest-cov",
- "pytest-asyncio",
- "pre-commit",
- "requests",
- "pystac[validation]==1.*",
- ],
- "docs": ["mkdocs", "mkdocs-material", "pdocs"],
-}
-
-
-setup(
- name="stac-fastapi.api",
- 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,
-)
diff --git a/stac_fastapi/api/stac_fastapi/api/__init__.py b/stac_fastapi/api/stac_fastapi/api/__init__.py
deleted file mode 100644
index df6f624..0000000
--- a/stac_fastapi/api/stac_fastapi/api/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""api submodule."""
diff --git a/stac_fastapi/api/stac_fastapi/api/app.py b/stac_fastapi/api/stac_fastapi/api/app.py
deleted file mode 100644
index d18844e..0000000
--- a/stac_fastapi/api/stac_fastapi/api/app.py
+++ /dev/null
@@ -1,416 +0,0 @@
-"""fastapi app creation."""
-from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
-
-import attr
-from brotli_asgi import BrotliMiddleware
-from fastapi import APIRouter, FastAPI
-from fastapi.openapi.utils import get_openapi
-from fastapi.params import Depends
-from pydantic import BaseModel
-from stac_pydantic import Collection, Item, ItemCollection
-from stac_pydantic.api import ConformanceClasses, LandingPage
-from stac_pydantic.api.collections import Collections
-from stac_pydantic.version import STAC_VERSION
-from starlette.responses import JSONResponse, Response
-
-from stac_fastapi.api.errors import DEFAULT_STATUS_CODES, add_exception_handlers
-from stac_fastapi.api.middleware import CORSMiddleware, ProxyHeaderMiddleware
-from stac_fastapi.api.models import (
- APIRequest,
- CollectionUri,
- EmptyRequest,
- GeoJSONResponse,
- ItemCollectionUri,
- ItemUri,
- create_request_model,
-)
-from stac_fastapi.api.openapi import update_openapi
-from stac_fastapi.api.routes import (
- Scope,
- add_route_dependencies,
- create_async_endpoint,
- create_sync_endpoint,
-)
-
-# TODO: make this module not depend on `stac_fastapi.extensions`
-from stac_fastapi.extensions.core import FieldsExtension, TokenPaginationExtension
-from stac_fastapi.types.config import ApiSettings, Settings
-from stac_fastapi.types.core import AsyncBaseCoreClient, BaseCoreClient
-from stac_fastapi.types.extension import ApiExtension
-from stac_fastapi.types.search import BaseSearchGetRequest, BaseSearchPostRequest
-
-
-@attr.s
-class StacApi:
- """StacApi factory.
-
- Factory for creating a STAC-compliant FastAPI application. After instantation, the application is accessible from
- the `StacApi.app` attribute.
-
- Attributes:
- settings:
- API settings and configuration, potentially using environment variables. See https://pydantic-docs.helpmanual.io/usage/settings/.
- client:
- A subclass of `stac_api.clients.BaseCoreClient`. Defines the application logic which is injected into the API.
- extensions:
- API extensions to include with the application. This may include official STAC extensions as well as third-party add ons.
- exceptions:
- Defines a global mapping between exceptions and status codes, allowing configuration of response behavior on certain exceptions (https://fastapi.tiangolo.com/tutorial/handling-errors/#install-custom-exception-handlers).
- app:
- The FastAPI application, defaults to a fresh application.
- route_dependencies (list of tuples of route scope dicts (eg `{'path': '/collections', 'method': 'POST'}`) and list of dependencies (e.g. `[Depends(oauth2_scheme)]`)):
- Applies specified dependencies to specified routes. This is useful for applying custom auth requirements to routes defined elsewhere in the application.
- """
-
- settings: ApiSettings = attr.ib()
- client: Union[AsyncBaseCoreClient, BaseCoreClient] = attr.ib()
- extensions: List[ApiExtension] = attr.ib(default=attr.Factory(list))
- exceptions: Dict[Type[Exception], int] = attr.ib(
- default=attr.Factory(lambda: DEFAULT_STATUS_CODES)
- )
- app: FastAPI = attr.ib(
- default=attr.Factory(
- lambda self: FastAPI(
- openapi_url=self.settings.openapi_url,
- docs_url=self.settings.docs_url,
- redoc_url=None,
- ),
- takes_self=True,
- ),
- converter=update_openapi,
- )
- router: APIRouter = attr.ib(default=attr.Factory(APIRouter))
- title: str = attr.ib(default="stac-fastapi")
- api_version: str = attr.ib(default="0.1")
- stac_version: str = attr.ib(default=STAC_VERSION)
- description: str = attr.ib(default="stac-fastapi")
- search_get_request_model: Type[BaseSearchGetRequest] = attr.ib(
- default=BaseSearchGetRequest
- )
- search_post_request_model: Type[BaseSearchPostRequest] = attr.ib(
- default=BaseSearchPostRequest
- )
- pagination_extension = attr.ib(default=TokenPaginationExtension)
- response_class: Type[Response] = attr.ib(default=JSONResponse)
- middlewares: List = attr.ib(
- default=attr.Factory(
- lambda: [BrotliMiddleware, CORSMiddleware, ProxyHeaderMiddleware]
- )
- )
- route_dependencies: List[Tuple[List[Scope], List[Depends]]] = attr.ib(default=[])
-
- def get_extension(self, extension: Type[ApiExtension]) -> Optional[ApiExtension]:
- """Get an extension.
-
- Args:
- extension: extension to check for.
-
- Returns:
- The extension instance, if it exists.
- """
- for ext in self.extensions:
- if isinstance(ext, extension):
- return ext
- return None
-
- def _create_endpoint(
- self,
- func: Callable,
- request_type: Union[Type[APIRequest], Type[BaseModel]],
- resp_class: Type[Response],
- ) -> Callable:
- """Create a FastAPI endpoint."""
- if isinstance(self.client, AsyncBaseCoreClient):
- return create_async_endpoint(func, request_type, response_class=resp_class)
- elif isinstance(self.client, BaseCoreClient):
- return create_sync_endpoint(func, request_type, response_class=resp_class)
- raise NotImplementedError
-
- def register_landing_page(self):
- """Register landing page (GET /).
-
- Returns:
- None
- """
- self.router.add_api_route(
- name="Landing Page",
- path="/",
- response_model=LandingPage
- if self.settings.enable_response_models
- else None,
- response_class=self.response_class,
- response_model_exclude_unset=False,
- response_model_exclude_none=True,
- methods=["GET"],
- endpoint=self._create_endpoint(
- self.client.landing_page, EmptyRequest, self.response_class
- ),
- )
-
- def register_conformance_classes(self):
- """Register conformance classes (GET /conformance).
-
- Returns:
- None
- """
- self.router.add_api_route(
- name="Conformance Classes",
- path="/conformance",
- response_model=ConformanceClasses
- if self.settings.enable_response_models
- else None,
- response_class=self.response_class,
- response_model_exclude_unset=True,
- response_model_exclude_none=True,
- methods=["GET"],
- endpoint=self._create_endpoint(
- self.client.conformance, EmptyRequest, self.response_class
- ),
- )
-
- def register_get_item(self):
- """Register get item endpoint (GET /collections/{collection_id}/items/{item_id}).
-
- Returns:
- None
- """
- self.router.add_api_route(
- name="Get Item",
- path="/collections/{collection_id}/items/{item_id}",
- response_model=Item if self.settings.enable_response_models else None,
- response_class=self.response_class,
- response_model_exclude_unset=True,
- response_model_exclude_none=True,
- methods=["GET"],
- endpoint=self._create_endpoint(
- self.client.get_item, ItemUri, self.response_class
- ),
- )
-
- def register_post_search(self):
- """Register search endpoint (POST /search).
-
- Returns:
- None
- """
- fields_ext = self.get_extension(FieldsExtension)
- self.router.add_api_route(
- name="Search",
- path="/search",
- response_model=(ItemCollection if not fields_ext else None)
- if self.settings.enable_response_models
- else None,
- response_class=GeoJSONResponse,
- response_model_exclude_unset=True,
- response_model_exclude_none=True,
- methods=["POST"],
- endpoint=self._create_endpoint(
- self.client.post_search, self.search_post_request_model, GeoJSONResponse
- ),
- )
-
- def register_get_search(self):
- """Register search endpoint (GET /search).
-
- Returns:
- None
- """
- fields_ext = self.get_extension(FieldsExtension)
- self.router.add_api_route(
- name="Search",
- path="/search",
- response_model=(ItemCollection if not fields_ext else None)
- if self.settings.enable_response_models
- else None,
- response_class=GeoJSONResponse,
- response_model_exclude_unset=True,
- response_model_exclude_none=True,
- methods=["GET"],
- endpoint=self._create_endpoint(
- self.client.get_search, self.search_get_request_model, GeoJSONResponse
- ),
- )
-
- def register_get_collections(self):
- """Register get collections endpoint (GET /collections).
-
- Returns:
- None
- """
- self.router.add_api_route(
- name="Get Collections",
- path="/collections",
- response_model=Collections
- if self.settings.enable_response_models
- else None,
- response_class=self.response_class,
- response_model_exclude_unset=True,
- response_model_exclude_none=True,
- methods=["GET"],
- endpoint=self._create_endpoint(
- self.client.all_collections, EmptyRequest, self.response_class
- ),
- )
-
- def register_get_collection(self):
- """Register get collection endpoint (GET /collection/{collection_id}).
-
- Returns:
- None
- """
- self.router.add_api_route(
- name="Get Collection",
- path="/collections/{collection_id}",
- response_model=Collection if self.settings.enable_response_models else None,
- response_class=self.response_class,
- response_model_exclude_unset=True,
- response_model_exclude_none=True,
- methods=["GET"],
- endpoint=self._create_endpoint(
- self.client.get_collection, CollectionUri, self.response_class
- ),
- )
-
- def register_get_item_collection(self):
- """Register get item collection endpoint (GET /collection/{collection_id}/items).
-
- Returns:
- None
- """
- pagination_extension = self.get_extension(self.pagination_extension)
- if pagination_extension is not None:
- mixins = [pagination_extension.GET]
- else:
- mixins = None
- request_model = create_request_model(
- "ItemCollectionURI",
- base_model=ItemCollectionUri,
- mixins=mixins,
- )
- self.router.add_api_route(
- name="Get ItemCollection",
- path="/collections/{collection_id}/items",
- response_model=ItemCollection
- if self.settings.enable_response_models
- else None,
- response_class=self.response_class,
- response_model_exclude_unset=True,
- response_model_exclude_none=True,
- methods=["GET"],
- endpoint=self._create_endpoint(
- self.client.item_collection, request_model, self.response_class
- ),
- )
-
- def register_core(self):
- """Register core STAC endpoints.
-
- GET /
- GET /conformance
- GET /collections
- GET /collections/{collection_id}
- GET /collections/{collection_id}/items
- GET /collection/{collection_id}/items/{item_id}
- GET /search
- POST /search
-
- Injects application logic (StacApi.client) into the API layer.
-
- Returns:
- None
- """
- self.register_landing_page()
- self.register_conformance_classes()
- self.register_get_item()
- self.register_post_search()
- self.register_get_search()
- self.register_get_collections()
- self.register_get_collection()
- self.register_get_item_collection()
-
- def customize_openapi(self) -> Optional[Dict[str, Any]]:
- """Customize openapi schema."""
- if self.app.openapi_schema:
- return self.app.openapi_schema
-
- openapi_schema = get_openapi(
- title=self.title, version=self.api_version, routes=self.app.routes
- )
-
- self.app.openapi_schema = openapi_schema
- return self.app.openapi_schema
-
- def add_health_check(self):
- """Add a health check."""
- mgmt_router = APIRouter(prefix=self.app.state.router_prefix)
-
- @mgmt_router.get("/_mgmt/ping")
- async def ping():
- """Liveliness/readiness probe."""
- return {"message": "PONG"}
-
- self.app.include_router(mgmt_router, tags=["Liveliness/Readiness"])
-
- def add_route_dependencies(
- self, scopes: List[Scope], dependencies=List[Depends]
- ) -> None:
- """Add custom dependencies to routes.
-
- Args:
- scopes: list of scopes. Each scope should be a dict with a `path` and `method` property.
- dependencies: list of [FastAPI dependencies](https://fastapi.tiangolo.com/tutorial/dependencies/) to apply to each scope.
-
- Returns:
- None
- """
- return add_route_dependencies(self.app.router.routes, scopes, dependencies)
-
- def __attrs_post_init__(self):
- """Post-init hook.
-
- Responsible for setting up the application upon instantiation of the class.
-
- Returns:
- None
- """
- # inject settings
- self.client.extensions = self.extensions
- self.client.stac_version = self.stac_version
- self.client.title = self.title
- self.client.description = self.description
-
- fields_ext = self.get_extension(FieldsExtension)
- if fields_ext:
- self.settings.default_includes = fields_ext.default_includes
-
- Settings.set(self.settings)
- self.app.state.settings = self.settings
-
- # Register core STAC endpoints
- self.register_core()
- self.app.include_router(self.router)
-
- # keep link to the router prefix value
- router_prefix = self.router.prefix
- self.app.state.router_prefix = router_prefix if router_prefix else ""
-
- # register extensions
- for ext in self.extensions:
- ext.register(self.app)
-
- # add health check
- self.add_health_check()
-
- # register exception handlers
- add_exception_handlers(self.app, status_codes=self.exceptions)
-
- # customize openapi
- self.app.openapi = self.customize_openapi
-
- # add middlewares
- for middleware in self.middlewares:
- self.app.add_middleware(middleware)
-
- # customize route dependencies
- for scopes, dependencies in self.route_dependencies:
- self.add_route_dependencies(scopes=scopes, dependencies=dependencies)
diff --git a/stac_fastapi/api/stac_fastapi/api/config.py b/stac_fastapi/api/stac_fastapi/api/config.py
deleted file mode 100644
index 3a423e4..0000000
--- a/stac_fastapi/api/stac_fastapi/api/config.py
+++ /dev/null
@@ -1,24 +0,0 @@
-"""Application settings."""
-import enum
-
-
-# TODO: Move to stac-pydantic
-# Does that make sense now? The shift to json schema rather than a well-known enumeration makes that less obvious.
-class ApiExtensions(enum.Enum):
- """Enumeration of available stac api extensions.
-
- Ref: https://github.com/radiantearth/stac-api-spec/tree/master/extensions
- """
-
- context = "context"
- fields = "fields"
- filter = "filter"
- query = "query"
- sort = "sort"
- transaction = "transaction"
-
-
-class AddOns(enum.Enum):
- """Enumeration of available third party add ons."""
-
- bulk_transaction = "bulk-transaction"
diff --git a/stac_fastapi/api/stac_fastapi/api/errors.py b/stac_fastapi/api/stac_fastapi/api/errors.py
deleted file mode 100644
index 29df3b9..0000000
--- a/stac_fastapi/api/stac_fastapi/api/errors.py
+++ /dev/null
@@ -1,95 +0,0 @@
-"""Error handling."""
-
-import logging
-from typing import Callable, Dict, Type, TypedDict
-
-from fastapi import FastAPI
-from fastapi.exceptions import RequestValidationError
-from starlette import status
-from starlette.requests import Request
-from starlette.responses import JSONResponse
-
-from stac_fastapi.types.errors import (
- ConflictError,
- DatabaseError,
- ForeignKeyError,
- InvalidQueryParameter,
- NotFoundError,
-)
-
-logger = logging.getLogger(__name__)
-
-
-DEFAULT_STATUS_CODES = {
- NotFoundError: status.HTTP_404_NOT_FOUND,
- ConflictError: status.HTTP_409_CONFLICT,
- ForeignKeyError: status.HTTP_424_FAILED_DEPENDENCY,
- DatabaseError: status.HTTP_424_FAILED_DEPENDENCY,
- Exception: status.HTTP_500_INTERNAL_SERVER_ERROR,
- InvalidQueryParameter: status.HTTP_400_BAD_REQUEST,
-}
-
-
-class ErrorResponse(TypedDict):
- """A JSON error response returned by the API.
-
- The STAC API spec expects that `code` and `description` are both present in the payload.
-
- Attributes:
- code: A code representing the error, semantics are up to implementor.
- description: A description of the error.
- """
-
- code: str
- description: str
-
-
-def exception_handler_factory(status_code: int) -> Callable:
- """Create a FastAPI exception handler for a particular status code.
-
- Args:
- status_code: HTTP status code.
-
- Returns:
- callable: an exception handler.
- """
-
- def handler(request: Request, exc: Exception):
- """I handle exceptions!!."""
- logger.error(exc, exc_info=True)
- return JSONResponse(
- content=ErrorResponse(code=exc.__class__.__name__, description=str(exc)),
- status_code=status_code,
- )
-
- return handler
-
-
-def add_exception_handlers(
- app: FastAPI, status_codes: Dict[Type[Exception], int]
-) -> None:
- """Add exception handlers to the FastAPI application.
-
- Args:
- app: the FastAPI application.
- status_codes: mapping between exceptions and status codes.
-
- Returns:
- None
- """
- for (exc, code) in status_codes.items():
- app.add_exception_handler(exc, exception_handler_factory(code))
-
- # By default FastAPI will return 422 status codes for invalid requests
- # But the STAC api spec suggests returning a 400 in this case
- def request_validation_exception_handler(
- request: Request, exc: RequestValidationError
- ) -> JSONResponse:
- return JSONResponse(
- content=ErrorResponse(code=exc.__class__.__name__, description=str(exc)),
- status_code=status.HTTP_400_BAD_REQUEST,
- )
-
- app.add_exception_handler(
- RequestValidationError, request_validation_exception_handler
- )
diff --git a/stac_fastapi/api/stac_fastapi/api/middleware.py b/stac_fastapi/api/stac_fastapi/api/middleware.py
deleted file mode 100644
index 21b5581..0000000
--- a/stac_fastapi/api/stac_fastapi/api/middleware.py
+++ /dev/null
@@ -1,134 +0,0 @@
-"""api middleware."""
-import re
-import typing
-from http.client import HTTP_PORT, HTTPS_PORT
-from typing import List, Tuple
-
-from starlette.middleware.cors import CORSMiddleware as _CORSMiddleware
-from starlette.types import ASGIApp, Receive, Scope, Send
-
-
-class CORSMiddleware(_CORSMiddleware):
- """
- Subclass of Starlette's standard CORS middleware with default values set to those reccomended by the STAC API spec.
-
- https://github.com/radiantearth/stac-api-spec/blob/914cf8108302e2ec734340080a45aaae4859bb63/implementation.md#cors
- """
-
- def __init__(
- self,
- app: ASGIApp,
- allow_origins: typing.Sequence[str] = ("*",),
- allow_methods: typing.Sequence[str] = (
- "OPTIONS",
- "POST",
- "GET",
- ),
- allow_headers: typing.Sequence[str] = ("Content-Type",),
- allow_credentials: bool = False,
- allow_origin_regex: typing.Optional[str] = None,
- expose_headers: typing.Sequence[str] = (),
- max_age: int = 600,
- ) -> None:
- """Create CORS middleware."""
- super().__init__(
- app,
- allow_origins,
- allow_methods,
- allow_headers,
- allow_credentials,
- allow_origin_regex,
- expose_headers,
- max_age,
- )
-
-
-class ProxyHeaderMiddleware:
- """
- Account for forwarding headers when deriving base URL.
-
- Prioritise standard Forwarded header, look for non-standard X-Forwarded-* if missing.
- Default to what can be derived from the URL if no headers provided.
- Middleware updates the host header that is interpreted by starlette when deriving Request.base_url.
- """
-
- def __init__(self, app: ASGIApp):
- """Create proxy header middleware."""
- self.app = app
-
- async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
- """Call from stac-fastapi framework."""
- if scope["type"] == "http":
- proto, domain, port = self._get_forwarded_url_parts(scope)
- scope["scheme"] = proto
- if domain is not None:
- port_suffix = ""
- if port is not None:
- if (proto == "http" and port != HTTP_PORT) or (
- proto == "https" and port != HTTPS_PORT
- ):
- port_suffix = f":{port}"
- scope["headers"] = self._replace_header_value_by_name(
- scope,
- "host",
- f"{domain}{port_suffix}",
- )
- await self.app(scope, receive, send)
-
- def _get_forwarded_url_parts(self, scope: Scope) -> Tuple[str]:
- proto = scope.get("scheme", "http")
- header_host = self._get_header_value_by_name(scope, "host")
- if header_host is None:
- domain, port = scope.get("server")
- else:
- header_host_parts = header_host.split(":")
- if len(header_host_parts) == 2:
- domain, port = header_host_parts
- else:
- domain = header_host_parts[0]
- port = None
- forwarded = self._get_header_value_by_name(scope, "forwarded")
- if forwarded is not None:
- parts = forwarded.split(";")
- for part in parts:
- if len(part) > 0 and re.search("=", part):
- key, value = part.split("=")
- if key == "proto":
- proto = value
- elif key == "host":
- host_parts = value.split(":")
- domain = host_parts[0]
- try:
- port = int(host_parts[1]) if len(host_parts) == 2 else None
- except ValueError:
- # ignore ports that are not valid integers
- pass
- else:
- proto = self._get_header_value_by_name(scope, "x-forwarded-proto", proto)
- port_str = self._get_header_value_by_name(scope, "x-forwarded-port", port)
- try:
- port = int(port_str) if port_str is not None else None
- except ValueError:
- # ignore ports that are not valid integers
- pass
-
- return (proto, domain, port)
-
- def _get_header_value_by_name(
- self, scope: Scope, header_name: str, default_value: str = None
- ) -> str:
- headers = scope["headers"]
- candidates = [
- value.decode() for key, value in headers if key.decode() == header_name
- ]
- return candidates[0] if len(candidates) == 1 else default_value
-
- @staticmethod
- def _replace_header_value_by_name(
- scope: Scope, header_name: str, new_value: str
- ) -> List[Tuple[str]]:
- return [
- (name, value)
- for name, value in scope["headers"]
- if name.decode() != header_name
- ] + [(str.encode(header_name), str.encode(new_value))]
diff --git a/stac_fastapi/api/stac_fastapi/api/models.py b/stac_fastapi/api/stac_fastapi/api/models.py
deleted file mode 100644
index d3b5ce4..0000000
--- a/stac_fastapi/api/stac_fastapi/api/models.py
+++ /dev/null
@@ -1,179 +0,0 @@
-"""api request/response models."""
-
-import importlib.util
-from typing import Optional, Type, Union
-
-import attr
-from fastapi import Body, Path
-from pydantic import BaseModel, create_model
-from pydantic.fields import UndefinedType
-
-from stac_fastapi.types.extension import ApiExtension
-from stac_fastapi.types.search import (
- APIRequest,
- BaseSearchGetRequest,
- BaseSearchPostRequest,
-)
-
-
-def create_request_model(
- model_name="SearchGetRequest",
- base_model: Union[Type[BaseModel], APIRequest] = BaseSearchGetRequest,
- extensions: Optional[ApiExtension] = None,
- mixins: Optional[Union[BaseModel, APIRequest]] = None,
- request_type: Optional[str] = "GET",
-) -> Union[Type[BaseModel], APIRequest]:
- """Create a pydantic model for validating request bodies."""
- fields = {}
- extension_models = []
-
- # Check extensions for additional parameters to search
- for extension in extensions or []:
- if extension_model := extension.get_request_model(request_type):
- extension_models.append(extension_model)
-
- mixins = mixins or []
-
- models = [base_model] + extension_models + mixins
-
- # Handle GET requests
- if all([issubclass(m, APIRequest) for m in models]):
- return attr.make_class(model_name, attrs={}, bases=tuple(models))
-
- # Handle POST requests
- elif all([issubclass(m, BaseModel) for m in models]):
- for model in models:
- for (k, v) in model.__fields__.items():
- field_info = v.field_info
- body = Body(
- None
- if isinstance(field_info.default, UndefinedType)
- else field_info.default,
- default_factory=field_info.default_factory,
- alias=field_info.alias,
- alias_priority=field_info.alias_priority,
- title=field_info.title,
- description=field_info.description,
- const=field_info.const,
- gt=field_info.gt,
- ge=field_info.ge,
- lt=field_info.lt,
- le=field_info.le,
- multiple_of=field_info.multiple_of,
- min_items=field_info.min_items,
- max_items=field_info.max_items,
- min_length=field_info.min_length,
- max_length=field_info.max_length,
- regex=field_info.regex,
- extra=field_info.extra,
- )
- fields[k] = (v.outer_type_, body)
- return create_model(model_name, **fields, __base__=base_model)
-
- raise TypeError("Mixed Request Model types. Check extension request types.")
-
-
-def create_get_request_model(
- extensions, base_model: BaseSearchGetRequest = BaseSearchGetRequest
-):
- """Wrap create_request_model to create the GET request model."""
- return create_request_model(
- "SearchGetRequest",
- base_model=base_model,
- extensions=extensions,
- request_type="GET",
- )
-
-
-def create_post_request_model(
- extensions, base_model: BaseSearchPostRequest = BaseSearchPostRequest
-):
- """Wrap create_request_model to create the POST request model."""
- return create_request_model(
- "SearchPostRequest",
- base_model=base_model,
- extensions=extensions,
- request_type="POST",
- )
-
-
-@attr.s # type:ignore
-class CollectionUri(APIRequest):
- """Delete collection."""
-
- collection_id: str = attr.ib(default=Path(..., description="Collection ID"))
-
-
-@attr.s
-class ItemUri(CollectionUri):
- """Delete item."""
-
- item_id: str = attr.ib(default=Path(..., description="Item ID"))
-
-
-@attr.s
-class EmptyRequest(APIRequest):
- """Empty request."""
-
- ...
-
-
-@attr.s
-class ItemCollectionUri(CollectionUri):
- """Get item collection."""
-
- limit: int = attr.ib(default=10)
-
-
-class POSTTokenPagination(BaseModel):
- """Token pagination model for POST requests."""
-
- token: Optional[str] = None
-
-
-@attr.s
-class GETTokenPagination(APIRequest):
- """Token pagination for GET requests."""
-
- token: Optional[str] = attr.ib(default=None)
-
-
-class POSTPagination(BaseModel):
- """Page based pagination for POST requests."""
-
- page: Optional[str] = None
-
-
-@attr.s
-class GETPagination(APIRequest):
- """Page based pagination for GET requests."""
-
- page: Optional[str] = attr.ib(default=None)
-
-
-# Test for ORJSON and use it rather than stdlib JSON where supported
-if importlib.util.find_spec("orjson") is not None:
- from fastapi.responses import ORJSONResponse
-
- class GeoJSONResponse(ORJSONResponse):
- """JSON with custom, vendor content-type."""
-
- media_type = "application/geo+json"
-
- class JSONSchemaResponse(ORJSONResponse):
- """JSON with custom, vendor content-type."""
-
- media_type = "application/schema+json"
-
-else:
- from starlette.responses import JSONResponse
-
- class GeoJSONResponse(JSONResponse):
- """JSON with custom, vendor content-type."""
-
- media_type = "application/geo+json"
-
- class JSONSchemaResponse(JSONResponse):
- """JSON with custom, vendor content-type."""
-
- media_type = "application/schema+json"
diff --git a/stac_fastapi/api/stac_fastapi/api/openapi.py b/stac_fastapi/api/stac_fastapi/api/openapi.py
deleted file mode 100644
index 574176a..0000000
--- a/stac_fastapi/api/stac_fastapi/api/openapi.py
+++ /dev/null
@@ -1,68 +0,0 @@
-"""openapi."""
-from fastapi import FastAPI
-from fastapi.openapi.utils import get_openapi
-from starlette.requests import Request
-from starlette.responses import JSONResponse
-
-from stac_fastapi.api.config import ApiExtensions
-from stac_fastapi.types.config import ApiSettings
-
-
-class VndOaiResponse(JSONResponse):
- """JSON with custom, vendor content-type."""
-
- media_type = "application/vnd.oai.openapi+json;version=3.0"
-
-
-def update_openapi(app: FastAPI) -> FastAPI:
- """Update OpenAPI response content-type.
-
- This function modifies the openapi route to comply with the STAC API spec's
- required content-type response header
- """
- urls = (server_data.get("url") for server_data in app.servers)
- server_urls = {url for url in urls if url}
-
- async def openapi(req: Request) -> JSONResponse:
- root_path = req.scope.get("root_path", "").rstrip("/")
- if root_path not in server_urls:
- if root_path and app.root_path_in_servers:
- app.servers.insert(0, {"url": root_path})
- server_urls.add(root_path)
- return VndOaiResponse(app.openapi())
-
- # Remove the default openapi route
- app.router.routes = list(
- filter(lambda r: r.path != app.openapi_url, app.router.routes)
- )
- # Add the updated openapi route
- app.add_route(app.openapi_url, openapi, include_in_schema=False)
- return app
-
-
-# TODO: Remove or fix, this is currently unused
-# and calls a missing method on ApiSettings
-def config_openapi(app: FastAPI, settings: ApiSettings):
- """Config openapi."""
-
- def custom_openapi():
- """Config openapi."""
- if app.openapi_schema:
- return app.openapi_schema
-
- openapi_schema = get_openapi(
- title="Arturo STAC API", version="0.1", routes=app.routes
- )
-
- if settings.api_extension_is_enabled(ApiExtensions.fields):
- openapi_schema["paths"]["/search"]["get"]["responses"]["200"]["content"][
- "application/json"
- ]["schema"] = {"$ref": "#/components/schemas/ItemCollection"}
- openapi_schema["paths"]["/search"]["post"]["responses"]["200"]["content"][
- "application/json"
- ]["schema"] = {"$ref": "#/components/schemas/ItemCollection"}
-
- app.openapi_schema = openapi_schema
- return app.openapi_schema
-
- app.openapi = custom_openapi
diff --git a/stac_fastapi/api/stac_fastapi/api/routes.py b/stac_fastapi/api/stac_fastapi/api/routes.py
deleted file mode 100644
index 941f05b..0000000
--- a/stac_fastapi/api/stac_fastapi/api/routes.py
+++ /dev/null
@@ -1,145 +0,0 @@
-"""route factories."""
-from typing import Any, Callable, Dict, List, Optional, Type, TypedDict, Union
-
-from fastapi import Depends, params
-from fastapi.dependencies.utils import get_parameterless_sub_dependant
-from pydantic import BaseModel
-from starlette.requests import Request
-from starlette.responses import JSONResponse, Response
-from starlette.routing import BaseRoute, Match
-from starlette.status import HTTP_204_NO_CONTENT
-
-from stac_fastapi.api.models import APIRequest
-
-
-def _wrap_response(resp: Any, response_class: Type[Response]) -> Response:
- if isinstance(resp, Response):
- return resp
- elif resp is not None:
- return response_class(resp)
- else: # None is returned as 204 No Content
- return Response(status_code=HTTP_204_NO_CONTENT)
-
-
-def create_async_endpoint(
- func: Callable,
- request_model: Union[Type[APIRequest], Type[BaseModel], Dict],
- response_class: Type[Response] = JSONResponse,
-):
- """Wrap a coroutine in another coroutine which may be used to create a FastAPI endpoint."""
- if issubclass(request_model, APIRequest):
-
- async def _endpoint(
- request: Request,
- request_data: request_model = Depends(), # type:ignore
- ):
- """Endpoint."""
- return _wrap_response(
- await func(request=request, **request_data.kwargs()), response_class
- )
-
- elif issubclass(request_model, BaseModel):
-
- async def _endpoint(
- request: Request,
- request_data: request_model, # type:ignore
- ):
- """Endpoint."""
- return _wrap_response(
- await func(request_data, request=request), response_class
- )
-
- else:
-
- async def _endpoint(
- request: Request,
- request_data: Dict[str, Any], # type:ignore
- ):
- """Endpoint."""
- return _wrap_response(
- await func(request_data, request=request), response_class
- )
-
- return _endpoint
-
-
-def create_sync_endpoint(
- func: Callable,
- request_model: Union[Type[APIRequest], Type[BaseModel], Dict],
- response_class: Type[Response] = JSONResponse,
-):
- """Wrap a function in another function which may be used to create a FastAPI endpoint."""
- if issubclass(request_model, APIRequest):
-
- def _endpoint(
- request: Request,
- request_data: request_model = Depends(), # type:ignore
- ):
- """Endpoint."""
- return _wrap_response(
- func(request=request, **request_data.kwargs()), response_class
- )
-
- elif issubclass(request_model, BaseModel):
-
- def _endpoint(
- request: Request,
- request_data: request_model, # type:ignore
- ):
- """Endpoint."""
- return _wrap_response(func(request_data, request=request), response_class)
-
- else:
-
- def _endpoint(
- request: Request,
- request_data: Dict[str, Any], # type:ignore
- ):
- """Endpoint."""
- return _wrap_response(func(request_data, request=request), response_class)
-
- return _endpoint
-
-
-class Scope(TypedDict, total=False):
- """More strict version of Starlette's Scope."""
-
- # https://github.com/encode/starlette/blob/6af5c515e0a896cbf3f86ee043b88f6c24200bcf/starlette/types.py#L3
- path: str
- method: str
- type: Optional[str]
-
-
-def add_route_dependencies(
- routes: List[BaseRoute], scopes: List[Scope], dependencies=List[params.Depends]
-) -> None:
- """Add dependencies to routes.
-
- Allows a developer to add dependencies to a route after the route has been
- defined.
-
- Returns:
- None
- """
- for scope in scopes:
- for route in routes:
-
- match, _ = route.matches({"type": "http", **scope})
- if match != Match.FULL:
- continue
-
- # Mimicking how APIRoute handles dependencies:
- # https://github.com/tiangolo/fastapi/blob/1760da0efa55585c19835d81afa8ca386036c325/fastapi/routing.py#L408-L412
- for depends in dependencies[::-1]:
- route.dependant.dependencies.insert(
- 0,
- get_parameterless_sub_dependant(
- depends=depends, path=route.path_format
- ),
- )
-
- # Register dependencies directly on route so that they aren't ignored if
- # the routes are later associated with an app (e.g. app.include_router(router))
- # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/applications.py#L337-L360
- # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/routing.py#L677-L678
- route.dependencies.extend(dependencies)
diff --git a/stac_fastapi/api/stac_fastapi/api/version.py b/stac_fastapi/api/stac_fastapi/api/version.py
deleted file mode 100644
index 895f63a..0000000
--- a/stac_fastapi/api/stac_fastapi/api/version.py
+++ /dev/null
@@ -1,2 +0,0 @@
-"""library version."""
-__version__ = "2.4.1"
diff --git a/stac_fastapi/api/tests/test_api.py b/stac_fastapi/api/tests/test_api.py
deleted file mode 100644
index ab5a304..0000000
--- a/stac_fastapi/api/tests/test_api.py
+++ /dev/null
@@ -1,131 +0,0 @@
-from fastapi import Depends, HTTPException, security, status
-from starlette.testclient import TestClient
-
-from stac_fastapi.api.app import StacApi
-from stac_fastapi.extensions.core import TokenPaginationExtension, TransactionExtension
-from stac_fastapi.types import config, core
-
-
-class TestRouteDependencies:
- @staticmethod
- def _build_api(**overrides):
- settings = config.ApiSettings()
- return StacApi(
- **{
- "settings": settings,
- "client": DummyCoreClient(),
- "extensions": [
- TransactionExtension(
- client=DummyTransactionsClient(), settings=settings
- ),
- TokenPaginationExtension(),
- ],
- **overrides,
- }
- )
-
- @staticmethod
- def _assert_dependency_applied(api, routes):
- with TestClient(api.app) as client:
- for route in routes:
- response = getattr(client, route["method"].lower())(route["path"])
- assert (
- response.status_code == 401
- ), "Unauthenticated requests should be rejected"
- assert response.json() == {"detail": "Not authenticated"}
-
- make_request = getattr(client, route["method"].lower())
- path = route["path"].format(
- collectionId="test_collection", itemId="test_item"
- )
- response = make_request(
- path,
- auth=("bob", "dobbs"),
- data='{"dummy": "payload"}',
- headers={"content-type": "application/json"},
- )
- assert (
- response.status_code == 200
- ), "Authenticated requests should be accepted"
- assert response.json() == "dummy response"
-
- def test_build_api_with_route_dependencies(self):
- routes = [
- {"path": "/collections", "method": "POST"},
- {"path": "/collections", "method": "PUT"},
- {"path": "/collections/{collectionId}", "method": "DELETE"},
- {"path": "/collections/{collectionId}/items", "method": "POST"},
- {"path": "/collections/{collectionId}/items/{itemId}", "method": "PUT"},
- {"path": "/collections/{collectionId}/items/{itemId}", "method": "DELETE"},
- ]
- dependencies = [Depends(must_be_bob)]
- api = self._build_api(route_dependencies=[(routes, dependencies)])
- self._assert_dependency_applied(api, routes)
-
- def test_add_route_dependencies_after_building_api(self):
- routes = [
- {"path": "/collections", "method": "POST"},
- {"path": "/collections", "method": "PUT"},
- {"path": "/collections/{collectionId}", "method": "DELETE"},
- {"path": "/collections/{collectionId}/items", "method": "POST"},
- {"path": "/collections/{collectionId}/items/{itemId}", "method": "PUT"},
- {"path": "/collections/{collectionId}/items/{itemId}", "method": "DELETE"},
- ]
- api = self._build_api()
- api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)])
- self._assert_dependency_applied(api, routes)
-
-
-class DummyCoreClient(core.BaseCoreClient):
- def all_collections(self, *args, **kwargs):
- ...
-
- def get_collection(self, *args, **kwargs):
- ...
-
- def get_item(self, *args, **kwargs):
- ...
-
- def get_search(self, *args, **kwargs):
- ...
-
- def post_search(self, *args, **kwargs):
- ...
-
- def item_collection(self, *args, **kwargs):
- ...
-
-
-class DummyTransactionsClient(core.BaseTransactionsClient):
- """Defines a pattern for implementing the STAC transaction extension."""
-
- def create_item(self, *args, **kwargs):
- return "dummy response"
-
- def update_item(self, *args, **kwargs):
- return "dummy response"
-
- def delete_item(self, *args, **kwargs):
- return "dummy response"
-
- def create_collection(self, *args, **kwargs):
- return "dummy response"
-
- def update_collection(self, *args, **kwargs):
- return "dummy response"
-
- def delete_collection(self, *args, **kwargs):
- return "dummy response"
-
-
-def must_be_bob(
- credentials: security.HTTPBasicCredentials = Depends(security.HTTPBasic()),
-):
- if credentials.username == "bob":
- return True
-
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="You're not Bob",
- headers={"WWW-Authenticate": "Basic"},
- )
diff --git a/stac_fastapi/api/tests/test_middleware.py b/stac_fastapi/api/tests/test_middleware.py
deleted file mode 100644
index e3e90be..0000000
--- a/stac_fastapi/api/tests/test_middleware.py
+++ /dev/null
@@ -1,159 +0,0 @@
-from unittest import mock
-
-import pytest
-from starlette.applications import Starlette
-from starlette.testclient import TestClient
-
-from stac_fastapi.api.app import StacApi
-from stac_fastapi.api.middleware import ProxyHeaderMiddleware
-from stac_fastapi.types.config import ApiSettings
-from stac_fastapi.types.core import BaseCoreClient
-
-
-@pytest.fixture
-def proxy_header_middleware() -> ProxyHeaderMiddleware:
- app = Starlette()
- return ProxyHeaderMiddleware(app)
-
-
-@pytest.fixture
-def test_client() -> TestClient:
- app = StacApi(settings=ApiSettings(), client=mock.create_autospec(BaseCoreClient))
- with TestClient(app.app) as client:
- yield client
-
-
-@pytest.mark.parametrize(
- "headers,key,expected",
- [
- ([(b"host", b"testserver")], "host", "testserver"),
- ([(b"host", b"testserver")], "user-agent", None),
- (
- [(b"host", b"testserver"), (b"accept-encoding", b"gzip, deflate, br")],
- "accept-encoding",
- "gzip, deflate, br",
- ),
- ],
-)
-def test_get_header_value_by_name(
- proxy_header_middleware: ProxyHeaderMiddleware, headers, key, expected
-):
- scope = {"headers": headers}
- actual = proxy_header_middleware._get_header_value_by_name(scope, key)
- assert actual == expected
-
-
-@pytest.mark.parametrize(
- "headers,key,value",
- [
- ([(b"host", b"testserver")], "host", "another-server"),
- ([(b"host", b"testserver")], "user-agent", "agent"),
- (
- [(b"host", b"testserver"), (b"accept-encoding", b"gzip, deflate, br")],
- "accept-encoding",
- "deflate",
- ),
- ],
-)
-def test_replace_header_value_by_name(
- proxy_header_middleware: ProxyHeaderMiddleware, headers, key, value
-):
- scope = {"headers": headers}
- updated_headers = proxy_header_middleware._replace_header_value_by_name(
- scope, key, value
- )
-
- header_value = proxy_header_middleware._get_header_value_by_name(
- {"headers": updated_headers}, key
- )
- assert header_value == value
-
-
-@pytest.mark.parametrize(
- "scope,expected",
- [
- (
- {"scheme": "https", "server": ["testserver", 80], "headers": []},
- ("https", "testserver", 80),
- ),
- (
- {
- "scheme": "http",
- "server": ["testserver", 80],
- "headers": [(b"host", b"testserver:81")],
- },
- ("http", "testserver", 81),
- ),
- (
- {
- "scheme": "http",
- "server": ["testserver", 80],
- "headers": [(b"host", b"testserver")],
- },
- ("http", "testserver", None),
- ),
- (
- {
- "scheme": "http",
- "server": ["testserver", 80],
- "headers": [(b"forwarded", b"proto=https;host=test:1234")],
- },
- ("https", "test", 1234),
- ),
- (
- {
- "scheme": "http",
- "server": ["testserver", 80],
- "headers": [(b"forwarded", b"proto=https;host=test:not-an-integer")],
- },
- ("https", "test", 80),
- ),
- (
- {
- "scheme": "http",
- "server": ["testserver", 80],
- "headers": [(b"x-forwarded-proto", b"https")],
- },
- ("https", "testserver", 80),
- ),
- (
- {
- "scheme": "http",
- "server": ["testserver", 80],
- "headers": [(b"x-forwarded-port", b"1111")],
- },
- ("http", "testserver", 1111),
- ),
- (
- {
- "scheme": "http",
- "server": ["testserver", 80],
- "headers": [(b"x-forwarded-port", b"not-an-integer")],
- },
- ("http", "testserver", 80),
- ),
- (
- {
- "scheme": "http",
- "server": ["testserver", 80],
- "headers": [
- (b"forwarded", b"proto=https;host=test:1234"),
- (b"x-forwarded-port", b"1111"),
- (b"x-forwarded-proto", b"https"),
- ],
- },
- ("https", "test", 1234),
- ),
- ],
-)
-def test_get_forwarded_url_parts(
- proxy_header_middleware: ProxyHeaderMiddleware, scope, expected
-):
- actual = proxy_header_middleware._get_forwarded_url_parts(scope)
- assert actual == expected
-
-
-def test_cors_middleware(test_client):
- resp = test_client.get("/_mgmt/ping", headers={"Origin": "http://netloc"})
- assert resp.status_code == 200
- assert resp.headers["access-control-allow-origin"] == "*"
diff --git a/stac_fastapi/extensions/README.md b/stac_fastapi/extensions/README.md
deleted file mode 100644
index e69de29..0000000
diff --git a/stac_fastapi/extensions/setup.cfg b/stac_fastapi/extensions/setup.cfg
deleted file mode 100644
index 91a9ab5..0000000
--- a/stac_fastapi/extensions/setup.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[metadata]
-version = attr: stac_fastapi.extensions.version.__version__
diff --git a/stac_fastapi/extensions/setup.py b/stac_fastapi/extensions/setup.py
deleted file mode 100644
index 7f5e288..0000000
--- a/stac_fastapi/extensions/setup.py
+++ /dev/null
@@ -1,51 +0,0 @@
-"""stac_fastapi: extensions 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.*",
- "stac-fastapi.types",
- "stac-fastapi.api",
-]
-
-extra_reqs = {
- "dev": [
- "pytest",
- "pytest-cov",
- "pytest-asyncio",
- "pre-commit",
- "requests",
- ],
- "docs": ["mkdocs", "mkdocs-material", "pdocs"],
-}
-
-
-setup(
- name="stac-fastapi.extensions",
- 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,
-)
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/__init__.py
deleted file mode 100644
index 7536c2a..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""extensions submodule."""
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py
deleted file mode 100644
index 96317fe..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""stac_api.extensions.core module."""
-from .context import ContextExtension
-from .fields import FieldsExtension
-from .filter import FilterExtension
-from .pagination import PaginationExtension, TokenPaginationExtension
-from .query import QueryExtension
-from .sort import SortExtension
-from .transaction import TransactionExtension
-
-__all__ = (
- "ContextExtension",
- "FieldsExtension",
- "FilterExtension",
- "PaginationExtension",
- "QueryExtension",
- "SortExtension",
- "TokenPaginationExtension",
- "TransactionExtension",
-)
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/context.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/context.py
deleted file mode 100644
index 5924eba..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/context.py
+++ /dev/null
@@ -1,36 +0,0 @@
-"""context extension."""
-from typing import List, Optional
-
-import attr
-from fastapi import FastAPI
-
-from stac_fastapi.types.extension import ApiExtension
-
-
-@attr.s
-class ContextExtension(ApiExtension):
- """Context Extension.
-
- The Context extension adds a JSON object to ItemCollection responses (`/search`, `/collections/{collection_id}/items`)
- which includes the number of items matched, returned, and the limit requested.
-
- https://github.com/radiantearth/stac-api-spec/blob/master/item-search/README.md#context
- """
-
- conformance_classes: List[str] = attr.ib(
- factory=lambda: ["https://api.stacspec.org/v1.0.0-rc.1/item-search#context"]
- )
- schema_href: Optional[str] = attr.ib(
- default="https://raw.githubusercontent.com/radiantearth/stac-api-spec/v1.0.0-rc.1/fragments/context/json-schema/schema.json"
- )
-
- def register(self, app: FastAPI) -> None:
- """Register the extension with a FastAPI application.
-
- Args:
- app: target FastAPI application.
-
- Returns:
- None
- """
- pass
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/__init__.py
deleted file mode 100644
index b9a246b..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""Fields extension module."""
-
-
-from .fields import FieldsExtension
-
-__all__ = ["FieldsExtension"]
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py
deleted file mode 100644
index 93a69a2..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py
+++ /dev/null
@@ -1,59 +0,0 @@
-"""fields extension."""
-from typing import List, Optional, Set
-
-import attr
-from fastapi import FastAPI
-
-from stac_fastapi.types.extension import ApiExtension
-
-from .request import FieldsExtensionGetRequest, FieldsExtensionPostRequest
-
-
-@attr.s
-class FieldsExtension(ApiExtension):
- """Fields Extension.
-
- The Fields extension adds functionality to the `/search` endpoint which allows the caller to include or exclude
- specific from the API response. Registering this extension with the application has the added effect of removing
- the `ItemCollection` response model from the `/search` endpoint, as the Fields extension allows the API to return
- potentially invalid responses by excluding fields which are required by the STAC spec, such as geometry.
-
- https://github.com/radiantearth/stac-api-spec/blob/master/item-search/README.md#fields
-
- Attributes:
- default_includes (set): defines the default set of included fields.
- conformance_classes (list): Defines the list of conformance classes for the extension
-
- """
-
- GET = FieldsExtensionGetRequest
- POST = FieldsExtensionPostRequest
-
- conformance_classes: List[str] = attr.ib(
- factory=lambda: ["https://api.stacspec.org/v1.0.0-rc.1/item-search#fields"]
- )
- default_includes: Set[str] = attr.ib(
- factory=lambda: {
- "id",
- "type",
- "stac_version",
- "geometry",
- "bbox",
- "links",
- "assets",
- "properties.datetime",
- "collection",
- }
- )
- schema_href: Optional[str] = attr.ib(default=None)
-
- def register(self, app: FastAPI) -> None:
- """Register the extension with a FastAPI application.
-
- Args:
- app (fastapi.FastAPI): target FastAPI application.
-
- Returns:
- None
- """
- pass
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/request.py
deleted file mode 100644
index da967a5..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/request.py
+++ /dev/null
@@ -1,72 +0,0 @@
-"""Request models for the fields extension."""
-
-from typing import Dict, Optional, Set
-
-import attr
-from pydantic import BaseModel, Field
-
-from stac_fastapi.types.config import Settings
-from stac_fastapi.types.search import APIRequest, str2list
-
-
-class PostFieldsExtension(BaseModel):
- """FieldsExtension.
-
- Attributes:
- include: set of fields to include.
- exclude: set of fields to exclude.
- """
-
- include: Optional[Set[str]] = set()
- exclude: Optional[Set[str]] = set()
-
- @staticmethod
- def _get_field_dict(fields: Optional[Set[str]]) -> Dict:
- """Pydantic include/excludes notation.
-
- Internal method to create a dictionary for advanced include or exclude of pydantic fields on model export
- Ref: https://pydantic-docs.helpmanual.io/usage/exporting_models/#advanced-include-and-exclude
- """
- field_dict = {}
- for field in fields or []:
- if "." in field:
- parent, key = field.split(".")
- if parent not in field_dict:
- field_dict[parent] = {key}
- else:
- if field_dict[parent] is not ...:
- field_dict[parent].add(key)
- else:
- field_dict[field] = ... # type:ignore
- return field_dict
-
- @property
- def filter_fields(self) -> Dict:
- """Create pydantic include/exclude expression.
-
- Create dictionary of fields to include/exclude on model export based on the included and excluded fields passed
- to the API
- Ref: https://pydantic-docs.helpmanual.io/usage/exporting_models/#advanced-include-and-exclude
- """
- # Always include default_includes, even if they
- # exist in the exclude list.
- include = (self.include or set()) - (self.exclude or set())
- include |= Settings.get().default_includes or set()
-
- return {
- "include": self._get_field_dict(include),
- "exclude": self._get_field_dict(self.exclude),
- }
-
-
-@attr.s
-class FieldsExtensionGetRequest(APIRequest):
- """Additional fields for the GET request."""
-
- fields: Optional[str] = attr.ib(default=None, converter=str2list)
-
-
-class FieldsExtensionPostRequest(BaseModel):
- """Additional fields and schema for the POST request."""
-
- fields: Optional[PostFieldsExtension] = Field(PostFieldsExtension())
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/__init__.py
deleted file mode 100644
index 78256bf..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""Filter extension module."""
-
-
-from .filter import FilterExtension
-
-__all__ = ["FilterExtension"]
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py
deleted file mode 100644
index 0854c9f..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py
+++ /dev/null
@@ -1,123 +0,0 @@
-# encoding: utf-8
-"""Filter Extension."""
-from enum import Enum
-from typing import Callable, List, Type, Union
-
-import attr
-from fastapi import APIRouter, FastAPI
-from starlette.responses import Response
-
-from stac_fastapi.api.models import (
- APIRequest,
- CollectionUri,
- EmptyRequest,
- JSONSchemaResponse,
-)
-from stac_fastapi.api.routes import create_async_endpoint, create_sync_endpoint
-from stac_fastapi.types.core import AsyncBaseFiltersClient, BaseFiltersClient
-from stac_fastapi.types.extension import ApiExtension
-
-from .request import FilterExtensionGetRequest, FilterExtensionPostRequest
-
-
-class FilterConformanceClasses(str, Enum):
- """Conformance classes for the Filter extension.
-
- See https://github.com/radiantearth/stac-api-spec/tree/v1.0.0-rc.1/fragments/filter
- """
-
- FILTER = "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/filter"
- FEATURES_FILTER = (
- "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter"
- )
- ITEM_SEARCH_FILTER = "https://api.stacspec.org/v1.0.0-rc.1/item-search#filter"
- CQL_TEXT = "https://api.stacspec.org/v1.0.0-rc.1/item-search#filter:cql-text"
- CQL_JSON = "https://api.stacspec.org/v1.0.0-rc.1/item-search#filter:cql-json"
- BASIC_CQL = "https://api.stacspec.org/v1.0.0-rc.1/item-search#filter:basic-cql"
- BASIC_SPATIAL_OPERATORS = "https://api.stacspec.org/v1.0.0-rc.1/item-search#filter:basic-spatial-operators"
- BASIC_TEMPORAL_OPERATORS = "https://api.stacspec.org/v1.0.0-rc.1/item-search#filter:basic-temporal-operators"
- ENHANCED_COMPARISON_OPERATORS = "https://api.stacspec.org/v1.0.0-rc.1/item-search#filter:enhanced-comparison-operators"
- ENHANCED_SPATIAL_OPERATORS = "https://api.stacspec.org/v1.0.0-rc.1/item-search#filter:enhanced-spatial-operators"
- ENHANCED_TEMPORAL_OPERATORS = "https://api.stacspec.org/v1.0.0-rc.1/item-search#filter:enhanced-temporal-operators"
- FUNCTIONS = "https://api.stacspec.org/v1.0.0-rc.1/item-search#filter:functions"
- ARITHMETIC = "https://api.stacspec.org/v1.0.0-rc.1/item-search#filter:arithmetic"
- ARRAYS = "https://api.stacspec.org/v1.0.0-rc.1/item-search#filter:arrays"
- QUERYABLE_SECOND_OPERAND = "https://api.stacspec.org/v1.0.0-rc.1/item-search#filter:queryable-second-operand"
-
-
-@attr.s
-class FilterExtension(ApiExtension):
- """Filter Extension.
-
- The filter extension adds several endpoints which allow the retrieval of queryables and
- provides an expressive mechanism for searching based on Item Attributes:
- GET /queryables
- GET /collections/{collection_id}/queryables
-
- https://github.com/radiantearth/stac-api-spec/blob/master/fragments/filter/README.md
-
- Attributes:
- client: Queryables endpoint logic
- conformance_classes: Conformance classes provided by the extension
-
- """
-
- GET = FilterExtensionGetRequest
- POST = FilterExtensionPostRequest
-
- client: Union[AsyncBaseFiltersClient, BaseFiltersClient] = attr.ib(
- factory=BaseFiltersClient
- )
- conformance_classes: List[str] = attr.ib(
- default=[
- FilterConformanceClasses.FILTER,
- FilterConformanceClasses.FEATURES_FILTER,
- FilterConformanceClasses.ITEM_SEARCH_FILTER,
- FilterConformanceClasses.BASIC_CQL,
- FilterConformanceClasses.CQL_TEXT,
- ]
- )
- router: APIRouter = attr.ib(factory=APIRouter)
- response_class: Type[Response] = attr.ib(default=JSONSchemaResponse)
-
- def _create_endpoint(
- self,
- func: Callable,
- request_type: Union[
- Type[APIRequest],
- ],
- ) -> Callable:
- """Create a FastAPI endpoint."""
- if isinstance(self.client, AsyncBaseFiltersClient):
- return create_async_endpoint(
- func, request_type, response_class=self.response_class
- )
- if isinstance(self.client, BaseFiltersClient):
- return create_sync_endpoint(
- func, request_type, response_class=self.response_class
- )
- raise NotImplementedError
-
- def register(self, app: FastAPI) -> None:
- """Register the extension with a FastAPI application.
-
- Args:
- app: target FastAPI application.
-
- Returns:
- None
- """
- self.router.prefix = app.state.router_prefix
- self.router.add_api_route(
- name="Queryables",
- path="/queryables",
- methods=["GET"],
- endpoint=self._create_endpoint(self.client.get_queryables, EmptyRequest),
- )
- self.router.add_api_route(
- name="Collection Queryables",
- path="/collections/{collection_id}/queryables",
- methods=["GET"],
- endpoint=self._create_endpoint(self.client.get_queryables, CollectionUri),
- )
- app.include_router(self.router, tags=["Filter Extension"])
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/request.py
deleted file mode 100644
index 633cc20..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/request.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""Filter extension request models."""
-
-from enum import Enum
-from typing import Any, Dict, Optional
-
-import attr
-from pydantic import BaseModel, Field
-
-from stac_fastapi.types.search import APIRequest
-
-
-class FilterLang(str, Enum):
- """Choices for filter-lang value in a POST request.
-
- Based on https://github.com/radiantearth/stac-api-spec/tree/master/fragments/filter#queryables
-
- Note the addition of cql2-json, which is used by the pgstac backend,
- but is not included in the spec above.
- """
-
- cql_json = "cql-json"
- cql2_json = "cql2-json"
- cql2_text = "cql2-text"
-
-
-@attr.s
-class FilterExtensionGetRequest(APIRequest):
- """Filter extension GET request model."""
-
- filter: Optional[str] = attr.ib(default=None)
- filter_crs: Optional[str] = Field(alias="filter-crs", default=None)
- filter_lang: Optional[FilterLang] = Field(alias="filter-lang", default="cql2-text")
-
-
-class FilterExtensionPostRequest(BaseModel):
- """Filter extension POST request model."""
-
- filter: Optional[Dict[str, Any]] = None
- filter_crs: Optional[str] = Field(alias="filter-crs", default=None)
- filter_lang: Optional[FilterLang] = Field(alias="filter-lang", default="cql-json")
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/__init__.py
deleted file mode 100644
index 2557012..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""pagination classes as extensions."""
-
-from .pagination import PaginationExtension
-from .token_pagination import TokenPaginationExtension
-
-__all__ = ["PaginationExtension", "TokenPaginationExtension"]
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/pagination.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/pagination.py
deleted file mode 100644
index 5e834ed..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/pagination.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""Pagination API extension."""
-
-from typing import List, Optional
-
-import attr
-from fastapi import FastAPI
-
-from stac_fastapi.api.models import GETPagination, POSTPagination
-from stac_fastapi.types.extension import ApiExtension
-
-
-@attr.s
-class PaginationExtension(ApiExtension):
- """Token Pagination.
-
- Though not strictly an extension, the chosen pagination will modify the
- form of the request object. By making pagination an extension class, we can
- use create_request_model to dynamically add the correct pagination parameter
- to the request model for OpenAPI generation.
- """
-
- GET = GETPagination
- POST = POSTPagination
-
- conformance_classes: List[str] = attr.ib(factory=list)
- schema_href: Optional[str] = attr.ib(default=None)
-
- def register(self, app: FastAPI) -> None:
- """Register the extension with a FastAPI application.
-
- Args:
- app: target FastAPI application.
-
- Returns:
- None
- """
- pass
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/token_pagination.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/token_pagination.py
deleted file mode 100644
index 1e13999..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/token_pagination.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""Token pagination API extension."""
-
-from typing import List, Optional
-
-import attr
-from fastapi import FastAPI
-
-from stac_fastapi.api.models import GETTokenPagination, POSTTokenPagination
-from stac_fastapi.types.extension import ApiExtension
-
-
-@attr.s
-class TokenPaginationExtension(ApiExtension):
- """Token Pagination.
-
- Though not strictly an extension, the chosen pagination will modify the
- form of the request object. By making pagination an extension class, we can
- use create_request_model to dynamically add the correct pagination parameter
- to the request model for OpenAPI generation.
- """
-
- GET = GETTokenPagination
- POST = POSTTokenPagination
-
- conformance_classes: List[str] = attr.ib(factory=list)
- schema_href: Optional[str] = attr.ib(default=None)
-
- def register(self, app: FastAPI) -> None:
- """Register the extension with a FastAPI application.
-
- Args:
- app: target FastAPI application.
-
- Returns:
- None
- """
- pass
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/__init__.py
deleted file mode 100644
index 5bbe705..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""Query extension module."""
-
-from .query import QueryExtension
-
-__all__ = ["QueryExtension"]
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py
deleted file mode 100644
index 4ae05ea..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""query extension."""
-from typing import List, Optional
-
-import attr
-from fastapi import FastAPI
-
-from stac_fastapi.types.extension import ApiExtension
-
-from .request import QueryExtensionGetRequest, QueryExtensionPostRequest
-
-
-@attr.s
-class QueryExtension(ApiExtension):
- """Query Extension.
-
- The Query extension adds an additional `query` parameter to `/search` requests which allows the caller to perform
- queries against item metadata (ex. find all images with cloud cover less than 15%).
-
- https://github.com/radiantearth/stac-api-spec/blob/master/item-search/README.md#query
- """
-
- GET = QueryExtensionGetRequest
- POST = QueryExtensionPostRequest
-
- conformance_classes: List[str] = attr.ib(
- factory=lambda: ["https://api.stacspec.org/v1.0.0-rc.1/item-search#query"]
- )
- schema_href: Optional[str] = attr.ib(default=None)
-
- def register(self, app: FastAPI) -> None:
- """Register the extension with a FastAPI application.
-
- Args:
- app: target FastAPI application.
-
- Returns:
- None
- """
- pass
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/request.py
deleted file mode 100644
index 8b28288..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/request.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""Request model for the Query extension."""
-
-from typing import Any, Dict, Optional
-
-import attr
-from pydantic import BaseModel
-
-from stac_fastapi.types.search import APIRequest
-
-
-@attr.s
-class QueryExtensionGetRequest(APIRequest):
- """Query Extension GET request model."""
-
- query: Optional[str] = attr.ib(default=None)
-
-
-class QueryExtensionPostRequest(BaseModel):
- """Query Extension POST request model."""
-
- query: Optional[Dict[str, Dict[str, Any]]]
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/__init__.py
deleted file mode 100644
index b6996b0..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""Sort extension module."""
-
-from .sort import SortExtension
-
-__all__ = ["SortExtension"]
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/request.py
deleted file mode 100644
index c19f40d..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/request.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# encoding: utf-8
-"""Request model for the Sort Extension."""
-
-from typing import List, Optional
-
-import attr
-from pydantic import BaseModel
-from stac_pydantic.api.extensions.sort import SortExtension as PostSortModel
-
-from stac_fastapi.types.search import APIRequest, str2list
-
-
-@attr.s
-class SortExtensionGetRequest(APIRequest):
- """Sortby Parameter for GET requests."""
-
- sortby: Optional[str] = attr.ib(default=None, converter=str2list)
-
-
-class SortExtensionPostRequest(BaseModel):
- """Sortby parameter for POST requests."""
-
- sortby: Optional[List[PostSortModel]]
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py
deleted file mode 100644
index 2e2a800..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""sort extension."""
-from typing import List, Optional
-
-import attr
-from fastapi import FastAPI
-
-from stac_fastapi.types.extension import ApiExtension
-
-from .request import SortExtensionGetRequest, SortExtensionPostRequest
-
-
-@attr.s
-class SortExtension(ApiExtension):
- """Sort Extension.
-
- The Sort extension adds the `sortby` parameter to the `/search` endpoint, allowing the caller to specify the sort
- order of the returned items.
-
- https://github.com/radiantearth/stac-api-spec/blob/master/item-search/README.md#sort
- """
-
- GET = SortExtensionGetRequest
- POST = SortExtensionPostRequest
-
- conformance_classes: List[str] = attr.ib(
- factory=lambda: ["https://api.stacspec.org/v1.0.0-rc.1/item-search#sort"]
- )
- schema_href: Optional[str] = attr.ib(default=None)
-
- def register(self, app: FastAPI) -> None:
- """Register the extension with a FastAPI application.
-
- Args:
- app: target FastAPI application.
-
- Returns:
- None
- """
- pass
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py
deleted file mode 100644
index 5967e71..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py
+++ /dev/null
@@ -1,184 +0,0 @@
-"""transaction extension."""
-from typing import Callable, List, Optional, Type, Union
-
-import attr
-from fastapi import APIRouter, Body, FastAPI
-from pydantic import BaseModel
-from stac_pydantic import Collection, Item
-from starlette.responses import JSONResponse, Response
-
-from stac_fastapi.api.models import APIRequest, CollectionUri, ItemUri
-from stac_fastapi.api.routes import create_async_endpoint, create_sync_endpoint
-from stac_fastapi.types import stac as stac_types
-from stac_fastapi.types.config import ApiSettings
-from stac_fastapi.types.core import AsyncBaseTransactionsClient, BaseTransactionsClient
-from stac_fastapi.types.extension import ApiExtension
-
-
-@attr.s
-class PostItem(CollectionUri):
- """Create Item."""
-
- item: stac_types.Item = attr.ib(default=Body())
-
-
-@attr.s
-class PutItem(ItemUri):
- """Update Item."""
-
- item: stac_types.Item = attr.ib(default=Body())
-
-
-@attr.s
-class TransactionExtension(ApiExtension):
- """Transaction Extension.
-
- The transaction extension adds several endpoints which allow the creation, deletion, and updating of items and
- collections:
- POST /collections
- PUT /collections/{collection_id}
- DELETE /collections/{collection_id}
- POST /collections/{collection_id}/items
- PUT /collections/{collection_id}/items
- DELETE /collections/{collection_id}/items
-
- https://github.com/radiantearth/stac-api-spec/blob/master/ogcapi-features/extensions/transaction/README.md
-
- Attributes:
- client: CRUD application logic
- """
-
- client: Union[AsyncBaseTransactionsClient, BaseTransactionsClient] = attr.ib()
- settings: ApiSettings = attr.ib()
- conformance_classes: List[str] = attr.ib(
- factory=lambda: [
- "https://api.stacspec.org/v1.0.0-rc.1/ogcapi-features/extensions/transaction",
- "http://www.opengis.net/spec/ogcapi-features-4/1.0/conf/simpletx",
- ]
- )
- schema_href: Optional[str] = attr.ib(default=None)
- router: APIRouter = attr.ib(factory=APIRouter)
- response_class: Type[Response] = attr.ib(default=JSONResponse)
-
- def _create_endpoint(
- self,
- func: Callable,
- request_type: Union[
- Type[APIRequest],
- Type[BaseModel],
- Type[stac_types.Item],
- Type[stac_types.Collection],
- ],
- ) -> Callable:
- """Create a FastAPI endpoint."""
- if isinstance(self.client, AsyncBaseTransactionsClient):
- return create_async_endpoint(
- func, request_type, response_class=self.response_class
- )
- elif isinstance(self.client, BaseTransactionsClient):
- return create_sync_endpoint(
- func, request_type, response_class=self.response_class
- )
- raise NotImplementedError
-
- def register_create_item(self):
- """Register create item endpoint (POST /collections/{collection_id}/items)."""
- self.router.add_api_route(
- name="Create Item",
- path="/collections/{collection_id}/items",
- response_model=Item if self.settings.enable_response_models else None,
- response_class=self.response_class,
- response_model_exclude_unset=True,
- response_model_exclude_none=True,
- methods=["POST"],
- endpoint=self._create_endpoint(self.client.create_item, PostItem),
- )
-
- def register_update_item(self):
- """Register update item endpoint (PUT /collections/{collection_id}/items)."""
- self.router.add_api_route(
- name="Update Item",
- path="/collections/{collection_id}/items/{item_id}",
- response_model=Item if self.settings.enable_response_models else None,
- response_class=self.response_class,
- response_model_exclude_unset=True,
- response_model_exclude_none=True,
- methods=["PUT"],
- endpoint=self._create_endpoint(self.client.update_item, PutItem),
- )
-
- def register_delete_item(self):
- """Register delete item endpoint (DELETE /collections/{collection_id}/items/{item_id})."""
- self.router.add_api_route(
- name="Delete Item",
- path="/collections/{collection_id}/items/{item_id}",
- response_model=Item if self.settings.enable_response_models else None,
- response_class=self.response_class,
- response_model_exclude_unset=True,
- response_model_exclude_none=True,
- methods=["DELETE"],
- endpoint=self._create_endpoint(self.client.delete_item, ItemUri),
- )
-
- def register_create_collection(self):
- """Register create collection endpoint (POST /collections)."""
- self.router.add_api_route(
- name="Create Collection",
- path="/collections",
- response_model=Collection if self.settings.enable_response_models else None,
- response_class=self.response_class,
- response_model_exclude_unset=True,
- response_model_exclude_none=True,
- methods=["POST"],
- endpoint=self._create_endpoint(
- self.client.create_collection, stac_types.Collection
- ),
- )
-
- def register_update_collection(self):
- """Register update collection endpoint (PUT /collections)."""
- self.router.add_api_route(
- name="Update Collection",
- path="/collections",
- response_model=Collection if self.settings.enable_response_models else None,
- response_class=self.response_class,
- response_model_exclude_unset=True,
- response_model_exclude_none=True,
- methods=["PUT"],
- endpoint=self._create_endpoint(
- self.client.update_collection, stac_types.Collection
- ),
- )
-
- def register_delete_collection(self):
- """Register delete collection endpoint (DELETE /collections/{collection_id})."""
- self.router.add_api_route(
- name="Delete Collection",
- path="/collections/{collection_id}",
- response_model=Collection if self.settings.enable_response_models else None,
- response_class=self.response_class,
- response_model_exclude_unset=True,
- response_model_exclude_none=True,
- methods=["DELETE"],
- endpoint=self._create_endpoint(
- self.client.delete_collection, CollectionUri
- ),
- )
-
- def register(self, app: FastAPI) -> None:
- """Register the extension with a FastAPI application.
-
- Args:
- app: target FastAPI application.
-
- Returns:
- None
- """
- self.router.prefix = app.state.router_prefix
- self.register_create_item()
- self.register_update_item()
- self.register_delete_item()
- self.register_create_collection()
- self.register_update_collection()
- self.register_delete_collection()
- app.include_router(self.router, tags=["Transaction Extension"])
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/third_party/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/third_party/__init__.py
deleted file mode 100644
index ab7349e..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/third_party/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-"""stac_api.extensions.third_party module."""
-from .bulk_transactions import BulkTransactionExtension
-
-__all__ = ("BulkTransactionExtension",)
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/third_party/bulk_transactions.py b/stac_fastapi/extensions/stac_fastapi/extensions/third_party/bulk_transactions.py
deleted file mode 100644
index 3fe25c9..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/third_party/bulk_transactions.py
+++ /dev/null
@@ -1,131 +0,0 @@
-"""bulk transactions extension."""
-import abc
-from typing import Any, Callable, Dict, List, Optional, Type, Union
-
-import attr
-from fastapi import APIRouter, FastAPI
-from pydantic import BaseModel
-
-from stac_fastapi.api.models import create_request_model
-from stac_fastapi.api.routes import create_async_endpoint, create_sync_endpoint
-from stac_fastapi.types.extension import ApiExtension
-from stac_fastapi.types.search import APIRequest
-
-
-class Items(BaseModel):
- """A group of STAC Item objects, in the form of a dictionary from Item.id -> Item."""
-
- items: Dict[str, Any]
-
- def __iter__(self):
- """Return an iterable of STAC Item objects."""
- return iter(self.items.values())
-
-
-@attr.s # type: ignore
-class BaseBulkTransactionsClient(abc.ABC):
- """BulkTransactionsClient."""
-
- @staticmethod
- def _chunks(lst, n):
- """Yield successive n-sized chunks from list.
-
- https://stackoverflow.com/questions/312443/how-do-you-split-a-list-into-evenly-sized-chunks
- """
- for i in range(0, len(lst), n):
- yield lst[i : i + n]
-
- @abc.abstractmethod
- def bulk_item_insert(
- self, items: Items, chunk_size: Optional[int] = None, **kwargs
- ) -> str:
- """Bulk creation of items.
-
- Args:
- items: list of items.
- chunk_size: number of items processed at a time.
-
- Returns:
- Message indicating the status of the insert.
-
- """
- raise NotImplementedError
-
-
-@attr.s # type: ignore
-class AsyncBaseBulkTransactionsClient(abc.ABC):
- """BulkTransactionsClient."""
-
- @abc.abstractmethod
- async def bulk_item_insert(self, items: Items, **kwargs) -> str:
- """Bulk creation of items.
-
- Args:
- items: list of items.
-
- Returns:
- Message indicating the status of the insert.
-
- """
- raise NotImplementedError
-
-
-@attr.s
-class BulkTransactionExtension(ApiExtension):
- """Bulk Transaction Extension.
-
- Bulk Transaction extension adds the `POST /collections/{collection_id}/bulk_items` endpoint to the application
- for efficient bulk insertion of items. The input to this is an object with an attribute "items", that has a value
- that is an object with a group of attributes that are the ids of each Item, and the value is the Item entity.
-
- {
- "items": {
- "id1": { "type": "Feature", ... },
- "id2": { "type": "Feature", ... },
- "id3": { "type": "Feature", ... }
- }
-
- """
-
- client: Union[
- AsyncBaseBulkTransactionsClient, BaseBulkTransactionsClient
- ] = attr.ib()
- conformance_classes: List[str] = attr.ib(default=list())
- schema_href: Optional[str] = attr.ib(default=None)
-
- def _create_endpoint(
- self,
- func: Callable,
- request_type: Union[Type[APIRequest], Type[BaseModel], Dict],
- ) -> Callable:
- """Create a FastAPI endpoint."""
- if isinstance(self.client, AsyncBaseBulkTransactionsClient):
- return create_async_endpoint(func, request_type)
- elif isinstance(self.client, BaseBulkTransactionsClient):
- return create_sync_endpoint(func, request_type)
- raise NotImplementedError
-
- def register(self, app: FastAPI) -> None:
- """Register the extension with a FastAPI application.
-
- Args:
- app: target FastAPI application.
-
- Returns:
- None
- """
- items_request_model = create_request_model("Items", base_model=Items)
-
- router = APIRouter(prefix=app.state.router_prefix)
- router.add_api_route(
- name="Bulk Create Item",
- path="/collections/{collection_id}/bulk_items",
- response_model=str,
- response_model_exclude_unset=True,
- response_model_exclude_none=True,
- methods=["POST"],
- endpoint=self._create_endpoint(
- self.client.bulk_item_insert, items_request_model
- ),
- )
- app.include_router(router, tags=["Bulk Transaction Extension"])
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/version.py b/stac_fastapi/extensions/stac_fastapi/extensions/version.py
deleted file mode 100644
index 895f63a..0000000
--- a/stac_fastapi/extensions/stac_fastapi/extensions/version.py
+++ /dev/null
@@ -1,2 +0,0 @@
-"""library version."""
-__version__ = "2.4.1"
diff --git a/stac_fastapi/pgstac/README.md b/stac_fastapi/pgstac/README.md
deleted file mode 100644
index 7961ad2..0000000
--- 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 8ce7fc4..0000000
--- 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 aea3393..0000000
--- 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 9e704cf..0000000
--- 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.1,<0.2",
- "pypgstac==0.6.*",
-]
-
-extra_reqs = {
- "dev": [
- "pypgstac[psycopg]==0.6.*",
- "pytest",
- "pytest-cov",
- "pytest-asyncio>=0.17",
- "pre-commit",
- "requests",
- "httpx",
- ],
- "docs": ["mkdocs", "mkdocs-material", "pdocs"],
- "server": ["uvicorn[standard]==0.17.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 c260321..0000000
--- 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 a715433..0000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/app.py
+++ /dev/null
@@ -1,91 +0,0 @@
-"""FastAPI application using PGStac."""
-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,
- 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.transactions import BulkTransactionsClient, TransactionsClient
-from stac_fastapi.pgstac.types.search import PgstacSearch
-
-settings = Settings()
-extensions = [
- TransactionExtension(
- client=TransactionsClient(),
- settings=settings,
- response_class=ORJSONResponse,
- ),
- QueryExtension(),
- SortExtension(),
- FieldsExtension(),
- TokenPaginationExtension(),
- ContextExtension(),
- BulkTransactionExtension(client=BulkTransactionsClient()),
-]
-
-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,
- )
- 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 5312fcd..0000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/config.py
+++ /dev/null
@@ -1,55 +0,0 @@
-"""Postgres API configuration."""
-
-from typing import Type
-
-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}:{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}"
-
- @property
- def testing_connection_string(self):
- """Create testing psql connection string."""
- return f"postgresql://{self.postgres_user}:{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 07844f4..0000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/core.py
+++ /dev/null
@@ -1,411 +0,0 @@
-"""Item crud client."""
-import re
-from datetime import datetime
-from typing import Any, Dict, List, Optional, Union
-from urllib.parse import urljoin
-
-import attr
-import orjson
-from asyncpg.exceptions import InvalidDatetimeFormatError
-from buildpg import render
-from fastapi import HTTPException
-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 starlette.requests import Request
-
-from stac_fastapi.pgstac.config import Settings
-from stac_fastapi.pgstac.models.links import CollectionLinks, 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, **kwargs) -> Collections:
- """Read all collections from the database."""
- request: Request = kwargs["request"]
- base_url = get_base_url(request)
- pool = request.app.state.readpool
-
- async with pool.acquire() 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, **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]]
-
- request: Request = kwargs["request"]
- pool = request.app.state.readpool
- async with pool.acquire() 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]]
-
- pool = request.app.state.readpool
- async with pool.acquire() 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,
- **kwargs: Any,
- ) -> 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]
-
- request: Request = kwargs["request"]
- settings: Settings = request.app.state.settings
- pool = request.app.state.readpool
-
- search_request.conf = search_request.conf or {}
- search_request.conf["nohydrate"] = settings.use_api_hydrate
- req = search_request.json(exclude_none=True, by_alias=True)
-
- try:
- async with pool.acquire() as conn:
- q, p = render(
- """
- SELECT * FROM search(:req::text::jsonb);
- """,
- req=req,
- )
- 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,
- 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, **kwargs)
-
- req = self.post_request_model(
- collections=[collection_id], limit=limit, token=token
- )
- item_collection = await self._search_base(req, **kwargs)
- links = await CollectionLinks(
- collection_id=collection_id, request=kwargs["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, **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, **kwargs)
-
- req = self.post_request_model(
- ids=[item_id], collections=[collection_id], limit=1
- )
- item_collection = await self._search_base(req, **kwargs)
- 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, **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, **kwargs)
- return ItemCollection(**item_collection)
-
- async 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] = 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,
- **kwargs,
- ) -> ItemCollection:
- """Cross catalog search (GET).
-
- Called with `GET /search`.
-
- Returns:
- ItemCollection containing items which match the search criteria.
- """
- request = kwargs["request"]
- 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(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 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=kwargs["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 2991a92..0000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/db.py
+++ /dev/null
@@ -1,115 +0,0 @@
-"""Database connection handling."""
-
-import json
-from typing import Dict, Union
-
-import attr
-import orjson
-from asyncpg import exceptions, pool
-from buildpg import asyncpg, render
-from fastapi import FastAPI
-
-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",
- )
-
-
-async def connect_to_db(app: FastAPI) -> None:
- """Connect to Database."""
- 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)
-
-
-async def close_db_connection(app: FastAPI) -> None:
- """Close connection."""
- await app.state.readpool.close()
- await app.state.writepool.close()
-
-
-async def dbfunc(pool: pool, 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
- """
- try:
- if isinstance(arg, str):
- async with pool.acquire() as conn:
- q, p = render(
- f"""
- SELECT * FROM {func}(:item::text);
- """,
- item=arg,
- )
- return await conn.fetchval(q, *p)
- else:
- async with pool.acquire() as conn:
- q, p = render(
- f"""
- SELECT * FROM {func}(:item::text::jsonb);
- """,
- item=json.dumps(arg),
- )
- return await conn.fetchval(q, *p)
- 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 410bc63..0000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""pgstac extension customisations."""
-
-from .query import QueryExtension
-
-__all__ = ["QueryExtension"]
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 91df853..0000000
--- 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 c260321..0000000
--- 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 798db54..0000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/models/links.py
+++ /dev/null
@@ -1,229 +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 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 9013a63..0000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/transactions.py
+++ /dev/null
@@ -1,131 +0,0 @@
-"""transactions extension client."""
-
-import logging
-from typing import Optional, Union
-
-import attr
-from fastapi import HTTPException
-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, **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
- request = kwargs["request"]
- pool = request.app.state.writepool
- await dbfunc(pool, "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, 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})",
- )
- request = kwargs["request"]
- pool = request.app.state.writepool
- await dbfunc(pool, "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, **kwargs
- ) -> Optional[Union[stac_types.Collection, Response]]:
- """Create collection."""
- request = kwargs["request"]
- pool = request.app.state.writepool
- await dbfunc(pool, "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, **kwargs
- ) -> Optional[Union[stac_types.Collection, Response]]:
- """Update collection."""
- request = kwargs["request"]
- pool = request.app.state.writepool
- await dbfunc(pool, "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, **kwargs
- ) -> Optional[Union[stac_types.Item, Response]]:
- """Delete item."""
- request = kwargs["request"]
- pool = request.app.state.writepool
- await dbfunc(pool, "delete_item", item_id)
- return JSONResponse({"deleted item": item_id})
-
- async def delete_collection(
- self, collection_id: str, **kwargs
- ) -> Optional[Union[stac_types.Collection, Response]]:
- """Delete collection."""
- request = kwargs["request"]
- pool = request.app.state.writepool
- await dbfunc(pool, "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, **kwargs) -> str:
- """Bulk item insertion using pgstac."""
- request = kwargs["request"]
- pool = request.app.state.writepool
- items = list(items.items.values())
- await dbfunc(pool, "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 9b92e75..0000000
--- 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 2b8abb9..0000000
--- 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 4a0ce4c..0000000
--- 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 895f63a..0000000
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/version.py
+++ /dev/null
@@ -1,2 +0,0 @@
-"""library version."""
-__version__ = "2.4.1"
diff --git a/stac_fastapi/pgstac/tests/__init__.py b/stac_fastapi/pgstac/tests/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/stac_fastapi/pgstac/tests/api/__init__.py b/stac_fastapi/pgstac/tests/api/__init__.py
deleted file mode 100644
index e69de29..0000000
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 4ace949..0000000
--- a/stac_fastapi/pgstac/tests/api/test_api.py
+++ /dev/null
@@ -1,393 +0,0 @@
-from datetime import datetime, timedelta
-
-import pytest
-
-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}",
-]
-
-
-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_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_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
-
-
-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 == 400
-
-
-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
-
- 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
-
-
-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/")
diff --git a/stac_fastapi/pgstac/tests/clients/__init__.py b/stac_fastapi/pgstac/tests/clients/__init__.py
deleted file mode 100644
index e69de29..0000000
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 345dc4f..0000000
--- a/stac_fastapi/pgstac/tests/clients/test_postgres.py
+++ /dev/null
@@ -1,172 +0,0 @@
-import uuid
-from copy import deepcopy
-from typing import Callable
-
-from stac_pydantic import Collection, Item
-
-# from tests.conftest import MockStarletteRequest
-
-
-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
diff --git a/stac_fastapi/pgstac/tests/conftest.py b/stac_fastapi/pgstac/tests/conftest.py
deleted file mode 100644
index ed3f970..0000000
--- a/stac_fastapi/pgstac/tests/conftest.py
+++ /dev/null
@@ -1,234 +0,0 @@
-import asyncio
-import json
-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 (
- 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.transactions import BulkTransactionsClient, TransactionsClient
-from stac_fastapi.pgstac.types.search import PgstacSearch
-
-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():
- print(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()
- print("migrating...")
- os.environ["postgres_dbname"] = "pgstactestdb"
- conn = await asyncpg.connect(dsn=settings.testing_connection_string)
- val = await conn.fetchval("SELECT true")
- print(val)
- await conn.close()
- db = PgstacDB(dsn=settings.testing_connection_string)
- migrator = Migrate(db)
- version = migrator.run_migration()
- db.close()
- print(f"PGStac Migrated to {version}")
-
- yield settings.testing_connection_string
-
- print("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):
- print(f"{os.environ['postgres_dbname']}")
- yield
- print("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()
- print(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
-
- print(
- "creating client with settings, hydrate: {}, router prefix: '{}'".format(
- api_settings.use_api_hydrate, prefix
- )
- )
-
- extensions = [
- TransactionExtension(client=TransactionsClient(), settings=settings),
- QueryExtension(),
- FilterExtension(),
- SortExtension(),
- FieldsExtension(),
- TokenPaginationExtension(),
- 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):
- print("Creating app Fixture")
- time.time()
- app = api_client.app
- await connect_to_db(app)
-
- yield app
-
- await close_db_connection(app)
-
- print("Closed Pools.")
-
-
-@pytest.fixture(scope="function")
-async def app_client(app):
- print("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 af76816..0000000
--- 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 1bc8dde..0000000
--- 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 32502a3..0000000
--- 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 62fa252..0000000
--- 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 6a6395f..0000000
--- 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 a6e85a0..0000000
--- 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 0d8ec76..0000000
--- 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 e69de29..0000000
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 6910f0f..0000000
--- 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 b9f7885..0000000
--- 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 f15f475..0000000
--- 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 l: l["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 l: l["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 9d2bc3d..0000000
--- 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/types/README.md b/stac_fastapi/types/README.md
deleted file mode 100644
index e69de29..0000000
diff --git a/stac_fastapi/types/setup.cfg b/stac_fastapi/types/setup.cfg
deleted file mode 100644
index d65b121..0000000
--- a/stac_fastapi/types/setup.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[metadata]
-version = attr: stac_fastapi.types.version.__version__
diff --git a/stac_fastapi/types/setup.py b/stac_fastapi/types/setup.py
deleted file mode 100644
index 85f4e6c..0000000
--- a/stac_fastapi/types/setup.py
+++ /dev/null
@@ -1,52 +0,0 @@
-"""stac_fastapi: types module."""
-
-from setuptools import find_namespace_packages, setup
-
-with open("README.md") as f:
- desc = f.read()
-
-install_requires = [
- "fastapi>=0.73.0",
- "attrs",
- "pydantic[dotenv]",
- "stac_pydantic==2.0.*",
- "pystac==1.*",
- "iso8601~=1.0.2",
-]
-
-extra_reqs = {
- "dev": [
- "pytest",
- "pytest-cov",
- "pytest-asyncio",
- "pre-commit",
- "requests",
- ],
- "docs": ["mkdocs", "mkdocs-material", "pdocs"],
-}
-
-
-setup(
- name="stac-fastapi.types",
- 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,
-)
diff --git a/stac_fastapi/types/stac_fastapi/types/__init__.py b/stac_fastapi/types/stac_fastapi/types/__init__.py
deleted file mode 100644
index e1a54d4..0000000
--- a/stac_fastapi/types/stac_fastapi/types/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""backend submodule."""
diff --git a/stac_fastapi/types/stac_fastapi/types/config.py b/stac_fastapi/types/stac_fastapi/types/config.py
deleted file mode 100644
index a5ffbb9..0000000
--- a/stac_fastapi/types/stac_fastapi/types/config.py
+++ /dev/null
@@ -1,54 +0,0 @@
-"""stac_fastapi.types.config module."""
-from typing import Optional, Set
-
-from pydantic import BaseSettings
-
-
-class ApiSettings(BaseSettings):
- """ApiSettings.
-
- Defines api configuration, potentially through environment variables.
- See https://pydantic-docs.helpmanual.io/usage/settings/.
- Attributes:
- environment: name of the environment (ex. dev/prod).
- debug: toggles debug mode.
- forbidden_fields: set of fields defined by STAC but not included in the database.
- indexed_fields:
- set of fields which are usually in `item.properties` but are indexed as distinct columns in
- the database.
- """
-
- # TODO: Remove `default_includes` attribute so we can use `pydantic.BaseSettings` instead
- default_includes: Optional[Set[str]] = None
-
- app_host: str = "0.0.0.0"
- app_port: int = 8000
- reload: bool = True
- enable_response_models: bool = False
-
- openapi_url: str = "/api"
- docs_url: str = "/api.html"
-
- class Config:
- """model config (https://pydantic-docs.helpmanual.io/usage/model_config/)."""
-
- extra = "allow"
- env_file = ".env"
-
-
-class Settings:
- """Holds the global instance of settings."""
-
- _instance: Optional[ApiSettings] = None
-
- @classmethod
- def set(cls, base_settings: ApiSettings):
- """Set the global settings."""
- cls._instance = base_settings
-
- @classmethod
- def get(cls) -> ApiSettings:
- """Get the settings. If they have not yet been set, throws an exception."""
- if cls._instance is None:
- raise ValueError("Settings have not yet been set.")
- return cls._instance
diff --git a/stac_fastapi/types/stac_fastapi/types/conformance.py b/stac_fastapi/types/stac_fastapi/types/conformance.py
deleted file mode 100644
index 49f1323..0000000
--- a/stac_fastapi/types/stac_fastapi/types/conformance.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""Conformance Classes."""
-from enum import Enum
-
-
-class STACConformanceClasses(str, Enum):
- """Conformance classes for the STAC API spec."""
-
- CORE = "https://api.stacspec.org/v1.0.0-rc.1/core"
- OGC_API_FEAT = "https://api.stacspec.org/v1.0.0-rc.1/ogcapi-features"
- COLLECTIONS = "https://api.stacspec.org/v1.0.0-rc.1/collections"
- ITEM_SEARCH = "https://api.stacspec.org/v1.0.0-rc.1/item-search"
-
-
-class OAFConformanceClasses(str, Enum):
- """Conformance classes for OGC API - Features."""
-
- CORE = "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core"
- OPEN_API = "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30"
- GEOJSON = "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson"
-
-
-BASE_CONFORMANCE_CLASSES = [
- STACConformanceClasses.CORE,
- STACConformanceClasses.OGC_API_FEAT,
- STACConformanceClasses.COLLECTIONS,
- STACConformanceClasses.ITEM_SEARCH,
- OAFConformanceClasses.CORE,
- OAFConformanceClasses.OPEN_API,
- OAFConformanceClasses.GEOJSON,
-]
diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py
deleted file mode 100644
index bce7ca2..0000000
--- a/stac_fastapi/types/stac_fastapi/types/core.py
+++ /dev/null
@@ -1,755 +0,0 @@
-"""Base clients."""
-import abc
-from datetime import datetime
-from typing import Any, Dict, List, Optional, Union
-from urllib.parse import urljoin
-
-import attr
-from fastapi import Request
-from stac_pydantic.links import Relations
-from stac_pydantic.shared import MimeTypes
-from stac_pydantic.version import STAC_VERSION
-from starlette.responses import Response
-
-from stac_fastapi.types import stac as stac_types
-from stac_fastapi.types.conformance import BASE_CONFORMANCE_CLASSES
-from stac_fastapi.types.extension import ApiExtension
-from stac_fastapi.types.requests import get_base_url
-from stac_fastapi.types.search import BaseSearchPostRequest
-from stac_fastapi.types.stac import Conformance
-
-NumType = Union[float, int]
-StacType = Dict[str, Any]
-
-
-@attr.s # type:ignore
-class BaseTransactionsClient(abc.ABC):
- """Defines a pattern for implementing the STAC API Transaction Extension."""
-
- @abc.abstractmethod
- def create_item(
- self, collection_id: str, item: stac_types.Item, **kwargs
- ) -> Optional[Union[stac_types.Item, Response]]:
- """Create a new item.
-
- Called with `POST /collections/{collection_id}/items`.
-
- Args:
- item: the item
- collection_id: the id of the collection from the resource path
-
- Returns:
- The item that was created.
-
- """
- ...
-
- @abc.abstractmethod
- def update_item(
- self, collection_id: str, item_id: str, item: stac_types.Item, **kwargs
- ) -> Optional[Union[stac_types.Item, Response]]:
- """Perform a complete update on an existing item.
-
- Called with `PUT /collections/{collection_id}/items`. It is expected that this item already exists. The update
- should do a diff against the saved item and perform any necessary updates. Partial updates are not supported
- by the transactions extension.
-
- Args:
- item: the item (must be complete)
- collection_id: the id of the collection from the resource path
-
- Returns:
- The updated item.
- """
- ...
-
- @abc.abstractmethod
- def delete_item(
- self, item_id: str, collection_id: str, **kwargs
- ) -> Optional[Union[stac_types.Item, Response]]:
- """Delete an item from a collection.
-
- Called with `DELETE /collections/{collection_id}/items/{item_id}`
-
- Args:
- item_id: id of the item.
- collection_id: id of the collection.
-
- Returns:
- The deleted item.
- """
- ...
-
- @abc.abstractmethod
- def create_collection(
- self, collection: stac_types.Collection, **kwargs
- ) -> Optional[Union[stac_types.Collection, Response]]:
- """Create a new collection.
-
- Called with `POST /collections`.
-
- Args:
- collection: the collection
-
- Returns:
- The collection that was created.
- """
- ...
-
- @abc.abstractmethod
- def update_collection(
- self, collection: stac_types.Collection, **kwargs
- ) -> Optional[Union[stac_types.Collection, Response]]:
- """Perform a complete update on an existing collection.
-
- Called with `PUT /collections`. It is expected that this item already exists. The update should do a diff
- against the saved collection and perform any necessary updates. Partial updates are not supported by the
- transactions extension.
-
- Args:
- collection: the collection (must be complete)
- collection_id: the id of the collection from the resource path
-
- Returns:
- The updated collection.
- """
- ...
-
- @abc.abstractmethod
- def delete_collection(
- self, collection_id: str, **kwargs
- ) -> Optional[Union[stac_types.Collection, Response]]:
- """Delete a collection.
-
- Called with `DELETE /collections/{collection_id}`
-
- Args:
- collection_id: id of the collection.
-
- Returns:
- The deleted collection.
- """
- ...
-
-
-@attr.s # type:ignore
-class AsyncBaseTransactionsClient(abc.ABC):
- """Defines a pattern for implementing the STAC transaction extension."""
-
- @abc.abstractmethod
- async def create_item(
- self, collection_id: str, item: stac_types.Item, **kwargs
- ) -> Optional[Union[stac_types.Item, Response]]:
- """Create a new item.
-
- Called with `POST /collections/{collection_id}/items`.
-
- Args:
- item: the item
-
- Returns:
- The item that was created.
-
- """
- ...
-
- @abc.abstractmethod
- async def update_item(
- self, collection_id: str, item_id: str, item: stac_types.Item, **kwargs
- ) -> Optional[Union[stac_types.Item, Response]]:
- """Perform a complete update on an existing item.
-
- Called with `PUT /collections/{collection_id}/items`. It is expected that this item already exists. The update
- should do a diff against the saved item and perform any necessary updates. Partial updates are not supported
- by the transactions extension.
-
- Args:
- item: the item (must be complete)
-
- Returns:
- The updated item.
- """
- ...
-
- @abc.abstractmethod
- async def delete_item(
- self, item_id: str, collection_id: str, **kwargs
- ) -> Optional[Union[stac_types.Item, Response]]:
- """Delete an item from a collection.
-
- Called with `DELETE /collections/{collection_id}/items/{item_id}`
-
- Args:
- item_id: id of the item.
- collection_id: id of the collection.
-
- Returns:
- The deleted item.
- """
- ...
-
- @abc.abstractmethod
- async def create_collection(
- self, collection: stac_types.Collection, **kwargs
- ) -> Optional[Union[stac_types.Collection, Response]]:
- """Create a new collection.
-
- Called with `POST /collections`.
-
- Args:
- collection: the collection
-
- Returns:
- The collection that was created.
- """
- ...
-
- @abc.abstractmethod
- async def update_collection(
- self, collection: stac_types.Collection, **kwargs
- ) -> Optional[Union[stac_types.Collection, Response]]:
- """Perform a complete update on an existing collection.
-
- Called with `PUT /collections`. It is expected that this item already exists. The update should do a diff
- against the saved collection and perform any necessary updates. Partial updates are not supported by the
- transactions extension.
-
- Args:
- collection: the collection (must be complete)
-
- Returns:
- The updated collection.
- """
- ...
-
- @abc.abstractmethod
- async def delete_collection(
- self, collection_id: str, **kwargs
- ) -> Optional[Union[stac_types.Collection, Response]]:
- """Delete a collection.
-
- Called with `DELETE /collections/{collection_id}`
-
- Args:
- collection_id: id of the collection.
-
- Returns:
- The deleted collection.
- """
- ...
-
-
-@attr.s
-class LandingPageMixin(abc.ABC):
- """Create a STAC landing page (GET /)."""
-
- stac_version: str = attr.ib(default=STAC_VERSION)
- landing_page_id: str = attr.ib(default="stac-fastapi")
- title: str = attr.ib(default="stac-fastapi")
- description: str = attr.ib(default="stac-fastapi")
-
- def _landing_page(
- self,
- base_url: str,
- conformance_classes: List[str],
- extension_schemas: List[str],
- ) -> stac_types.LandingPage:
- landing_page = stac_types.LandingPage(
- type="Catalog",
- id=self.landing_page_id,
- title=self.title,
- description=self.description,
- stac_version=self.stac_version,
- conformsTo=conformance_classes,
- links=[
- {
- "rel": Relations.self.value,
- "type": MimeTypes.json,
- "href": base_url,
- },
- {
- "rel": Relations.root.value,
- "type": MimeTypes.json,
- "href": base_url,
- },
- {
- "rel": "data",
- "type": MimeTypes.json,
- "href": urljoin(base_url, "collections"),
- },
- {
- "rel": Relations.conformance.value,
- "type": MimeTypes.json,
- "title": "STAC/WFS3 conformance classes implemented by this server",
- "href": urljoin(base_url, "conformance"),
- },
- {
- "rel": Relations.search.value,
- "type": MimeTypes.geojson,
- "title": "STAC search",
- "href": urljoin(base_url, "search"),
- "method": "GET",
- },
- {
- "rel": Relations.search.value,
- "type": MimeTypes.geojson,
- "title": "STAC search",
- "href": urljoin(base_url, "search"),
- "method": "POST",
- },
- ],
- stac_extensions=extension_schemas,
- )
- return landing_page
-
-
-@attr.s # type:ignore
-class BaseCoreClient(LandingPageMixin, abc.ABC):
- """Defines a pattern for implementing STAC api core endpoints.
-
- Attributes:
- extensions: list of registered api extensions.
- """
-
- base_conformance_classes: List[str] = attr.ib(
- factory=lambda: BASE_CONFORMANCE_CLASSES
- )
- extensions: List[ApiExtension] = attr.ib(default=attr.Factory(list))
- post_request_model = attr.ib(default=BaseSearchPostRequest)
-
- def conformance_classes(self) -> List[str]:
- """Generate conformance classes by adding extension conformance to base conformance classes."""
- base_conformance_classes = self.base_conformance_classes.copy()
-
- for extension in self.extensions:
- extension_classes = getattr(extension, "conformance_classes", [])
- base_conformance_classes.extend(extension_classes)
-
- return list(set(base_conformance_classes))
-
- def extension_is_enabled(self, extension: str) -> bool:
- """Check if an api extension is enabled."""
- return any([type(ext).__name__ == extension for ext in self.extensions])
-
- def list_conformance_classes(self):
- """Return a list of conformance classes, including implemented extensions."""
- base_conformance = BASE_CONFORMANCE_CLASSES
-
- for extension in self.extensions:
- extension_classes = getattr(extension, "conformance_classes", [])
- base_conformance.extend(extension_classes)
-
- return base_conformance
-
- def landing_page(self, **kwargs) -> stac_types.LandingPage:
- """Landing page.
-
- Called with `GET /`.
-
- Returns:
- API landing page, serving as an entry point to the API.
- """
- request: Request = kwargs["request"]
- base_url = get_base_url(request)
- extension_schemas = [
- schema.schema_href for schema in self.extensions if schema.schema_href
- ]
- landing_page = self._landing_page(
- base_url=base_url,
- conformance_classes=self.conformance_classes(),
- extension_schemas=extension_schemas,
- )
-
- # Add Collections links
- collections = self.all_collections(request=kwargs["request"])
- for collection in collections["collections"]:
- landing_page["links"].append(
- {
- "rel": Relations.child.value,
- "type": MimeTypes.json.value,
- "title": collection.get("title") or collection.get("id"),
- "href": urljoin(base_url, f"collections/{collection['id']}"),
- }
- )
-
- # Add OpenAPI URL
- landing_page["links"].append(
- {
- "rel": "service-desc",
- "type": "application/vnd.oai.openapi+json;version=3.0",
- "title": "OpenAPI service description",
- "href": urljoin(
- str(request.base_url), request.app.openapi_url.lstrip("/")
- ),
- }
- )
-
- # Add human readable service-doc
- landing_page["links"].append(
- {
- "rel": "service-doc",
- "type": "text/html",
- "title": "OpenAPI service documentation",
- "href": urljoin(
- str(request.base_url), request.app.docs_url.lstrip("/")
- ),
- }
- )
-
- return landing_page
-
- def conformance(self, **kwargs) -> stac_types.Conformance:
- """Conformance classes.
-
- Called with `GET /conformance`.
-
- Returns:
- Conformance classes which the server conforms to.
- """
- return Conformance(conformsTo=self.conformance_classes())
-
- @abc.abstractmethod
- def post_search(
- self, search_request: BaseSearchPostRequest, **kwargs
- ) -> stac_types.ItemCollection:
- """Cross catalog search (POST).
-
- Called with `POST /search`.
-
- Args:
- search_request: search request parameters.
-
- Returns:
- ItemCollection containing items which match the search criteria.
- """
- ...
-
- @abc.abstractmethod
- 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,
- **kwargs,
- ) -> stac_types.ItemCollection:
- """Cross catalog search (GET).
-
- Called with `GET /search`.
-
- Returns:
- ItemCollection containing items which match the search criteria.
- """
- ...
-
- @abc.abstractmethod
- def get_item(self, item_id: str, collection_id: str, **kwargs) -> stac_types.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.
-
- Returns:
- Item.
- """
- ...
-
- @abc.abstractmethod
- def all_collections(self, **kwargs) -> stac_types.Collections:
- """Get all available collections.
-
- Called with `GET /collections`.
-
- Returns:
- A list of collections.
- """
- ...
-
- @abc.abstractmethod
- def get_collection(self, collection_id: str, **kwargs) -> stac_types.Collection:
- """Get collection by id.
-
- Called with `GET /collections/{collection_id}`.
-
- Args:
- collection_id: Id of the collection.
-
- Returns:
- Collection.
- """
- ...
-
- @abc.abstractmethod
- def item_collection(
- self, collection_id: str, limit: int = 10, token: str = None, **kwargs
- ) -> stac_types.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.
- """
- ...
-
-
-@attr.s # type:ignore
-class AsyncBaseCoreClient(LandingPageMixin, abc.ABC):
- """Defines a pattern for implementing STAC api core endpoints.
-
- Attributes:
- extensions: list of registered api extensions.
- """
-
- base_conformance_classes: List[str] = attr.ib(
- factory=lambda: BASE_CONFORMANCE_CLASSES
- )
- extensions: List[ApiExtension] = attr.ib(default=attr.Factory(list))
- post_request_model = attr.ib(default=BaseSearchPostRequest)
-
- def conformance_classes(self) -> List[str]:
- """Generate conformance classes by adding extension conformance to base conformance classes."""
- conformance_classes = self.base_conformance_classes.copy()
-
- for extension in self.extensions:
- extension_classes = getattr(extension, "conformance_classes", [])
- conformance_classes.extend(extension_classes)
-
- return list(set(conformance_classes))
-
- def extension_is_enabled(self, extension: str) -> bool:
- """Check if an api extension is enabled."""
- return any([type(ext).__name__ == extension for ext in self.extensions])
-
- async def landing_page(self, **kwargs) -> stac_types.LandingPage:
- """Landing page.
-
- Called with `GET /`.
-
- Returns:
- API landing page, serving as an entry point to the API.
- """
- request: Request = kwargs["request"]
- base_url = get_base_url(request)
- extension_schemas = [
- schema.schema_href for schema in self.extensions if schema.schema_href
- ]
- landing_page = self._landing_page(
- base_url=base_url,
- conformance_classes=self.conformance_classes(),
- extension_schemas=extension_schemas,
- )
- collections = await self.all_collections(request=kwargs["request"])
- for collection in collections["collections"]:
- landing_page["links"].append(
- {
- "rel": Relations.child.value,
- "type": MimeTypes.json.value,
- "title": collection.get("title") or collection.get("id"),
- "href": urljoin(base_url, f"collections/{collection['id']}"),
- }
- )
-
- # Add OpenAPI URL
- landing_page["links"].append(
- {
- "rel": "service-desc",
- "type": "application/vnd.oai.openapi+json;version=3.0",
- "title": "OpenAPI service description",
- "href": urljoin(
- str(request.base_url), request.app.openapi_url.lstrip("/")
- ),
- }
- )
-
- # Add human readable service-doc
- landing_page["links"].append(
- {
- "rel": "service-doc",
- "type": "text/html",
- "title": "OpenAPI service documentation",
- "href": urljoin(
- str(request.base_url), request.app.docs_url.lstrip("/")
- ),
- }
- )
-
- return landing_page
-
- async def conformance(self, **kwargs) -> stac_types.Conformance:
- """Conformance classes.
-
- Called with `GET /conformance`.
-
- Returns:
- Conformance classes which the server conforms to.
- """
- return Conformance(conformsTo=self.conformance_classes())
-
- @abc.abstractmethod
- async def post_search(
- self, search_request: BaseSearchPostRequest, **kwargs
- ) -> stac_types.ItemCollection:
- """Cross catalog search (POST).
-
- Called with `POST /search`.
-
- Args:
- search_request: search request parameters.
-
- Returns:
- ItemCollection containing items which match the search criteria.
- """
- ...
-
- @abc.abstractmethod
- async 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,
- **kwargs,
- ) -> stac_types.ItemCollection:
- """Cross catalog search (GET).
-
- Called with `GET /search`.
-
- Returns:
- ItemCollection containing items which match the search criteria.
- """
- ...
-
- @abc.abstractmethod
- async def get_item(
- self, item_id: str, collection_id: str, **kwargs
- ) -> stac_types.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.
-
- Returns:
- Item.
- """
- ...
-
- @abc.abstractmethod
- async def all_collections(self, **kwargs) -> stac_types.Collections:
- """Get all available collections.
-
- Called with `GET /collections`.
-
- Returns:
- A list of collections.
- """
- ...
-
- @abc.abstractmethod
- async def get_collection(
- self, collection_id: str, **kwargs
- ) -> stac_types.Collection:
- """Get collection by id.
-
- Called with `GET /collections/{collection_id}`.
-
- Args:
- collection_id: Id of the collection.
-
- Returns:
- Collection.
- """
- ...
-
- @abc.abstractmethod
- async def item_collection(
- self, collection_id: str, limit: int = 10, token: str = None, **kwargs
- ) -> stac_types.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.
- """
- ...
-
-
-@attr.s
-class AsyncBaseFiltersClient(abc.ABC):
- """Defines a pattern for implementing the STAC filter extension."""
-
- async def get_queryables(
- self, collection_id: Optional[str] = None, **kwargs
- ) -> Dict[str, Any]:
- """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
- """
- return {
- "$schema": "https://json-schema.org/draft/2019-09/schema",
- "$id": "https://example.org/queryables",
- "type": "object",
- "title": "Queryables for Example STAC API",
- "description": "Queryable names for the example STAC API Item Search filter.",
- "properties": {},
- }
-
-
-@attr.s
-class BaseFiltersClient(abc.ABC):
- """Defines a pattern for implementing the STAC filter extension."""
-
- def get_queryables(
- self, collection_id: Optional[str] = None, **kwargs
- ) -> Dict[str, Any]:
- """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
- """
- return {
- "$schema": "https://json-schema.org/draft/2019-09/schema",
- "$id": "https://example.org/queryables",
- "type": "object",
- "title": "Queryables for Example STAC API",
- "description": "Queryable names for the example STAC API Item Search filter.",
- "properties": {},
- }
diff --git a/stac_fastapi/types/stac_fastapi/types/errors.py b/stac_fastapi/types/stac_fastapi/types/errors.py
deleted file mode 100644
index 9bd51ed..0000000
--- a/stac_fastapi/types/stac_fastapi/types/errors.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""stac_fastapi.types.errors module."""
-
-
-class StacApiError(Exception):
- """Generic API error."""
-
- pass
-
-
-class ConflictError(StacApiError):
- """Database conflict."""
-
- pass
-
-
-class NotFoundError(StacApiError):
- """Resource not found."""
-
- pass
-
-
-class ForeignKeyError(StacApiError):
- """Foreign key error (collection does not exist)."""
-
- pass
-
-
-class DatabaseError(StacApiError):
- """Generic database errors."""
-
- pass
-
-
-class InvalidQueryParameter(StacApiError):
- """Error for unknown or invalid query parameters.
-
- Used to capture errors that should respond according to
- http://docs.opengeospatial.org/is/17-069r3/17-069r3.html#query_parameters
- """
-
- pass
diff --git a/stac_fastapi/types/stac_fastapi/types/extension.py b/stac_fastapi/types/stac_fastapi/types/extension.py
deleted file mode 100644
index 1e4774b..0000000
--- a/stac_fastapi/types/stac_fastapi/types/extension.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""base api extension."""
-import abc
-from typing import List, Optional
-
-import attr
-from fastapi import FastAPI
-from pydantic import BaseModel
-
-
-@attr.s
-class ApiExtension(abc.ABC):
- """Abstract base class for defining API extensions."""
-
- GET = None
- POST = None
-
- def get_request_model(self, verb: Optional[str] = "GET") -> Optional[BaseModel]:
- """Return the request model for the extension.method.
-
- The model can differ based on HTTP verb
- """
- return getattr(self, verb)
-
- conformance_classes: List[str] = attr.ib(factory=list)
- schema_href: Optional[str] = attr.ib(default=None)
-
- @abc.abstractmethod
- def register(self, app: FastAPI) -> None:
- """Register the extension with a FastAPI application.
-
- Args:
- app: target FastAPI application.
-
- Returns:
- None
- """
- pass
diff --git a/stac_fastapi/types/stac_fastapi/types/links.py b/stac_fastapi/types/stac_fastapi/types/links.py
deleted file mode 100644
index 0349984..0000000
--- a/stac_fastapi/types/stac_fastapi/types/links.py
+++ /dev/null
@@ -1,110 +0,0 @@
-"""link helpers."""
-
-from typing import Any, Dict, List
-from urllib.parse import urljoin
-
-import attr
-from stac_pydantic.links import Relations
-from stac_pydantic.shared import MimeTypes
-
-# 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 resolve_links(links: list, base_url: str) -> List[Dict]:
- """Convert relative links to absolute links."""
- filtered_links = filter_links(links)
- for link in filtered_links:
- link.update({"href": urljoin(base_url, link["href"])})
- return filtered_links
-
-
-@attr.s
-class BaseLinks:
- """Create inferred links common to collections and items."""
-
- collection_id: str = attr.ib()
- base_url: str = attr.ib()
-
- def root(self) -> Dict[str, Any]:
- """Return the catalog root."""
- return dict(rel=Relations.root, type=MimeTypes.json, href=self.base_url)
-
-
-@attr.s
-class CollectionLinks(BaseLinks):
- """Create inferred links specific to collections."""
-
- def self(self) -> Dict[str, Any]:
- """Create the `self` link."""
- return dict(
- rel=Relations.self,
- type=MimeTypes.json,
- href=urljoin(self.base_url, f"collections/{self.collection_id}"),
- )
-
- def parent(self) -> Dict[str, Any]:
- """Create the `parent` link."""
- return dict(rel=Relations.parent, type=MimeTypes.json, href=self.base_url)
-
- def items(self) -> Dict[str, Any]:
- """Create the `items` link."""
- return dict(
- rel="items",
- type=MimeTypes.geojson,
- href=urljoin(self.base_url, f"collections/{self.collection_id}/items"),
- )
-
- def create_links(self) -> List[Dict[str, Any]]:
- """Return all inferred links."""
- return [self.self(), self.parent(), self.items(), self.root()]
-
-
-@attr.s
-class ItemLinks(BaseLinks):
- """Create inferred links specific to items."""
-
- item_id: str = attr.ib()
-
- def self(self) -> Dict[str, Any]:
- """Create the `self` link."""
- return dict(
- rel=Relations.self,
- type=MimeTypes.geojson,
- href=urljoin(
- self.base_url,
- f"collections/{self.collection_id}/items/{self.item_id}",
- ),
- )
-
- def parent(self) -> Dict[str, Any]:
- """Create the `parent` link."""
- return dict(
- rel=Relations.parent,
- type=MimeTypes.json,
- href=urljoin(self.base_url, f"collections/{self.collection_id}"),
- )
-
- def collection(self) -> Dict[str, Any]:
- """Create the `collection` link."""
- return dict(
- rel=Relations.collection,
- type=MimeTypes.json,
- href=urljoin(self.base_url, f"collections/{self.collection_id}"),
- )
-
- def create_links(self) -> List[Dict[str, Any]]:
- """Return all inferred links."""
- links = [
- self.self(),
- self.parent(),
- self.collection(),
- self.root(),
- ]
- return links
diff --git a/stac_fastapi/types/stac_fastapi/types/requests.py b/stac_fastapi/types/stac_fastapi/types/requests.py
deleted file mode 100644
index 7ce0e81..0000000
--- a/stac_fastapi/types/stac_fastapi/types/requests.py
+++ /dev/null
@@ -1,14 +0,0 @@
-"""requests helpers."""
-
-from starlette.requests import Request
-
-
-def get_base_url(request: Request) -> str:
- """Get base URL with respect of APIRouter prefix."""
- app = request.app
- if not app.state.router_prefix:
- return str(request.base_url)
- else:
- return "{}{}/".format(
- str(request.base_url), app.state.router_prefix.lstrip("/")
- )
diff --git a/stac_fastapi/types/stac_fastapi/types/rfc3339.py b/stac_fastapi/types/stac_fastapi/types/rfc3339.py
deleted file mode 100644
index 6e3f977..0000000
--- a/stac_fastapi/types/stac_fastapi/types/rfc3339.py
+++ /dev/null
@@ -1,86 +0,0 @@
-"""rfc3339."""
-import re
-from datetime import datetime, timezone
-from typing import Optional, Tuple
-
-import iso8601
-from pystac.utils import datetime_to_str
-
-RFC33339_PATTERN = r"^(\d\d\d\d)\-(\d\d)\-(\d\d)(T|t)(\d\d):(\d\d):(\d\d)([.]\d+)?(Z|([-+])(\d\d):(\d\d))$"
-
-
-def rfc3339_str_to_datetime(s: str) -> datetime:
- """Convert a string conforming to RFC 3339 to a :class:`datetime.datetime`.
-
- Uses :meth:`iso8601.parse_date` under the hood.
-
- Args:
- s (str) : The string to convert to :class:`datetime.datetime`.
-
- Returns:
- str: The datetime represented by the ISO8601 (RFC 3339) formatted string.
-
- Raises:
- ValueError: If the string is not a valid RFC 3339 string.
- """
- # Uppercase the string
- s = s.upper()
-
- # Match against RFC3339 regex.
- result = re.match(RFC33339_PATTERN, s)
- if not result:
- raise ValueError("Invalid RFC3339 datetime.")
-
- # Parse with pyiso8601
- return iso8601.parse_date(s)
-
-
-def str_to_interval(
- interval: str,
-) -> Optional[Tuple[Optional[datetime], Optional[datetime]]]:
- """Extract a tuple of datetimes from an interval string.
-
- Interval strings are defined by
- OGC API - Features Part 1 for the datetime query parameter value. These follow the
- form '1985-04-12T23:20:50.52Z/1986-04-12T23:20:50.52Z', and allow either the start
- or end (but not both) to be open-ended with '..' or ''.
-
- Args:
- interval (str) : The interval string to convert to a :class:`datetime.datetime`
- tuple.
-
- Raises:
- ValueError: If the string is not a valid interval string.
- """
- if not interval:
- raise ValueError("Empty interval string is invalid.")
-
- values = interval.split("/")
- if len(values) != 2:
- raise ValueError(
- f"Interval string '{interval}' contains more than one forward slash."
- )
-
- start = None
- end = None
- if not values[0] in ["..", ""]:
- start = rfc3339_str_to_datetime(values[0])
- if not values[1] in ["..", ""]:
- end = rfc3339_str_to_datetime(values[1])
-
- if start is None and end is None:
- raise ValueError("Double open-ended intervals are not allowed.")
- if start is not None and end is not None and start > end:
- raise ValueError("Start datetime cannot be before end datetime.")
- else:
- return start, end
-
-
-def now_in_utc() -> datetime:
- """Return a datetime value of now with the UTC timezone applied."""
- return datetime.now(timezone.utc)
-
-
-def now_to_rfc3339_str() -> str:
- """Return an RFC 3339 string representing now."""
- return datetime_to_str(now_in_utc())
diff --git a/stac_fastapi/types/stac_fastapi/types/search.py b/stac_fastapi/types/stac_fastapi/types/search.py
deleted file mode 100644
index f12c3c5..0000000
--- a/stac_fastapi/types/stac_fastapi/types/search.py
+++ /dev/null
@@ -1,201 +0,0 @@
-"""stac_fastapi.types.search module.
-
-# TODO: replace with stac-pydantic
-"""
-
-import abc
-import operator
-from datetime import datetime
-from enum import auto
-from types import DynamicClassAttribute
-from typing import Any, Callable, Dict, List, Optional, Union
-
-import attr
-from geojson_pydantic.geometries import (
- LineString,
- MultiLineString,
- MultiPoint,
- MultiPolygon,
- Point,
- Polygon,
- _GeometryBase,
-)
-from pydantic import BaseModel, conint, validator
-from stac_pydantic.shared import BBox
-from stac_pydantic.utils import AutoValueEnum
-
-from stac_fastapi.types.rfc3339 import rfc3339_str_to_datetime, str_to_interval
-
-# 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_)
-
-
-def str2list(x: str) -> Optional[List]:
- """Convert string to list base on , delimiter."""
- if x:
- return x.split(",")
-
-
-@attr.s # type:ignore
-class APIRequest(abc.ABC):
- """Generic API Request base class."""
-
- def kwargs(self) -> Dict:
- """Transform api request params into format which matches the signature of the endpoint."""
- return self.__dict__
-
-
-@attr.s
-class BaseSearchGetRequest(APIRequest):
- """Base arguments for GET Request."""
-
- collections: Optional[str] = attr.ib(default=None, converter=str2list)
- ids: Optional[str] = attr.ib(default=None, converter=str2list)
- bbox: Optional[str] = attr.ib(default=None, converter=str2list)
- intersects: Optional[str] = attr.ib(default=None, converter=str2list)
- datetime: Optional[str] = attr.ib(default=None)
- limit: Optional[int] = attr.ib(default=10)
-
-
-class BaseSearchPostRequest(BaseModel):
- """Search model.
-
- Replace base model in STAC-pydantic as it includes additional fields,
- not in the core model.
- https://github.com/radiantearth/stac-api-spec/tree/master/item-search#query-parameter-table
-
- PR to fix this:
- https://github.com/stac-utils/stac-pydantic/pull/100
- """
-
- collections: Optional[List[str]]
- ids: Optional[List[str]]
- bbox: Optional[BBox]
- intersects: Optional[
- Union[Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon]
- ]
- datetime: Optional[str]
- limit: Optional[conint(gt=0, le=10000)] = 10
-
- @property
- def start_date(self) -> Optional[datetime]:
- """Extract the start date from the datetime string."""
- interval = str_to_interval(self.datetime)
- return interval[0] if interval else None
-
- @property
- def end_date(self) -> Optional[datetime]:
- """Extract the end date from the datetime string."""
- interval = str_to_interval(self.datetime)
- return interval[1] if interval else None
-
- @validator("intersects")
- def validate_spatial(cls, v, values):
- """Check bbox and intersects are not both supplied."""
- if v and values["bbox"]:
- raise ValueError("intersects and bbox parameters are mutually exclusive")
- return v
-
- @validator("bbox")
- def validate_bbox(cls, v: BBox):
- """Check order of supplied bbox coordinates."""
- if v:
- # Validate order
- if len(v) == 4:
- xmin, ymin, xmax, ymax = v
- else:
- xmin, ymin, min_elev, xmax, ymax, max_elev = v
- if max_elev < min_elev:
- raise ValueError(
- "Maximum elevation must greater than minimum elevation"
- )
-
- if xmax < xmin:
- raise ValueError(
- "Maximum longitude must be greater than minimum longitude"
- )
-
- if ymax < ymin:
- raise ValueError(
- "Maximum longitude must be greater than minimum longitude"
- )
-
- # Validate against WGS84
- if xmin < -180 or ymin < -90 or xmax > 180 or ymax > 90:
- raise ValueError("Bounding box must be within (-180, -90, 180, 90)")
-
- return v
-
- @validator("datetime")
- def validate_datetime(cls, v):
- """Validate datetime."""
- if "/" in v:
- values = v.split("/")
- else:
- # Single date is interpreted as end date
- values = ["..", v]
-
- dates = []
- for value in values:
- if value == ".." or value == "":
- dates.append("..")
- continue
-
- # throws ValueError if invalid RFC 3339 string
- dates.append(rfc3339_str_to_datetime(value))
-
- if dates[0] == ".." and dates[1] == "..":
- raise ValueError(
- "Invalid datetime range, both ends of range may not be open"
- )
-
- if ".." not in dates and dates[0] > dates[1]:
- raise ValueError(
- "Invalid datetime range, must match format (begin_date, end_date)"
- )
-
- return v
-
- @property
- def spatial_filter(self) -> Optional[_GeometryBase]:
- """Return a geojson-pydantic object representing the spatial filter for the search request.
-
- Check for both because the ``bbox`` and ``intersects`` parameters are mutually exclusive.
- """
- if self.bbox:
- return Polygon(
- coordinates=[
- [
- [self.bbox[0], self.bbox[3]],
- [self.bbox[2], self.bbox[3]],
- [self.bbox[2], self.bbox[1]],
- [self.bbox[0], self.bbox[1]],
- [self.bbox[0], self.bbox[3]],
- ]
- ]
- )
- if self.intersects:
- return self.intersects
- return
diff --git a/stac_fastapi/types/stac_fastapi/types/stac.py b/stac_fastapi/types/stac_fastapi/types/stac.py
deleted file mode 100644
index ef61c2f..0000000
--- a/stac_fastapi/types/stac_fastapi/types/stac.py
+++ /dev/null
@@ -1,89 +0,0 @@
-"""STAC types."""
-import sys
-from typing import Any, Dict, List, Optional, Union
-
-# Avoids a Pydantic error:
-# TypeError: You should use `typing_extensions.TypedDict` instead of `typing.TypedDict` with Python < 3.9.2.
-# Without it, there is no way to differentiate required and optional fields when subclassed.
-if sys.version_info < (3, 9, 2):
- from typing_extensions import TypedDict
-else:
- from typing import TypedDict
-
-NumType = Union[float, int]
-
-
-class LandingPage(TypedDict, total=False):
- """STAC Landing Page."""
-
- type: str
- stac_version: str
- stac_extensions: Optional[List[str]]
- id: str
- title: str
- description: str
- conformsTo: List[str]
- links: List[Dict[str, Any]]
-
-
-class Conformance(TypedDict):
- """STAC Conformance Classes."""
-
- conformsTo: List[str]
-
-
-class Catalog(TypedDict, total=False):
- """STAC Catalog."""
-
- type: str
- stac_version: str
- stac_extensions: Optional[List[str]]
- id: str
- title: Optional[str]
- description: str
- links: List[Dict[str, Any]]
-
-
-class Collection(Catalog, total=False):
- """STAC Collection."""
-
- keywords: List[str]
- license: str
- providers: List[Dict[str, Any]]
- extent: Dict[str, Any]
- summaries: Dict[str, Any]
- assets: Dict[str, Any]
-
-
-class Item(TypedDict, total=False):
- """STAC Item."""
-
- type: str
- stac_version: str
- stac_extensions: Optional[List[str]]
- id: str
- geometry: Dict[str, Any]
- bbox: List[NumType]
- properties: Dict[str, Any]
- links: List[Dict[str, Any]]
- assets: Dict[str, Any]
- collection: str
-
-
-class ItemCollection(TypedDict, total=False):
- """STAC Item Collection."""
-
- type: str
- features: List[Item]
- links: List[Dict[str, Any]]
- context: Optional[Dict[str, int]]
-
-
-class Collections(TypedDict, total=False):
- """All collections endpoint.
-
- https://github.com/radiantearth/stac-api-spec/tree/master/collections
- """
-
- collections: List[Collection]
- links: List[Dict[str, Any]]
diff --git a/stac_fastapi/types/stac_fastapi/types/version.py b/stac_fastapi/types/stac_fastapi/types/version.py
deleted file mode 100644
index 895f63a..0000000
--- a/stac_fastapi/types/stac_fastapi/types/version.py
+++ /dev/null
@@ -1,2 +0,0 @@
-"""library version."""
-__version__ = "2.4.1"
diff --git a/stac_fastapi/types/tests/test_rfc3339.py b/stac_fastapi/types/tests/test_rfc3339.py
deleted file mode 100644
index 0a40269..0000000
--- a/stac_fastapi/types/tests/test_rfc3339.py
+++ /dev/null
@@ -1,105 +0,0 @@
-from datetime import timezone
-
-import pytest
-
-from stac_fastapi.types.rfc3339 import (
- now_in_utc,
- now_to_rfc3339_str,
- rfc3339_str_to_datetime,
- str_to_interval,
-)
-
-invalid_datetimes = [
- "1985-04-12", # date only
- "1937-01-01T12:00:27.87+0100", # invalid TZ format, no sep :
- "37-01-01T12:00:27.87Z", # invalid year, must be 4 digits
- "1985-12-12T23:20:50.52", # no TZ
- "21985-12-12T23:20:50.52Z", # year must be 4 digits
- "1985-13-12T23:20:50.52Z", # month > 12
- "1985-12-32T23:20:50.52Z", # day > 31
- "1985-12-01T25:20:50.52Z", # hour > 24
- "1985-12-01T00:60:50.52Z", # minute > 59
- "1985-12-01T00:06:61.52Z", # second > 60
- "1985-04-12T23:20:50.Z", # fractional sec . but no frac secs
- "1985-04-12T23:20:50,Z", # fractional sec , but no frac secs
- "1990-12-31T23:59:61Z", # second > 60 w/o fractional seconds
- "1985-04-12T23:20:50,52Z", # comma as frac sec sep allowed in ISO8601 but not RFC3339
-]
-
-valid_datetimes = [
- "1985-04-12T23:20:50.52Z",
- "1996-12-19T16:39:57-00:00",
- "1996-12-19T16:39:57+00:00",
- "1996-12-19T16:39:57-08:00",
- "1996-12-19T16:39:57+08:00",
- "1937-01-01T12:00:27.87+01:00",
- "1985-04-12T23:20:50.52Z",
- "1937-01-01T12:00:27.8710+01:00",
- "1937-01-01T12:00:27.8+01:00",
- "1937-01-01T12:00:27.8Z",
- "2020-07-23T00:00:00.000+03:00",
- "2020-07-23T00:00:00+03:00",
- "1985-04-12t23:20:50.000z",
- "2020-07-23T00:00:00Z",
- "2020-07-23T00:00:00.0Z",
- "2020-07-23T00:00:00.01Z",
- "2020-07-23T00:00:00.012Z",
- "2020-07-23T00:00:00.0123Z",
- "2020-07-23T00:00:00.01234Z",
- "2020-07-23T00:00:00.012345Z",
- "2020-07-23T00:00:00.0123456Z",
- "2020-07-23T00:00:00.01234567Z",
- "2020-07-23T00:00:00.012345678Z",
-]
-
-invalid_intervals = [
- "/"
- "../"
- "/.."
- "../.."
- "/1984-04-12T23:20:50.52Z/1985-04-12T23:20:50.52Z", # extra start /
- "1984-04-12T23:20:50.52Z/1985-04-12T23:20:50.52Z/", # extra end /
- "1986-04-12T23:20:50.52Z/1985-04-12T23:20:50.52Z", # start > end
-]
-
-valid_intervals = [
- "../1985-04-12T23:20:50.52Z",
- "1985-04-12T23:20:50.52Z/..",
- "/1985-04-12T23:20:50.52Z",
- "1985-04-12T23:20:50.52Z/",
- "1985-04-12T23:20:50.52Z/1986-04-12T23:20:50.52Z",
- "1985-04-12T23:20:50.52+01:00/1986-04-12T23:20:50.52+01:00",
- "1985-04-12T23:20:50.52-01:00/1986-04-12T23:20:50.52-01:00",
-]
-
-
-@pytest.mark.parametrize("test_input", invalid_datetimes)
-def test_parse_invalid_str_to_datetime(test_input):
- with pytest.raises(ValueError):
- rfc3339_str_to_datetime(test_input)
-
-
-@pytest.mark.parametrize("test_input", valid_datetimes)
-def test_parse_valid_str_to_datetime(test_input):
- assert rfc3339_str_to_datetime(test_input)
-
-
-@pytest.mark.parametrize("test_input", invalid_intervals)
-def test_parse_invalid_interval_to_datetime(test_input):
- with pytest.raises(ValueError):
- str_to_interval(test_input)
-
-
-@pytest.mark.parametrize("test_input", valid_intervals)
-def test_parse_valid_interval_to_datetime(test_input):
- assert str_to_interval(test_input)
-
-
-def test_now_functions() -> None:
- now1 = now_in_utc()
- now2 = now_in_utc()
-
- assert now1 < now2
- assert now1.tzinfo == timezone.utc
-
- rfc3339_str_to_datetime(now_to_rfc3339_str())
From adf7471c42583923d2e5cd355f747ab0550064ed Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Wed, 10 Aug 2022 14:08:49 -0600
Subject: [PATCH 02/21] remove pgstac from docker/make, local dev working
---
Dockerfile | 6 +---
Makefile | 70 ++++++++++------------------------------------
docker-compose.yml | 63 ++++-------------------------------------
3 files changed, 21 insertions(+), 118 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 5c218e2..affe199 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -16,8 +16,4 @@ WORKDIR /app
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]
+RUN pip install -e ./stac_fastapi/sqlalchemy[dev,server]
diff --git a/Makefile b/Makefile
index 36187c2..9cbd9a4 100644
--- a/Makefile
+++ b/Makefile
@@ -2,17 +2,11 @@
APP_HOST ?= 0.0.0.0
APP_PORT ?= 8080
EXTERNAL_APP_PORT ?= ${APP_PORT}
-run_sqlalchemy = docker-compose run --rm \
+run_container = 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
+ stac-fastapi
.PHONY: image
image:
@@ -22,63 +16,27 @@ image:
docker-run-all:
docker-compose up
-.PHONY: docker-run-sqlalchemy
-docker-run-sqlalchemy: image
- $(run_sqlalchemy)
+.PHONY: docker-run
+docker-run: image
+ $(run_container)
-.PHONY: docker-run-pgstac
-docker-run-pgstac: image
- $(run_pgstac)
+.PHONY: docker-shell
+docker-shell:
+ $(run_container) /bin/bash
-.PHONY: docker-shell-sqlalchemy
-docker-shell-sqlalchemy:
- $(run_sqlalchemy) /bin/bash
-.PHONY: docker-shell-pgstac
-docker-shell-pgstac:
- $(run_pgstac) /bin/bash
-
-.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'
-
-.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'
+.PHONY: test
+test: run-joplin
+ $(run_container) /bin/bash -c 'export && ./scripts/wait-for-it.sh database:5432 && cd /app/stac_fastapi/sqlalchemy/tests/ && pytest -vvv'
-.PHONY: test-api
-test-api:
- $(run_sqlalchemy) /bin/bash -c 'cd /app/stac_fastapi/api && pytest -svvv'
.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:
- 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: run-joplin
+run-joplin:
+ docker-compose run --rm load-joplin
.PHONY: docs-image
docs-image:
diff --git a/docker-compose.yml b/docker-compose.yml
index 6c79e5e..05ff879 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,7 +1,7 @@
version: '3'
services:
- app-sqlalchemy:
- container_name: stac-fastapi-sqlalchemy
+ stac-fastapi:
+ container_name: stac-fastapi
image: stac-utils/stac-fastapi
build:
context: .
@@ -29,40 +29,8 @@ services:
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
+ container_name: postgres
image: ghcr.io/stac-utils/pgstac:v0.6.6
environment:
- POSTGRES_USER=username
@@ -77,7 +45,7 @@ services:
command: postgres -N 500
# Load joplin demo dataset into the SQLAlchemy Application
- loadjoplin-sqlalchemy:
+ load-joplin:
image: stac-utils/stac-fastapi
environment:
- ENVIRONMENT=development
@@ -90,29 +58,10 @@ services:
- ./stac_fastapi:/app/stac_fastapi
- ./scripts:/app/scripts
command: >
- bash -c "./scripts/wait-for-it.sh app-sqlalchemy:8081 && 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"
- - "app-pgstac:8082"
- - "--"
- - "python"
- - "/app/scripts/ingest_joplin.py"
- - "http://app-pgstac:8082"
+ bash -c "./scripts/wait-for-it.sh stac-fastapi:8081 && cd stac_fastapi/sqlalchemy && alembic upgrade head && python /app/scripts/ingest_joplin.py http://stac-fastapi:8081"
depends_on:
- database
- - app-pgstac
+ - stac-fastapi
networks:
default:
From 2b34e16cbde5639f84f905a58db851d316ea6f77 Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Wed, 10 Aug 2022 14:11:21 -0600
Subject: [PATCH 03/21] move python packaging, alembic, and tests to top level
of repo
---
stac_fastapi/sqlalchemy/alembic.ini => alembic.ini | 0
{stac_fastapi/sqlalchemy/alembic => alembic}/README | 0
{stac_fastapi/sqlalchemy/alembic => alembic}/env.py | 0
{stac_fastapi/sqlalchemy/alembic => alembic}/script.py.mako | 0
.../alembic => alembic}/versions/131aab4d9e49_create_tables.py | 0
.../versions/407037cb1636_add_stac_1_0_0_fields.py | 0
.../versions/5909bd10f2e6_change_item_geometry_column_type.py | 0
.../7016c1bf3fbf_make_item_geometry_and_bbox_nullable.py | 0
.../77c019af60bf_use_timestamptz_rather_than_timestamp.py | 0
.../versions/821aa04011e8_change_pri_key_for_item.py | 0
stac_fastapi/sqlalchemy/pytest.ini => pytest.ini | 0
stac_fastapi/sqlalchemy/setup.cfg => setup.cfg | 0
stac_fastapi/sqlalchemy/setup.py => setup.py | 0
stac_fastapi/sqlalchemy/README.md | 3 ---
{stac_fastapi/sqlalchemy/tests => tests}/__init__.py | 0
{stac_fastapi/sqlalchemy/tests => tests}/api/__init__.py | 0
{stac_fastapi/sqlalchemy/tests => tests}/api/test_api.py | 0
{stac_fastapi/sqlalchemy/tests => tests}/clients/__init__.py | 0
.../sqlalchemy/tests => tests}/clients/test_postgres.py | 0
{stac_fastapi/sqlalchemy/tests => tests}/conftest.py | 0
.../sqlalchemy/tests => tests}/data/test_collection.json | 0
{stac_fastapi/sqlalchemy/tests => tests}/data/test_item.json | 0
.../tests => tests}/data/test_item_geometry_null.json | 0
.../tests => tests}/data/test_item_multipolygon.json | 0
{stac_fastapi/sqlalchemy/tests => tests}/features/__init__.py | 0
.../sqlalchemy/tests => tests}/features/test_custom_models.py | 0
{stac_fastapi/sqlalchemy/tests => tests}/resources/__init__.py | 0
.../sqlalchemy/tests => tests}/resources/test_collection.py | 0
.../sqlalchemy/tests => tests}/resources/test_conformance.py | 0
.../sqlalchemy/tests => tests}/resources/test_item.py | 0
.../sqlalchemy/tests => tests}/resources/test_mgmt.py | 0
31 files changed, 3 deletions(-)
rename stac_fastapi/sqlalchemy/alembic.ini => alembic.ini (100%)
rename {stac_fastapi/sqlalchemy/alembic => alembic}/README (100%)
rename {stac_fastapi/sqlalchemy/alembic => alembic}/env.py (100%)
rename {stac_fastapi/sqlalchemy/alembic => alembic}/script.py.mako (100%)
rename {stac_fastapi/sqlalchemy/alembic => alembic}/versions/131aab4d9e49_create_tables.py (100%)
rename {stac_fastapi/sqlalchemy/alembic => alembic}/versions/407037cb1636_add_stac_1_0_0_fields.py (100%)
rename {stac_fastapi/sqlalchemy/alembic => alembic}/versions/5909bd10f2e6_change_item_geometry_column_type.py (100%)
rename {stac_fastapi/sqlalchemy/alembic => alembic}/versions/7016c1bf3fbf_make_item_geometry_and_bbox_nullable.py (100%)
rename {stac_fastapi/sqlalchemy/alembic => alembic}/versions/77c019af60bf_use_timestamptz_rather_than_timestamp.py (100%)
rename {stac_fastapi/sqlalchemy/alembic => alembic}/versions/821aa04011e8_change_pri_key_for_item.py (100%)
rename stac_fastapi/sqlalchemy/pytest.ini => pytest.ini (100%)
rename stac_fastapi/sqlalchemy/setup.cfg => setup.cfg (100%)
rename stac_fastapi/sqlalchemy/setup.py => setup.py (100%)
delete mode 100644 stac_fastapi/sqlalchemy/README.md
rename {stac_fastapi/sqlalchemy/tests => tests}/__init__.py (100%)
rename {stac_fastapi/sqlalchemy/tests => tests}/api/__init__.py (100%)
rename {stac_fastapi/sqlalchemy/tests => tests}/api/test_api.py (100%)
rename {stac_fastapi/sqlalchemy/tests => tests}/clients/__init__.py (100%)
rename {stac_fastapi/sqlalchemy/tests => tests}/clients/test_postgres.py (100%)
rename {stac_fastapi/sqlalchemy/tests => tests}/conftest.py (100%)
rename {stac_fastapi/sqlalchemy/tests => tests}/data/test_collection.json (100%)
rename {stac_fastapi/sqlalchemy/tests => tests}/data/test_item.json (100%)
rename {stac_fastapi/sqlalchemy/tests => tests}/data/test_item_geometry_null.json (100%)
rename {stac_fastapi/sqlalchemy/tests => tests}/data/test_item_multipolygon.json (100%)
rename {stac_fastapi/sqlalchemy/tests => tests}/features/__init__.py (100%)
rename {stac_fastapi/sqlalchemy/tests => tests}/features/test_custom_models.py (100%)
rename {stac_fastapi/sqlalchemy/tests => tests}/resources/__init__.py (100%)
rename {stac_fastapi/sqlalchemy/tests => tests}/resources/test_collection.py (100%)
rename {stac_fastapi/sqlalchemy/tests => tests}/resources/test_conformance.py (100%)
rename {stac_fastapi/sqlalchemy/tests => tests}/resources/test_item.py (100%)
rename {stac_fastapi/sqlalchemy/tests => tests}/resources/test_mgmt.py (100%)
diff --git a/stac_fastapi/sqlalchemy/alembic.ini b/alembic.ini
similarity index 100%
rename from stac_fastapi/sqlalchemy/alembic.ini
rename to alembic.ini
diff --git a/stac_fastapi/sqlalchemy/alembic/README b/alembic/README
similarity index 100%
rename from stac_fastapi/sqlalchemy/alembic/README
rename to alembic/README
diff --git a/stac_fastapi/sqlalchemy/alembic/env.py b/alembic/env.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/alembic/env.py
rename to alembic/env.py
diff --git a/stac_fastapi/sqlalchemy/alembic/script.py.mako b/alembic/script.py.mako
similarity index 100%
rename from stac_fastapi/sqlalchemy/alembic/script.py.mako
rename to alembic/script.py.mako
diff --git a/stac_fastapi/sqlalchemy/alembic/versions/131aab4d9e49_create_tables.py b/alembic/versions/131aab4d9e49_create_tables.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/alembic/versions/131aab4d9e49_create_tables.py
rename to alembic/versions/131aab4d9e49_create_tables.py
diff --git a/stac_fastapi/sqlalchemy/alembic/versions/407037cb1636_add_stac_1_0_0_fields.py b/alembic/versions/407037cb1636_add_stac_1_0_0_fields.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/alembic/versions/407037cb1636_add_stac_1_0_0_fields.py
rename to alembic/versions/407037cb1636_add_stac_1_0_0_fields.py
diff --git a/stac_fastapi/sqlalchemy/alembic/versions/5909bd10f2e6_change_item_geometry_column_type.py b/alembic/versions/5909bd10f2e6_change_item_geometry_column_type.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/alembic/versions/5909bd10f2e6_change_item_geometry_column_type.py
rename to alembic/versions/5909bd10f2e6_change_item_geometry_column_type.py
diff --git a/stac_fastapi/sqlalchemy/alembic/versions/7016c1bf3fbf_make_item_geometry_and_bbox_nullable.py b/alembic/versions/7016c1bf3fbf_make_item_geometry_and_bbox_nullable.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/alembic/versions/7016c1bf3fbf_make_item_geometry_and_bbox_nullable.py
rename to alembic/versions/7016c1bf3fbf_make_item_geometry_and_bbox_nullable.py
diff --git a/stac_fastapi/sqlalchemy/alembic/versions/77c019af60bf_use_timestamptz_rather_than_timestamp.py b/alembic/versions/77c019af60bf_use_timestamptz_rather_than_timestamp.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/alembic/versions/77c019af60bf_use_timestamptz_rather_than_timestamp.py
rename to alembic/versions/77c019af60bf_use_timestamptz_rather_than_timestamp.py
diff --git a/stac_fastapi/sqlalchemy/alembic/versions/821aa04011e8_change_pri_key_for_item.py b/alembic/versions/821aa04011e8_change_pri_key_for_item.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/alembic/versions/821aa04011e8_change_pri_key_for_item.py
rename to alembic/versions/821aa04011e8_change_pri_key_for_item.py
diff --git a/stac_fastapi/sqlalchemy/pytest.ini b/pytest.ini
similarity index 100%
rename from stac_fastapi/sqlalchemy/pytest.ini
rename to pytest.ini
diff --git a/stac_fastapi/sqlalchemy/setup.cfg b/setup.cfg
similarity index 100%
rename from stac_fastapi/sqlalchemy/setup.cfg
rename to setup.cfg
diff --git a/stac_fastapi/sqlalchemy/setup.py b/setup.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/setup.py
rename to setup.py
diff --git a/stac_fastapi/sqlalchemy/README.md b/stac_fastapi/sqlalchemy/README.md
deleted file mode 100644
index 40bd804..0000000
--- 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/tests/__init__.py b/tests/__init__.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/tests/__init__.py
rename to tests/__init__.py
diff --git a/stac_fastapi/sqlalchemy/tests/api/__init__.py b/tests/api/__init__.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/tests/api/__init__.py
rename to tests/api/__init__.py
diff --git a/stac_fastapi/sqlalchemy/tests/api/test_api.py b/tests/api/test_api.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/tests/api/test_api.py
rename to tests/api/test_api.py
diff --git a/stac_fastapi/sqlalchemy/tests/clients/__init__.py b/tests/clients/__init__.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/tests/clients/__init__.py
rename to tests/clients/__init__.py
diff --git a/stac_fastapi/sqlalchemy/tests/clients/test_postgres.py b/tests/clients/test_postgres.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/tests/clients/test_postgres.py
rename to tests/clients/test_postgres.py
diff --git a/stac_fastapi/sqlalchemy/tests/conftest.py b/tests/conftest.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/tests/conftest.py
rename to tests/conftest.py
diff --git a/stac_fastapi/sqlalchemy/tests/data/test_collection.json b/tests/data/test_collection.json
similarity index 100%
rename from stac_fastapi/sqlalchemy/tests/data/test_collection.json
rename to tests/data/test_collection.json
diff --git a/stac_fastapi/sqlalchemy/tests/data/test_item.json b/tests/data/test_item.json
similarity index 100%
rename from stac_fastapi/sqlalchemy/tests/data/test_item.json
rename to tests/data/test_item.json
diff --git a/stac_fastapi/sqlalchemy/tests/data/test_item_geometry_null.json b/tests/data/test_item_geometry_null.json
similarity index 100%
rename from stac_fastapi/sqlalchemy/tests/data/test_item_geometry_null.json
rename to tests/data/test_item_geometry_null.json
diff --git a/stac_fastapi/sqlalchemy/tests/data/test_item_multipolygon.json b/tests/data/test_item_multipolygon.json
similarity index 100%
rename from stac_fastapi/sqlalchemy/tests/data/test_item_multipolygon.json
rename to tests/data/test_item_multipolygon.json
diff --git a/stac_fastapi/sqlalchemy/tests/features/__init__.py b/tests/features/__init__.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/tests/features/__init__.py
rename to tests/features/__init__.py
diff --git a/stac_fastapi/sqlalchemy/tests/features/test_custom_models.py b/tests/features/test_custom_models.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/tests/features/test_custom_models.py
rename to tests/features/test_custom_models.py
diff --git a/stac_fastapi/sqlalchemy/tests/resources/__init__.py b/tests/resources/__init__.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/tests/resources/__init__.py
rename to tests/resources/__init__.py
diff --git a/stac_fastapi/sqlalchemy/tests/resources/test_collection.py b/tests/resources/test_collection.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/tests/resources/test_collection.py
rename to tests/resources/test_collection.py
diff --git a/stac_fastapi/sqlalchemy/tests/resources/test_conformance.py b/tests/resources/test_conformance.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/tests/resources/test_conformance.py
rename to tests/resources/test_conformance.py
diff --git a/stac_fastapi/sqlalchemy/tests/resources/test_item.py b/tests/resources/test_item.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/tests/resources/test_item.py
rename to tests/resources/test_item.py
diff --git a/stac_fastapi/sqlalchemy/tests/resources/test_mgmt.py b/tests/resources/test_mgmt.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/tests/resources/test_mgmt.py
rename to tests/resources/test_mgmt.py
From 51b604633a4ffde9dea84c4ffb895fd9d130d1df Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Wed, 10 Aug 2022 14:12:28 -0600
Subject: [PATCH 04/21] squash the package
---
stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/__init__.py | 0
stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/app.py | 0
stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/config.py | 0
stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/core.py | 0
.../{stac_fastapi/sqlalchemy => }/extensions/__init__.py | 0
.../sqlalchemy/{stac_fastapi/sqlalchemy => }/extensions/query.py | 0
.../sqlalchemy/{stac_fastapi/sqlalchemy => }/models/__init__.py | 0
.../sqlalchemy/{stac_fastapi/sqlalchemy => }/models/database.py | 0
.../sqlalchemy/{stac_fastapi/sqlalchemy => }/models/search.py | 0
.../sqlalchemy/{stac_fastapi/sqlalchemy => }/serializers.py | 0
stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/session.py | 0
stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/tokens.py | 0
.../sqlalchemy/{stac_fastapi/sqlalchemy => }/transactions.py | 0
stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/version.py | 0
14 files changed, 0 insertions(+), 0 deletions(-)
rename stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/__init__.py (100%)
rename stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/app.py (100%)
rename stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/config.py (100%)
rename stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/core.py (100%)
rename stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/extensions/__init__.py (100%)
rename stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/extensions/query.py (100%)
rename stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/models/__init__.py (100%)
rename stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/models/database.py (100%)
rename stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/models/search.py (100%)
rename stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/serializers.py (100%)
rename stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/session.py (100%)
rename stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/tokens.py (100%)
rename stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/transactions.py (100%)
rename stac_fastapi/sqlalchemy/{stac_fastapi/sqlalchemy => }/version.py (100%)
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/__init__.py b/stac_fastapi/sqlalchemy/__init__.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/__init__.py
rename to stac_fastapi/sqlalchemy/__init__.py
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/app.py b/stac_fastapi/sqlalchemy/app.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/app.py
rename to stac_fastapi/sqlalchemy/app.py
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/config.py b/stac_fastapi/sqlalchemy/config.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/config.py
rename to stac_fastapi/sqlalchemy/config.py
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/core.py b/stac_fastapi/sqlalchemy/core.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/core.py
rename to stac_fastapi/sqlalchemy/core.py
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/extensions/__init__.py b/stac_fastapi/sqlalchemy/extensions/__init__.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/extensions/__init__.py
rename to stac_fastapi/sqlalchemy/extensions/__init__.py
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/extensions/query.py b/stac_fastapi/sqlalchemy/extensions/query.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/extensions/query.py
rename to stac_fastapi/sqlalchemy/extensions/query.py
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/__init__.py b/stac_fastapi/sqlalchemy/models/__init__.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/__init__.py
rename to stac_fastapi/sqlalchemy/models/__init__.py
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/database.py b/stac_fastapi/sqlalchemy/models/database.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/database.py
rename to stac_fastapi/sqlalchemy/models/database.py
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/search.py b/stac_fastapi/sqlalchemy/models/search.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/models/search.py
rename to stac_fastapi/sqlalchemy/models/search.py
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/serializers.py b/stac_fastapi/sqlalchemy/serializers.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/serializers.py
rename to stac_fastapi/sqlalchemy/serializers.py
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/session.py b/stac_fastapi/sqlalchemy/session.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/session.py
rename to stac_fastapi/sqlalchemy/session.py
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/tokens.py b/stac_fastapi/sqlalchemy/tokens.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/tokens.py
rename to stac_fastapi/sqlalchemy/tokens.py
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/transactions.py b/stac_fastapi/sqlalchemy/transactions.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/transactions.py
rename to stac_fastapi/sqlalchemy/transactions.py
diff --git a/stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/version.py b/stac_fastapi/sqlalchemy/version.py
similarity index 100%
rename from stac_fastapi/sqlalchemy/stac_fastapi/sqlalchemy/version.py
rename to stac_fastapi/sqlalchemy/version.py
From 2993d4f95006be4fd7b59aa5913b1cee7c894f98 Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Wed, 10 Aug 2022 14:17:02 -0600
Subject: [PATCH 05/21] update paths in dockerfile to new repo structure
---
Dockerfile | 2 +-
docker-compose.yml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index affe199..1bd8331 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -16,4 +16,4 @@ WORKDIR /app
COPY . /app
-RUN pip install -e ./stac_fastapi/sqlalchemy[dev,server]
+RUN pip install -e .[dev,server]
diff --git a/docker-compose.yml b/docker-compose.yml
index 05ff879..20fcf6a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -58,7 +58,7 @@ services:
- ./stac_fastapi:/app/stac_fastapi
- ./scripts:/app/scripts
command: >
- bash -c "./scripts/wait-for-it.sh stac-fastapi:8081 && cd stac_fastapi/sqlalchemy && alembic upgrade head && python /app/scripts/ingest_joplin.py http://stac-fastapi:8081"
+ bash -c "./scripts/wait-for-it.sh stac-fastapi:8081 && alembic upgrade head && python /app/scripts/ingest_joplin.py http://stac-fastapi:8081"
depends_on:
- database
- stac-fastapi
From 06620419ef7f54720618920278348b14a77c00e2 Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Wed, 10 Aug 2022 14:19:04 -0600
Subject: [PATCH 06/21] fix paths when running tests locally
---
Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index 9cbd9a4..9d64425 100644
--- a/Makefile
+++ b/Makefile
@@ -27,7 +27,7 @@ docker-shell:
.PHONY: test
test: run-joplin
- $(run_container) /bin/bash -c 'export && ./scripts/wait-for-it.sh database:5432 && cd /app/stac_fastapi/sqlalchemy/tests/ && pytest -vvv'
+ $(run_container) /bin/bash -c 'export && ./scripts/wait-for-it.sh database:5432 && pytest -vvv'
.PHONY: run-database
From 131690d257eac7a92d80ffb0e9ef11a383b1e5a0 Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Wed, 10 Aug 2022 14:26:18 -0600
Subject: [PATCH 07/21] move joplin data to tests/ folder
---
scripts/ingest_joplin.py | 2 +-
{stac_fastapi/testdata => tests/data}/joplin/collection.json | 0
{stac_fastapi/testdata => tests/data}/joplin/feature.geojson | 0
{stac_fastapi/testdata => tests/data}/joplin/index.geojson | 0
4 files changed, 1 insertion(+), 1 deletion(-)
rename {stac_fastapi/testdata => tests/data}/joplin/collection.json (100%)
rename {stac_fastapi/testdata => tests/data}/joplin/feature.geojson (100%)
rename {stac_fastapi/testdata => tests/data}/joplin/index.geojson (100%)
diff --git a/scripts/ingest_joplin.py b/scripts/ingest_joplin.py
index 76320fa..5e41ef5 100644
--- a/scripts/ingest_joplin.py
+++ b/scripts/ingest_joplin.py
@@ -7,7 +7,7 @@
import requests
workingdir = Path(__file__).parent.absolute()
-joplindata = workingdir.parent / "stac_fastapi" / "testdata" / "joplin"
+joplindata = workingdir.parent / "tests" / "data" / "joplin"
app_host = sys.argv[1]
diff --git a/stac_fastapi/testdata/joplin/collection.json b/tests/data/joplin/collection.json
similarity index 100%
rename from stac_fastapi/testdata/joplin/collection.json
rename to tests/data/joplin/collection.json
diff --git a/stac_fastapi/testdata/joplin/feature.geojson b/tests/data/joplin/feature.geojson
similarity index 100%
rename from stac_fastapi/testdata/joplin/feature.geojson
rename to tests/data/joplin/feature.geojson
diff --git a/stac_fastapi/testdata/joplin/index.geojson b/tests/data/joplin/index.geojson
similarity index 100%
rename from stac_fastapi/testdata/joplin/index.geojson
rename to tests/data/joplin/index.geojson
From 34e7e223cd4a0321a6d692ef90568468828f3254 Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Wed, 10 Aug 2022 14:29:27 -0600
Subject: [PATCH 08/21] update github ci
---
.github/workflows/cicd.yaml | 51 +++----------------------------------
1 file changed, 4 insertions(+), 47 deletions(-)
diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml
index f4a4041..f82d283 100644
--- a/.github/workflows/cicd.yaml
+++ b/.github/workflows/cicd.yaml
@@ -46,33 +46,13 @@ jobs:
- name: Lint code
uses: pre-commit/action@v2.0.0
- - name: Install pipenv
+ - name: Install sqlalchemy backend
run: |
- python -m pip install --upgrade pipenv wheel
-
- - name: Install types
- run: |
- pip install ./stac_fastapi/types[dev]
-
- - name: Install core api
- run: |
- pip install ./stac_fastapi/api[dev]
-
- - name: Install Extensions
- 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]
+ pip install .[dev,server]
- name: Run migration
run: |
- cd stac_fastapi/sqlalchemy && alembic upgrade head
+ alembic upgrade head
env:
POSTGRES_USER: username
POSTGRES_PASS: password
@@ -82,19 +62,7 @@ jobs:
- 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
+ pytest -svvv
env:
ENVIRONMENT: testing
POSTGRES_USER: username
@@ -104,17 +72,6 @@ jobs:
POSTGRES_HOST_WRITER: localhost
POSTGRES_PORT: 5432
- - name: Run test suite
- run: |
- cd stac_fastapi/pgstac && 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
test-docs:
runs-on: ubuntu-latest
steps:
From d4cec7244242dded581ea79df4644f277a8d80a2 Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Wed, 10 Aug 2022 14:30:42 -0600
Subject: [PATCH 09/21] change ci target branch to main
---
.github/workflows/cicd.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml
index f82d283..4702c88 100644
--- a/.github/workflows/cicd.yaml
+++ b/.github/workflows/cicd.yaml
@@ -1,9 +1,9 @@
name: stac-fastapi
on:
push:
- branches: [ master ]
+ branches: [ main ]
pull_request:
- branches: [ master ]
+ branches: [ main ]
jobs:
test:
From 88045a6b6e6935fc0f5df25e5115ac842eb03ad2 Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Wed, 10 Aug 2022 14:35:17 -0600
Subject: [PATCH 10/21] run pre-commit
---
alembic/env.py | 3 ++-
alembic/versions/131aab4d9e49_create_tables.py | 3 ++-
alembic/versions/407037cb1636_add_stac_1_0_0_fields.py | 1 +
.../versions/5909bd10f2e6_change_item_geometry_column_type.py | 1 -
tests/clients/test_postgres.py | 2 +-
5 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/alembic/env.py b/alembic/env.py
index 20af555..6c5aa82 100644
--- a/alembic/env.py
+++ b/alembic/env.py
@@ -2,9 +2,10 @@
import os
from logging.config import fileConfig
-from alembic import context
from sqlalchemy import engine_from_config, pool
+from alembic import context
+
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
diff --git a/alembic/versions/131aab4d9e49_create_tables.py b/alembic/versions/131aab4d9e49_create_tables.py
index efc3338..c4e97ca 100644
--- a/alembic/versions/131aab4d9e49_create_tables.py
+++ b/alembic/versions/131aab4d9e49_create_tables.py
@@ -6,10 +6,11 @@
""" # noqa
import sqlalchemy as sa
-from alembic import op
from geoalchemy2.types import Geometry
from sqlalchemy.dialects.postgresql import JSONB
+from alembic import op
+
# revision identifiers, used by Alembic.
revision = "131aab4d9e49"
down_revision = None
diff --git a/alembic/versions/407037cb1636_add_stac_1_0_0_fields.py b/alembic/versions/407037cb1636_add_stac_1_0_0_fields.py
index fdf15cd..db997ab 100644
--- a/alembic/versions/407037cb1636_add_stac_1_0_0_fields.py
+++ b/alembic/versions/407037cb1636_add_stac_1_0_0_fields.py
@@ -6,6 +6,7 @@
"""
import sqlalchemy as sa
+
from alembic import op
# revision identifiers, used by Alembic.
diff --git a/alembic/versions/5909bd10f2e6_change_item_geometry_column_type.py b/alembic/versions/5909bd10f2e6_change_item_geometry_column_type.py
index 2c1edd9..2e4a937 100644
--- a/alembic/versions/5909bd10f2e6_change_item_geometry_column_type.py
+++ b/alembic/versions/5909bd10f2e6_change_item_geometry_column_type.py
@@ -6,7 +6,6 @@
"""
from alembic import op
-
from stac_fastapi.sqlalchemy.models.database import GeojsonGeometry
# revision identifiers, used by Alembic.
diff --git a/tests/clients/test_postgres.py b/tests/clients/test_postgres.py
index da69c78..9e7e9af 100644
--- a/tests/clients/test_postgres.py
+++ b/tests/clients/test_postgres.py
@@ -4,7 +4,6 @@
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
@@ -14,6 +13,7 @@
TransactionsClient,
)
from stac_fastapi.types.errors import ConflictError, NotFoundError
+from tests.conftest import MockStarletteRequest
def test_create_collection(
From 2627d18b60dab490b345a330e96056324a01d4fe Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Wed, 10 Aug 2022 14:45:13 -0600
Subject: [PATCH 11/21] add pystac validation to dev dependencies
---
setup.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/setup.py b/setup.py
index c7d6e98..46a2721 100644
--- a/setup.py
+++ b/setup.py
@@ -27,6 +27,7 @@
"pytest-cov",
"pre-commit",
"requests",
+ "pystac[validation]==1.*",
],
"docs": ["mkdocs", "mkdocs-material", "pdocs"],
"server": ["uvicorn[standard]==0.17.0"],
From 472540e05aa094f1258ceb8579343f7e6c8e0d3a Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Wed, 10 Aug 2022 15:26:58 -0600
Subject: [PATCH 12/21] fix building of docs, simplify a little
---
Dockerfile.docs | 8 ++------
Makefile | 14 ++++++++++----
docker-compose.docs.yml | 18 ------------------
mkdocs.yml | 37 ++++---------------------------------
4 files changed, 16 insertions(+), 61 deletions(-)
delete mode 100644 docker-compose.docs.yml
diff --git a/Dockerfile.docs b/Dockerfile.docs
index f145b31..9300c68 100644
--- a/Dockerfile.docs
+++ b/Dockerfile.docs
@@ -10,11 +10,7 @@ COPY . /opt/src
WORKDIR /opt/src
-RUN python -m pip install \
- stac_fastapi/api \
- stac_fastapi/types \
- stac_fastapi/extensions \
- stac_fastapi/sqlalchemy
+RUN python -m pip install .
CMD ["pdocs", \
"as_markdown", \
@@ -22,4 +18,4 @@ CMD ["pdocs", \
"docs/api/", \
"--exclude_source", \
"--overwrite", \
- "stac_fastapi"]
\ No newline at end of file
+ "stac_fastapi.sqlalchemy"]
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 9d64425..fed00fe 100644
--- a/Makefile
+++ b/Makefile
@@ -40,10 +40,16 @@ run-joplin:
.PHONY: docs-image
docs-image:
- docker-compose -f docker-compose.docs.yml \
- build
+ docker build . -f Dockerfile.docs -t stac-fastapi-sqlalchemy-docs:latest
.PHONY: docs
docs: docs-image
- docker-compose -f docker-compose.docs.yml \
- run docs
+ docker run --rm \
+ -v ${CURDIR}/:/opt/src \
+ -e POSTGRES_USER=username \
+ -e POSTGRES_PASS=password \
+ -e POSTGRES_DBNAME=postgis \
+ -e POSTGRES_HOST_READER=database \
+ -e POSTGRES_HOST_WRITER=database \
+ -e POSTGRES_PORT=5432 \
+ stac-fastapi-sqlalchemy-docs:latest
\ No newline at end of file
diff --git a/docker-compose.docs.yml b/docker-compose.docs.yml
deleted file mode 100644
index 9c441f1..0000000
--- a/docker-compose.docs.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-version: '3'
-
-services:
- docs:
- container_name: stac-fastapi-docs-dev
- build:
- context: .
- dockerfile: Dockerfile.docs
- platform: linux/amd64
- environment:
- - POSTGRES_USER=username
- - POSTGRES_PASS=password
- - POSTGRES_DBNAME=postgis
- - POSTGRES_HOST_READER=database
- - POSTGRES_HOST_WRITER=database
- - POSTGRES_PORT=5432
- volumes:
- - .:/opt/src
diff --git a/mkdocs.yml b/mkdocs.yml
index d0e1b02..2e27048 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,9 +1,9 @@
-site_name: stac-fastapi
-site_description: STAC FastAPI.
+site_name: stac-fastapi-sqlachemy
+site_description: SQLAlchemy backend for stac-fastapi.
# Repository
-repo_name: 'stac-utils/stac-fastapi'
-repo_url: 'https://github.com/stac-utils/stac-fastapi'
+repo_name: 'stac-utils/stac-fastapi-sqlalchemy'
+repo_url: 'https://github.com/stac-utils/stac-fastapi-sqlalchemy'
edit_uri: 'blob/master/docs/src/'
@@ -17,27 +17,6 @@ extra:
nav:
- 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
@@ -50,14 +29,6 @@ nav:
- 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'
From 1fb943f23c4bf1adf5c64eaaba361f77c3424235 Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Wed, 10 Aug 2022 15:31:07 -0600
Subject: [PATCH 13/21] update dependabot config
---
.github/dependabot.yml | 20 --------------------
1 file changed, 20 deletions(-)
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 465c88f..7c3b077 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -8,26 +8,6 @@ updates:
directory: "/.github/workflows"
schedule:
interval: weekly
- - package-ecosystem: pip
- 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:
- interval: weekly
- - package-ecosystem: pip
- 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:
From db261ad0f420a1c3b53c362f2046f8dffe27ea07 Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Wed, 10 Aug 2022 15:33:23 -0600
Subject: [PATCH 14/21] fix deploy docs workflow maybe? not tested yet
---
.github/workflows/deploy_mkdocs.yml | 12 ++++--------
1 file changed, 4 insertions(+), 8 deletions(-)
diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml
index 00aa6dd..81d7e20 100644
--- a/.github/workflows/deploy_mkdocs.yml
+++ b/.github/workflows/deploy_mkdocs.yml
@@ -3,7 +3,7 @@ name: Publish docs via GitHub Pages
on:
push:
branches:
- - master
+ - main
paths:
# Rebuild website when docs have changed or code has changed
- 'README.md'
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - name: Checkout master
+ - name: Checkout main
uses: actions/checkout@v3
- name: Set up Python 3.8
@@ -28,11 +28,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
- python -m pip install -e \
- stac_fastapi/api \
- stac_fastapi/types \
- stac_fastapi/extensions \
- stac_fastapi/sqlalchemy
+ python -m pip install -e .
python -m pip install mkdocs mkdocs-material pdocs
- name: update API docs
@@ -41,7 +37,7 @@ jobs:
--output_dir docs/api/ \
--exclude_source \
--overwrite \
- stac_fastapi
+ stac_fastapi.sqlalchemy
env:
POSTGRES_USER: username
POSTGRES_PASS: password
From ae45b7bdf0fc2f00df73cfd81272842463361002 Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Wed, 10 Aug 2022 15:34:04 -0600
Subject: [PATCH 15/21] remove publish workflow, this project won't ship a
python library
---
.github/workflows/publish.yml | 30 ------------------------------
1 file changed, 30 deletions(-)
delete mode 100644 .github/workflows/publish.yml
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
deleted file mode 100644
index 795104f..0000000
--- a/.github/workflows/publish.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-name: Publish
-
-on:
- push:
- tags:
- - "*"
-
-jobs:
- release:
- name: release
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
-
- - name: Set up Python 3.x
- uses: actions/setup-python@v3
- with:
- python-version: "3.x"
-
- - name: Install release dependencies
- run: |
- python -m pip install --upgrade pip
- pip install setuptools wheel twine
-
- - name: Build and publish package
- env:
- TWINE_USERNAME: ${{ secrets.PYPI_STACUTILS_USERNAME }}
- TWINE_PASSWORD: ${{ secrets.PYPI_STACUTILS_PASSWORD }}
- run: |
- scripts/publish
\ No newline at end of file
From 896adc65b54f0a7645c4a6ef61ee79a9e136fd36 Mon Sep 17 00:00:00 2001
From: Jeff Albrecht
Date: Fri, 12 Aug 2022 21:48:28 -0600
Subject: [PATCH 16/21] Update mkdocs.yml
Co-authored-by: Jon Duckworth
---
mkdocs.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/mkdocs.yml b/mkdocs.yml
index 2e27048..4106cc9 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -4,7 +4,7 @@ site_description: SQLAlchemy backend for stac-fastapi.
# Repository
repo_name: 'stac-utils/stac-fastapi-sqlalchemy'
repo_url: 'https://github.com/stac-utils/stac-fastapi-sqlalchemy'
-edit_uri: 'blob/master/docs/src/'
+edit_uri: 'blob/main/docs/src/'
# Social links
From e86ea6a2595de9dfd154c8acaa01c7f14a5b8116 Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Fri, 12 Aug 2022 22:11:25 -0600
Subject: [PATCH 17/21] postfix container names, network with -sqlalchemy
---
docker-compose.yml | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index 20fcf6a..8c2487c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,7 +1,7 @@
version: '3'
services:
- stac-fastapi:
- container_name: stac-fastapi
+ stac-fastapi-sqlalchemy:
+ container_name: stac-fastapi-sqlalchemy
image: stac-utils/stac-fastapi
build:
context: .
@@ -58,11 +58,11 @@ services:
- ./stac_fastapi:/app/stac_fastapi
- ./scripts:/app/scripts
command: >
- bash -c "./scripts/wait-for-it.sh stac-fastapi:8081 && alembic upgrade head && python /app/scripts/ingest_joplin.py http://stac-fastapi:8081"
+ bash -c "./scripts/wait-for-it.sh stac-fastapi-sqlalchemy:8081 && alembic upgrade head && python /app/scripts/ingest_joplin.py http://stac-fastapi-sqlalchemy:8081"
depends_on:
- database
- - stac-fastapi
+ - stac-fastapi-sqlalchemy
networks:
default:
- name: stac-fastapi-network
+ name: stac-fastapi-sqlalchemy-network
From 5f63e88bdde5f5a3fc7fe28dda9ffcb5c9c27a06 Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Fri, 12 Aug 2022 22:11:53 -0600
Subject: [PATCH 18/21] install pyscopg2 instead of psycopg2-binary
---
Dockerfile | 2 +-
setup.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 1bd8331..b7d645e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,7 @@ FROM python:3.8-slim as base
# need the following packages in order to build
RUN apt-get update && \
apt-get -y upgrade && \
- apt-get install -y build-essential git && \
+ apt-get install -y build-essential git postgresql libpq-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
diff --git a/setup.py b/setup.py
index 46a2721..fb021d1 100644
--- a/setup.py
+++ b/setup.py
@@ -16,7 +16,7 @@
"geoalchemy2<0.8.0",
"sqlalchemy==1.3.23",
"shapely",
- "psycopg2-binary",
+ "psycopg2",
"alembic",
"fastapi-utils",
]
From 08c3764a522298c119858e0d579b715f33368e43 Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Fri, 12 Aug 2022 22:30:18 -0600
Subject: [PATCH 19/21] add psycopg2 system dependencies to ci and docs
container, revert module name to stac_fastapi
---
.github/workflows/deploy_mkdocs.yml | 3 ++-
Dockerfile.docs | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml
index 81d7e20..a96a230 100644
--- a/.github/workflows/deploy_mkdocs.yml
+++ b/.github/workflows/deploy_mkdocs.yml
@@ -27,6 +27,7 @@ jobs:
- name: Install dependencies
run: |
+ sudo apt-get install -y postgresql libpq-dev
python -m pip install --upgrade pip
python -m pip install -e .
python -m pip install mkdocs mkdocs-material pdocs
@@ -37,7 +38,7 @@ jobs:
--output_dir docs/api/ \
--exclude_source \
--overwrite \
- stac_fastapi.sqlalchemy
+ stac_fastapi
env:
POSTGRES_USER: username
POSTGRES_PASS: password
diff --git a/Dockerfile.docs b/Dockerfile.docs
index 9300c68..0c15842 100644
--- a/Dockerfile.docs
+++ b/Dockerfile.docs
@@ -1,7 +1,7 @@
FROM python:3.8-slim
# build-essential is required to build a wheel for ciso8601
-RUN apt update && apt install -y build-essential
+RUN apt update && apt install -y build-essential postgresql libpq-dev
RUN python -m pip install --upgrade pip
RUN python -m pip install mkdocs mkdocs-material pdocs
From 879cb7d1f4b4185412d5847e1a453686920a3418 Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Fri, 12 Aug 2022 22:42:50 -0600
Subject: [PATCH 20/21] update readme
---
README.md | 78 +++++++++----------------------------------------------
1 file changed, 12 insertions(+), 66 deletions(-)
diff --git a/README.md b/README.md
index 4db19e7..70945e5 100644
--- a/README.md
+++ b/README.md
@@ -3,62 +3,32 @@
FastAPI implemention of the STAC API spec.
-
-
+
+
-
-
+
+
-
-
+
+
---
-**Documentation**: [https://stac-utils.github.io/stac-fastapi/](https://stac-utils.github.io/stac-fastapi/)
+**Documentation**: [https://stac-utils.github.io/stac-fastapi-sqlalchemy/](https://stac-utils.github.io/stac-fastapi-sqlalchemy/)
-**Source Code**: [https://github.com/stac-utils/stac-fastapi](https://github.com/stac-utils/stac-fastapi)
+**Source Code**: [https://github.com/stac-utils/stac-fastapi-sqlalchemy](https://github.com/stac-utils/stac-fastapi-sqlalchemy)
---
-Python library for building a STAC compliant FastAPI application. The project is split up into several namespace
-packages:
+Sqlalchemy/postgis backend for [stac-fastapi](https://github.com/stac-utils/stac-fastapi).
-- **stac_fastapi.api**: An API layer which enforces the [stac-api-spec](https://github.com/radiantearth/stac-api-spec).
-- **stac_fastapi.extensions**: Abstract base classes for [STAC API extensions](https://github.com/radiantearth/stac-api-spec/blob/master/extensions.md) and third-party extensions.
-- **stac_fastapi.types**: Shared types and abstract base classes used by the library.
-
-#### Backends
-- **stac_fastapi.sqlalchemy**: Postgres backend implementation with sqlalchemy.
-- **stac_fastapi.pgstac**: Postgres backend implementation with [PGStac](https://github.com/stac-utils/pgstac).
-
-`stac-fastapi` was initially developed by [arturo-ai](https://github.com/arturo-ai).
## Installation
```bash
-# Install from pypi.org
-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
+pip install stac-fastapi.sqlalchey
```
## Local Development
@@ -69,18 +39,6 @@ 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`.
@@ -111,22 +69,10 @@ To run tests for both the pgstac and sqlalchemy backends, execute:
make test
```
-To only run pgstac backend tests:
-
-```shell
-make test-pgstac
-```
-
-To only run sqlalchemy backend tests:
-
-```shell
-make test-sqlalchemy
-```
Run individual tests by running pytest within a docker container:
```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
+make docker-shell
+pytest -v tests/api/test_api::test_app_search_response
```
From 58cf32d3be96c1661f5b16b1921569ab111779f0 Mon Sep 17 00:00:00 2001
From: geospatial-jeff
Date: Fri, 12 Aug 2022 22:43:10 -0600
Subject: [PATCH 21/21] rename cicd pipeline
---
.github/workflows/cicd.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml
index 4702c88..f41cac6 100644
--- a/.github/workflows/cicd.yaml
+++ b/.github/workflows/cicd.yaml
@@ -1,4 +1,4 @@
-name: stac-fastapi
+name: stac-fastapi-sqlalchemy
on:
push:
branches: [ main ]