diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index c8afdcc7..2a6ca861 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -88,29 +88,39 @@ jobs: run: | pip install ./stac_fastapi/elasticsearch[dev,server] + - name: Install opensearch stac-fastapi + run: | + pip install ./stac_fastapi/opensearch[dev,server] + + - name: Install core library stac-fastapi + run: | + pip install ./stac_fastapi/core + - name: Run test suite against Elasticsearch 7.x run: | - cd stac_fastapi/elasticsearch && pipenv run pytest -svvv + pipenv run pytest -svvv env: ENVIRONMENT: testing ES_PORT: 9200 ES_HOST: 172.17.0.1 ES_USE_SSL: false ES_VERIFY_CERTS: false + BACKEND: elasticsearch - name: Run test suite against Elasticsearch 8.x run: | - cd stac_fastapi/elasticsearch && pipenv run pytest -svvv + pipenv run pytest -svvv env: ENVIRONMENT: testing ES_PORT: 9400 ES_HOST: 172.17.0.1 ES_USE_SSL: false ES_VERIFY_CERTS: false + BACKEND: elasticsearch - name: Run test suite against OpenSearch 2.11.1 run: | - cd stac_fastapi/elasticsearch && pipenv run pytest -svvv + pipenv run pytest -svvv env: ENVIRONMENT: testing ES_PORT: 9202 diff --git a/CHANGELOG.md b/CHANGELOG.md index a11d0722..432c01cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added +- Added core library package for common logic [#186](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/186) + ### Changed +- Moved Elasticsearch and Opensearch backends into separate packages [#186](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/186) + ### Fixed +- Allow additional top-level properties on collections [#191](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/191) + ## [v1.1.0] ### Added @@ -28,7 +34,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed -- Allow additional top-level properties on collections [#191](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/191) - Exclude unset fields in search response [#166](https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/166) - Upgrade stac-fastapi to v2.4.9 [#172](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/172) - Set correct default filter-lang for GET /search requests [#179](https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/179) @@ -115,4 +120,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. [v1.0.0]: [v0.3.0]: [v0.2.0]: -[v0.1.0]: +[v0.1.0]: \ No newline at end of file diff --git a/Dockerfile.deploy b/Dockerfile.deploy.es similarity index 88% rename from Dockerfile.deploy rename to Dockerfile.deploy.es index 85d540fc..2eab7b9d 100644 --- a/Dockerfile.deploy +++ b/Dockerfile.deploy.es @@ -12,6 +12,7 @@ WORKDIR /app COPY . /app +RUN pip install --no-cache-dir -e ./stac_fastapi/core RUN pip install --no-cache-dir ./stac_fastapi/elasticsearch[server] EXPOSE 8080 diff --git a/Dockerfile.deploy.os b/Dockerfile.deploy.os new file mode 100644 index 00000000..035b181e --- /dev/null +++ b/Dockerfile.deploy.os @@ -0,0 +1,20 @@ +FROM python:3.10-slim + +RUN apt-get update && \ + apt-get -y upgrade && \ + apt-get -y install gcc && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +ENV CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt + +WORKDIR /app + +COPY . /app + +RUN pip install --no-cache-dir -e ./stac_fastapi/core +RUN pip install --no-cache-dir ./stac_fastapi/opensearch[server] + +EXPOSE 8080 + +CMD ["uvicorn", "stac_fastapi.opensearch.app:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/Dockerfile.dev b/Dockerfile.dev.es similarity index 88% rename from Dockerfile.dev rename to Dockerfile.dev.es index 4e2f0f4b..a4248d39 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev.es @@ -15,4 +15,5 @@ WORKDIR /app COPY . /app +RUN pip install --no-cache-dir -e ./stac_fastapi/core RUN pip install --no-cache-dir -e ./stac_fastapi/elasticsearch[dev,server] diff --git a/Dockerfile.dev.os b/Dockerfile.dev.os new file mode 100644 index 00000000..d9dc8b0a --- /dev/null +++ b/Dockerfile.dev.os @@ -0,0 +1,19 @@ +FROM python:3.10-slim + + +# update apt pkgs, and install build-essential for ciso8601 +RUN apt-get update && \ + apt-get -y upgrade && \ + apt-get install -y build-essential && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# update certs used by Requests +ENV CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt + +WORKDIR /app + +COPY . /app + +RUN pip install --no-cache-dir -e ./stac_fastapi/core +RUN pip install --no-cache-dir -e ./stac_fastapi/opensearch[dev,server] diff --git a/Makefile b/Makefile index 068c86d1..545d2311 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ #!make APP_HOST ?= 0.0.0.0 -ES_APP_PORT ?= 8080 EXTERNAL_APP_PORT ?= ${APP_PORT} ES_APP_PORT ?= 8080 @@ -8,7 +7,7 @@ ES_HOST ?= docker.for.mac.localhost ES_PORT ?= 9200 OS_APP_PORT ?= 8082 -ES_HOST ?= docker.for.mac.localhost +OS_HOST ?= docker.for.mac.localhost OS_PORT ?= 9202 run_es = docker-compose \ @@ -27,9 +26,13 @@ run_os = docker-compose \ -e APP_PORT=${OS_APP_PORT} \ app-opensearch -.PHONY: image-deploy -image-deploy: - docker build -f Dockerfile.deploy -t stac-fastapi-elasticsearch:latest . +.PHONY: image-deploy-es +image-deploy-es: + docker build -f Dockerfile.dev.es -t stac-fastapi-elasticsearch:latest . + +.PHONY: image-deploy-os +image-deploy-os: + docker build -f Dockerfile.dev.os -t stac-fastapi-opensearch:latest . .PHONY: run-deploy-locally run-deploy-locally: @@ -44,30 +47,38 @@ run-deploy-locally: image-dev: docker-compose build -.PHONY: docker-run -docker-run: image-dev +.PHONY: docker-run-es +docker-run-es: image-dev $(run_es) -.PHONY: docker-shell -docker-shell: +.PHONY: docker-run-os +docker-run-os: image-dev + $(run_os) + +.PHONY: docker-shell-es +docker-shell-es: $(run_es) /bin/bash +.PHONY: docker-shell-os +docker-shell-os: + $(run_os) /bin/bash + .PHONY: test-elasticsearch -test: - -$(run_es) /bin/bash -c 'export && ./scripts/wait-for-it-es.sh elasticsearch:9200 && cd /app/stac_fastapi/elasticsearch/tests/ && pytest' +test-elasticsearch: + -$(run_es) /bin/bash -c 'export && ./scripts/wait-for-it-es.sh elasticsearch:9200 && cd stac_fastapi/tests/ && pytest' docker-compose down .PHONY: test-opensearch test-opensearch: - -$(run_os) /bin/bash -c 'export && ./scripts/wait-for-it-es.sh opensearch:9202 && cd /app/stac_fastapi/elasticsearch/tests/ && pytest' + -$(run_os) /bin/bash -c 'export && ./scripts/wait-for-it-es.sh opensearch:9202 && cd stac_fastapi/tests/ && pytest' docker-compose down .PHONY: test test: - -$(run_es) /bin/bash -c 'export && ./scripts/wait-for-it-es.sh elasticsearch:9200 && cd /app/stac_fastapi/elasticsearch/tests/ && pytest' + -$(run_es) /bin/bash -c 'export && ./scripts/wait-for-it-es.sh elasticsearch:9200 && cd stac_fastapi/tests/ && pytest' docker-compose down - -$(run_os) /bin/bash -c 'export && ./scripts/wait-for-it-es.sh opensearch:9202 && cd /app/stac_fastapi/elasticsearch/tests/ && pytest' + -$(run_os) /bin/bash -c 'export && ./scripts/wait-for-it-es.sh opensearch:9202 && cd stac_fastapi/tests/ && pytest' docker-compose down .PHONY: run-database-es @@ -83,12 +94,17 @@ 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] + pip install -e ./stac_fastapi/extensions[dev] && \ + pip install -e ./stac_fastapi/core -.PHONY: install -install: pybase-install +.PHONY: install-es +install-es: pybase-install pip install -e ./stac_fastapi/elasticsearch[dev,server] +.PHONY: install-os +install-os: pybase-install + pip install -e ./stac_fastapi/opensearch[dev,server] + .PHONY: ingest ingest: python3 data_loader/data_loader.py diff --git a/README.md b/README.md index 7fbcae6b..7c662480 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,18 @@ -# STAC FastAPI Elasticsearch (sfes) +# stac-fastapi-elasticsearch-opensearch (sfeos) -## Elasticsearch backend for stac-fastapi with Opensearch support +## Elasticsearch and Opensearch backends for the stac-fastapi project -#### Join our [Gitter](https://gitter.im/stac-fastapi-elasticsearch/community) page +[![PyPI version](https://badge.fury.io/py/stac-fastapi.elasticsearch.svg)](https://badge.fury.io/py/stac-fastapi.elasticsearch) -#### Check out the public Postman documentation [Postman](https://documenter.getpostman.com/view/12888943/2s8ZDSdRHA) +To install from PyPI: -#### Check out the examples folder for deployment options, ex. running sfes from pip in docker +```shell +pip install stac_fastapi.elasticsearch +``` +or +``` +pip install stac_fastapi.opensearch +``` #### For changes, see the [Changelog](CHANGELOG.md) @@ -19,6 +25,13 @@ To install the classes in your local Python env, run: pip install -e 'stac_fastapi/elasticsearch[dev]' ``` +or + +```shell +pip install -e 'stac_fastapi/opensearch[dev]' +``` + + ### Pre-commit Install [pre-commit](https://pre-commit.com/#install). @@ -29,17 +42,17 @@ Prior to commit, run: pre-commit run --all-files ``` - -## Building +## Build Elasticsearh API backend ```shell -docker-compose build +docker-compose up elasticsearch +docker-compose build app-elasticsearch ``` -## Running API on localhost:8080 +## Running Elasticsearh API on localhost:8080 ```shell -docker-compose up +docker-compose up app-elasticsearch ``` By default, docker-compose uses Elasticsearch 8.x and OpenSearch 2.11.1. diff --git a/docker-compose.yml b/docker-compose.yml index 916b9e82..9d665bce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: restart: always build: context: . - dockerfile: Dockerfile.dev + dockerfile: Dockerfile.dev.es environment: - APP_HOST=0.0.0.0 - APP_PORT=8080 @@ -36,7 +36,7 @@ services: restart: always build: context: . - dockerfile: Dockerfile.dev + dockerfile: Dockerfile.dev.os environment: - APP_HOST=0.0.0.0 - APP_PORT=8082 @@ -57,7 +57,7 @@ services: depends_on: - opensearch command: - bash -c "./scripts/wait-for-it-es.sh os-container:9202 && python -m stac_fastapi.elasticsearch.app" + bash -c "./scripts/wait-for-it-es.sh os-container:9202 && python -m stac_fastapi.opensearch.app" elasticsearch: container_name: es-container diff --git a/examples/pip_docker/Dockerfile b/examples/pip_docker/Dockerfile index 609ada8c..1db773f5 100644 --- a/examples/pip_docker/Dockerfile +++ b/examples/pip_docker/Dockerfile @@ -15,4 +15,4 @@ WORKDIR /app COPY . /app -RUN pip install stac-fastapi.elasticsearch==0.3.0 \ No newline at end of file +RUN pip install stac-fastapi.elasticsearch==1.1.0 \ No newline at end of file diff --git a/postman_collections/stac-fastapi-elasticsearch.postman_collection.json b/examples/postman_collections/stac-fastapi-elasticsearch.postman_collection.json similarity index 100% rename from postman_collections/stac-fastapi-elasticsearch.postman_collection.json rename to examples/postman_collections/stac-fastapi-elasticsearch.postman_collection.json diff --git a/stac_fastapi/core/README.md b/stac_fastapi/core/README.md new file mode 100644 index 00000000..02f4e35a --- /dev/null +++ b/stac_fastapi/core/README.md @@ -0,0 +1 @@ +# stac-fastapi core library for Elasticsearch and Opensearch backends \ No newline at end of file diff --git a/stac_fastapi/core/setup.cfg b/stac_fastapi/core/setup.cfg new file mode 100644 index 00000000..1eb3fa49 --- /dev/null +++ b/stac_fastapi/core/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +version = attr: stac_fastapi.core.version.__version__ diff --git a/stac_fastapi/core/setup.py b/stac_fastapi/core/setup.py new file mode 100644 index 00000000..68ba8f70 --- /dev/null +++ b/stac_fastapi/core/setup.py @@ -0,0 +1,44 @@ +"""stac_fastapi: core elasticsearch/ opensearch module.""" + +from setuptools import find_namespace_packages, setup + +with open("README.md") as f: + desc = f.read() + +install_requires = [ + "fastapi", + "attrs", + "pydantic[dotenv]<2", + "stac_pydantic==2.0.*", + "stac-fastapi.types==2.4.9", + "stac-fastapi.api==2.4.9", + "stac-fastapi.extensions==2.4.9", + "pystac[validation]", + "orjson", + "overrides", + "geojson-pydantic", + "pygeofilter==0.2.1", +] + +setup( + name="stac-fastapi.core", + description="Core library for the Elasticsearch and Opensearch stac-fastapi backends.", + 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", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "License :: OSI Approved :: MIT License", + ], + url="https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch", + license="MIT", + packages=find_namespace_packages(), + zip_safe=False, + install_requires=install_requires, +) diff --git a/stac_fastapi/core/stac_fastapi/core/__init__.py b/stac_fastapi/core/stac_fastapi/core/__init__.py new file mode 100644 index 00000000..32b338eb --- /dev/null +++ b/stac_fastapi/core/stac_fastapi/core/__init__.py @@ -0,0 +1 @@ +"""Core library.""" diff --git a/stac_fastapi/core/stac_fastapi/core/base_database_logic.py b/stac_fastapi/core/stac_fastapi/core/base_database_logic.py new file mode 100644 index 00000000..0043cfb8 --- /dev/null +++ b/stac_fastapi/core/stac_fastapi/core/base_database_logic.py @@ -0,0 +1,54 @@ +"""Base database logic.""" + +import abc +from typing import Any, Dict, Iterable, Optional + + +class BaseDatabaseLogic(abc.ABC): + """ + Abstract base class for database logic. + + This class defines the basic structure and operations for database interactions. + Subclasses must provide implementations for these methods. + """ + + @abc.abstractmethod + async def get_all_collections( + self, token: Optional[str], limit: int + ) -> Iterable[Dict[str, Any]]: + """Retrieve a list of all collections from the database.""" + pass + + @abc.abstractmethod + async def get_one_item(self, collection_id: str, item_id: str) -> Dict: + """Retrieve a single item from the database.""" + pass + + @abc.abstractmethod + async def create_item(self, item: Dict, refresh: bool = False) -> None: + """Create an item in the database.""" + pass + + @abc.abstractmethod + async def delete_item( + self, item_id: str, collection_id: str, refresh: bool = False + ) -> None: + """Delete an item from the database.""" + pass + + @abc.abstractmethod + async def create_collection(self, collection: Dict, refresh: bool = False) -> None: + """Create a collection in the database.""" + pass + + @abc.abstractmethod + async def find_collection(self, collection_id: str) -> Dict: + """Find a collection in the database.""" + pass + + @abc.abstractmethod + async def delete_collection( + self, collection_id: str, refresh: bool = False + ) -> None: + """Delete a collection from the database.""" + pass diff --git a/stac_fastapi/core/stac_fastapi/core/base_settings.py b/stac_fastapi/core/stac_fastapi/core/base_settings.py new file mode 100644 index 00000000..f30d07a4 --- /dev/null +++ b/stac_fastapi/core/stac_fastapi/core/base_settings.py @@ -0,0 +1,12 @@ +"""Base settings.""" + +from abc import ABC, abstractmethod + + +class ApiBaseSettings(ABC): + """Abstract base class for API settings.""" + + @abstractmethod + def create_client(self): + """Create a database client.""" + pass diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/core.py b/stac_fastapi/core/stac_fastapi/core/core.py similarity index 85% rename from stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/core.py rename to stac_fastapi/core/stac_fastapi/core/core.py index ed6d7da9..63c43944 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/core.py +++ b/stac_fastapi/core/stac_fastapi/core/core.py @@ -1,6 +1,5 @@ """Item crud client.""" import logging -import os import re from base64 import urlsafe_b64encode from datetime import datetime as datetime_type @@ -18,22 +17,18 @@ from pygeofilter.parsers.cql2_text import parse as parse_cql2_text from stac_pydantic.links import Relations from stac_pydantic.shared import MimeTypes - -if os.getenv("BACKEND", "elasticsearch").lower() == "opensearch": - from stac_fastapi.elasticsearch.config.config_opensearch import SearchSettings - from stac_fastapi.elasticsearch.database_logic.database_logic_opensearch import ( - DatabaseLogic, - ) -else: - from stac_fastapi.elasticsearch.database_logic.database_logic_elasticsearch import ( - DatabaseLogic, - ) - from stac_fastapi.elasticsearch.config.config_elasticsearch import SearchSettings - -from stac_fastapi.elasticsearch import serializers -from stac_fastapi.elasticsearch.models.links import PagingLinks -from stac_fastapi.elasticsearch.serializers import CollectionSerializer, ItemSerializer -from stac_fastapi.elasticsearch.session import Session +from stac_pydantic.version import STAC_VERSION + +from stac_fastapi.core.base_database_logic import BaseDatabaseLogic +from stac_fastapi.core.base_settings import ApiBaseSettings +from stac_fastapi.core.models.links import PagingLinks +from stac_fastapi.core.serializers import CollectionSerializer, ItemSerializer +from stac_fastapi.core.session import Session +from stac_fastapi.core.types.core import ( + AsyncBaseCoreClient, + AsyncBaseFiltersClient, + AsyncBaseTransactionsClient, +) from stac_fastapi.extensions.third_party.bulk_transactions import ( BaseBulkTransactionsClient, BulkTransactionMethod, @@ -41,12 +36,10 @@ ) from stac_fastapi.types import stac as stac_types from stac_fastapi.types.config import Settings -from stac_fastapi.types.core import ( - AsyncBaseCoreClient, - AsyncBaseFiltersClient, - AsyncBaseTransactionsClient, -) +from stac_fastapi.types.conformance import BASE_CONFORMANCE_CLASSES +from stac_fastapi.types.extension import ApiExtension from stac_fastapi.types.links import CollectionLinks +from stac_fastapi.types.requests import get_base_url from stac_fastapi.types.search import BaseSearchPostRequest from stac_fastapi.types.stac import Collection, Collections, Item, ItemCollection @@ -74,16 +67,129 @@ class CoreClient(AsyncBaseCoreClient): with the database. """ - session: Session = attr.ib(default=attr.Factory(Session.create_from_env)) - item_serializer: Type[serializers.ItemSerializer] = attr.ib( - default=serializers.ItemSerializer + database: BaseDatabaseLogic = attr.ib() + base_conformance_classes: List[str] = attr.ib( + factory=lambda: BASE_CONFORMANCE_CLASSES ) - collection_serializer: Type[serializers.CollectionSerializer] = attr.ib( - default=serializers.CollectionSerializer + extensions: List[ApiExtension] = attr.ib(default=attr.Factory(list)) + + session: Session = attr.ib(default=attr.Factory(Session.create_from_env)) + item_serializer: Type[ItemSerializer] = attr.ib(default=ItemSerializer) + collection_serializer: Type[CollectionSerializer] = attr.ib( + default=CollectionSerializer ) - database = DatabaseLogic() + post_request_model = attr.ib(default=BaseSearchPostRequest) + 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 + + 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) + landing_page = self._landing_page( + base_url=base_url, + conformance_classes=self.conformance_classes(), + 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 - @overrides async def all_collections(self, **kwargs) -> Collections: """Read all collections from the database. @@ -148,7 +254,6 @@ async def all_collections(self, **kwargs) -> Collections: links=links, ) - @overrides async def get_collection(self, collection_id: str, **kwargs) -> Collection: """Get a collection from the database by its id. @@ -166,7 +271,6 @@ async def get_collection(self, collection_id: str, **kwargs) -> Collection: collection = await self.database.find_collection(collection_id=collection_id) return self.collection_serializer.db_to_stac(collection, base_url) - @overrides async def item_collection( self, collection_id: str, @@ -254,7 +358,6 @@ async def item_collection( context=context_obj, ) - @overrides async def get_item(self, item_id: str, collection_id: str, **kwargs) -> Item: """Get an item from the database based on its id and collection id. @@ -551,13 +654,14 @@ async def post_search( class TransactionsClient(AsyncBaseTransactionsClient): """Transactions extension specific CRUD operations.""" + database: BaseDatabaseLogic = attr.ib() + settings: ApiBaseSettings = attr.ib() session: Session = attr.ib(default=attr.Factory(Session.create_from_env)) - database = DatabaseLogic() @overrides async def create_item( self, collection_id: str, item: stac_types.Item, **kwargs - ) -> stac_types.Item: + ) -> Optional[stac_types.Item]: """Create an item in the collection. Args: @@ -577,7 +681,9 @@ async def create_item( # If a feature collection is posted if item["type"] == "FeatureCollection": - bulk_client = BulkTransactionsClient() + bulk_client = BulkTransactionsClient( + database=self.database, settings=self.settings + ) processed_items = [ bulk_client.preprocess_item(item, base_url, BulkTransactionMethod.INSERT) for item in item["features"] # type: ignore ] @@ -586,7 +692,7 @@ async def create_item( collection_id, processed_items, refresh=kwargs.get("refresh", False) ) - return None # type: ignore + return None else: item = await self.database.prep_create_item(item=item, base_url=base_url) await self.database.create_item(item, refresh=kwargs.get("refresh", False)) @@ -624,7 +730,7 @@ async def update_item( @overrides async def delete_item( self, item_id: str, collection_id: str, **kwargs - ) -> stac_types.Item: + ) -> Optional[stac_types.Item]: """Delete an item from a collection. Args: @@ -635,7 +741,7 @@ async def delete_item( Optional[stac_types.Item]: The deleted item, or `None` if the item was successfully deleted. """ await self.database.delete_item(item_id=item_id, collection_id=collection_id) - return None # type: ignore + return None @overrides async def create_collection( @@ -704,7 +810,7 @@ async def update_collection( @overrides async def delete_collection( self, collection_id: str, **kwargs - ) -> stac_types.Collection: + ) -> Optional[stac_types.Collection]: """ Delete a collection. @@ -721,7 +827,7 @@ async def delete_collection( NotFoundError: If the collection doesn't exist. """ await self.database.delete_collection(collection_id=collection_id) - return None # type: ignore + return None @attr.s @@ -733,13 +839,13 @@ class BulkTransactionsClient(BaseBulkTransactionsClient): database: An instance of `DatabaseLogic` to perform database operations. """ + database: BaseDatabaseLogic = attr.ib() + settings: ApiBaseSettings = attr.ib() session: Session = attr.ib(default=attr.Factory(Session.create_from_env)) - database = DatabaseLogic() def __attrs_post_init__(self): """Create es engine.""" - settings = SearchSettings() - self.client = settings.create_client + self.client = self.settings.create_client def preprocess_item( self, item: stac_types.Item, base_url, method: BulkTransactionMethod diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/datetime_utils.py b/stac_fastapi/core/stac_fastapi/core/datetime_utils.py similarity index 100% rename from stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/datetime_utils.py rename to stac_fastapi/core/stac_fastapi/core/datetime_utils.py diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/extensions/__init__.py b/stac_fastapi/core/stac_fastapi/core/extensions/__init__.py similarity index 100% rename from stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/extensions/__init__.py rename to stac_fastapi/core/stac_fastapi/core/extensions/__init__.py diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/extensions/filter.py b/stac_fastapi/core/stac_fastapi/core/extensions/filter.py similarity index 100% rename from stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/extensions/filter.py rename to stac_fastapi/core/stac_fastapi/core/extensions/filter.py diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/extensions/query.py b/stac_fastapi/core/stac_fastapi/core/extensions/query.py similarity index 100% rename from stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/extensions/query.py rename to stac_fastapi/core/stac_fastapi/core/extensions/query.py diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/models/__init__.py b/stac_fastapi/core/stac_fastapi/core/models/__init__.py similarity index 100% rename from stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/models/__init__.py rename to stac_fastapi/core/stac_fastapi/core/models/__init__.py diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/models/links.py b/stac_fastapi/core/stac_fastapi/core/models/links.py similarity index 100% rename from stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/models/links.py rename to stac_fastapi/core/stac_fastapi/core/models/links.py diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/models/search.py b/stac_fastapi/core/stac_fastapi/core/models/search.py similarity index 100% rename from stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/models/search.py rename to stac_fastapi/core/stac_fastapi/core/models/search.py diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/serializers.py b/stac_fastapi/core/stac_fastapi/core/serializers.py similarity index 98% rename from stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/serializers.py rename to stac_fastapi/core/stac_fastapi/core/serializers.py index ee53320b..8e83ef7c 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/serializers.py +++ b/stac_fastapi/core/stac_fastapi/core/serializers.py @@ -5,7 +5,7 @@ import attr -from stac_fastapi.elasticsearch.datetime_utils import now_to_rfc3339_str +from stac_fastapi.core.datetime_utils import now_to_rfc3339_str from stac_fastapi.types import stac as stac_types from stac_fastapi.types.links import CollectionLinks, ItemLinks, resolve_links diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/session.py b/stac_fastapi/core/stac_fastapi/core/session.py similarity index 100% rename from stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/session.py rename to stac_fastapi/core/stac_fastapi/core/session.py diff --git a/stac_fastapi/core/stac_fastapi/core/types/core.py b/stac_fastapi/core/stac_fastapi/core/types/core.py new file mode 100644 index 00000000..1212619c --- /dev/null +++ b/stac_fastapi/core/stac_fastapi/core/types/core.py @@ -0,0 +1,306 @@ +"""Base clients. Takef from stac-fastapi.types.core v2.4.9.""" +import abc +from datetime import datetime +from typing import Any, Dict, List, Optional, Union + +import attr +from starlette.responses import Response + +from stac_fastapi.core.base_database_logic import BaseDatabaseLogic +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.search import BaseSearchPostRequest +from stac_fastapi.types.stac import Conformance + +NumType = Union[float, int] +StacType = Dict[str, Any] + + +@attr.s +class AsyncBaseTransactionsClient(abc.ABC): + """Defines a pattern for implementing the STAC transaction extension.""" + + database = attr.ib(default=BaseDatabaseLogic) + + @abc.abstractmethod + async def create_item( + self, + collection_id: str, + item: Union[stac_types.Item, stac_types.ItemCollection], + **kwargs, + ) -> Optional[Union[stac_types.Item, Response, None]]: + """Create a new item. + + Called with `POST /collections/{collection_id}/items`. + + Args: + item: the item or item collection + collection_id: the id of the collection from the resource path + + Returns: + The item that was created or None if item collection. + """ + ... + + @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 # type:ignore +class AsyncBaseCoreClient(abc.ABC): + """Defines a pattern for implementing STAC api core endpoints. + + Attributes: + extensions: list of registered api extensions. + """ + + database = attr.ib(default=BaseDatabaseLogic) + + 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.""" + 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 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, + intersects: 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, + bbox: Optional[List[NumType]] = None, + datetime: Optional[Union[str, datetime]] = None, + 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": {}, + } diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/utilities.py b/stac_fastapi/core/stac_fastapi/core/utilities.py similarity index 100% rename from stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/utilities.py rename to stac_fastapi/core/stac_fastapi/core/utilities.py diff --git a/stac_fastapi/core/stac_fastapi/core/version.py b/stac_fastapi/core/stac_fastapi/core/version.py new file mode 100644 index 00000000..04a6346d --- /dev/null +++ b/stac_fastapi/core/stac_fastapi/core/version.py @@ -0,0 +1,2 @@ +"""library version.""" +__version__ = "0.1.0" diff --git a/stac_fastapi/elasticsearch/setup.cfg b/stac_fastapi/elasticsearch/setup.cfg index ad4714c2..7a42432c 100644 --- a/stac_fastapi/elasticsearch/setup.cfg +++ b/stac_fastapi/elasticsearch/setup.cfg @@ -1,2 +1,2 @@ [metadata] -version = 1.1.0 +version = attr: stac_fastapi.elasticsearch.version.__version__ diff --git a/stac_fastapi/elasticsearch/setup.py b/stac_fastapi/elasticsearch/setup.py index f038b9b1..587c1aee 100644 --- a/stac_fastapi/elasticsearch/setup.py +++ b/stac_fastapi/elasticsearch/setup.py @@ -6,24 +6,11 @@ desc = f.read() install_requires = [ - "fastapi", - "attrs", - "pydantic[dotenv]<2", - "stac_pydantic==2.0.*", - "stac-fastapi.types==2.4.9", - "stac-fastapi.api==2.4.9", - "stac-fastapi.extensions==2.4.9", + "stac-fastapi.core==0.1.0", "elasticsearch[async]==8.11.0", "elasticsearch-dsl==8.11.0", - "opensearch-py==2.4.2", - "opensearch-py[async]==2.4.2", - "pystac[validation]", "uvicorn", - "orjson", - "overrides", "starlette", - "geojson-pydantic", - "pygeofilter==0.2.1", ] extra_reqs = { @@ -56,7 +43,7 @@ "Programming Language :: Python :: 3.11", "License :: OSI Approved :: MIT License", ], - url="https://github.com/stac-utils/stac-fastapi-elasticsearch", + url="https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch", license="MIT", packages=find_namespace_packages(exclude=["alembic", "tests", "scripts"]), zip_safe=False, diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py index 570edcc5..0d896534 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py @@ -1,28 +1,20 @@ """FastAPI application.""" -import os from stac_fastapi.api.app import StacApi from stac_fastapi.api.models import create_get_request_model, create_post_request_model -from stac_fastapi.elasticsearch.core import ( +from stac_fastapi.core.core import ( BulkTransactionsClient, CoreClient, EsAsyncBaseFiltersClient, TransactionsClient, ) - -if os.getenv("BACKEND", "elasticsearch").lower() == "opensearch": - from stac_fastapi.elasticsearch.config.config_opensearch import SearchSettings - from stac_fastapi.elasticsearch.database_logic.database_logic_opensearch import ( - create_collection_index, - ) -else: - from stac_fastapi.elasticsearch.config.config_elasticsearch import SearchSettings - from stac_fastapi.elasticsearch.database_logic.database_logic_elasticsearch import ( - create_collection_index, - ) - -from stac_fastapi.elasticsearch.extensions import QueryExtension -from stac_fastapi.elasticsearch.session import Session +from stac_fastapi.core.extensions import QueryExtension +from stac_fastapi.core.session import Session +from stac_fastapi.elasticsearch.config import ElasticsearchSettings +from stac_fastapi.elasticsearch.database_logic import ( + DatabaseLogic, + create_collection_index, +) from stac_fastapi.extensions.core import ( ContextExtension, FieldsExtension, @@ -33,7 +25,7 @@ ) from stac_fastapi.extensions.third_party import BulkTransactionExtension -settings = SearchSettings() +settings = ElasticsearchSettings() session = Session.create_from_settings(settings) filter_extension = FilterExtension(client=EsAsyncBaseFiltersClient()) @@ -41,9 +33,22 @@ "http://www.opengis.net/spec/cql2/1.0/conf/advanced-comparison-operators" ) +database_logic = DatabaseLogic() + extensions = [ - TransactionExtension(client=TransactionsClient(session=session), settings=settings), - BulkTransactionExtension(client=BulkTransactionsClient(session=session)), + TransactionExtension( + client=TransactionsClient( + database=database_logic, session=session, settings=settings + ), + settings=settings, + ), + BulkTransactionExtension( + client=BulkTransactionsClient( + database=database_logic, + session=session, + settings=settings, + ) + ), FieldsExtension(), QueryExtension(), SortExtension(), @@ -57,7 +62,9 @@ api = StacApi( settings=settings, extensions=extensions, - client=CoreClient(session=session, post_request_model=post_request_model), + client=CoreClient( + database=database_logic, session=session, post_request_model=post_request_model + ), search_get_request_model=create_get_request_model(extensions), search_post_request_model=post_request_model, ) diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/config/config_elasticsearch.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/config.py similarity index 96% rename from stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/config/config_elasticsearch.py rename to stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/config.py index 903fdc3b..10cf95e9 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/config/config_elasticsearch.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/config.py @@ -54,7 +54,7 @@ def _es_config() -> Dict[str, Any]: _forbidden_fields: Set[str] = {"type"} -class SearchSettings(ApiSettings): +class ElasticsearchSettings(ApiSettings): """API settings.""" # Fields which are defined by STAC but not included in the database model @@ -67,7 +67,7 @@ def create_client(self): return Elasticsearch(**_es_config()) -class AsyncSearchSettings(ApiSettings): +class AsyncElasticsearchSettings(ApiSettings): """API settings.""" # Fields which are defined by STAC but not included in the database model diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/config/__init__.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/config/__init__.py deleted file mode 100644 index 77cfd61d..00000000 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/config/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""client config implementations.""" diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic/database_logic_elasticsearch.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py similarity index 97% rename from stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic/database_logic_elasticsearch.py rename to stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py index 44104151..9a60bfd7 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic/database_logic_elasticsearch.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py @@ -9,13 +9,13 @@ from elasticsearch_dsl import Q, Search from elasticsearch import exceptions, helpers # type: ignore -from stac_fastapi.elasticsearch import serializers -from stac_fastapi.elasticsearch.config.config_elasticsearch import AsyncSearchSettings -from stac_fastapi.elasticsearch.config.config_elasticsearch import ( - SearchSettings as SyncSearchSettings, +from stac_fastapi.core.extensions import filter +from stac_fastapi.core.serializers import CollectionSerializer, ItemSerializer +from stac_fastapi.core.utilities import bbox2polygon +from stac_fastapi.elasticsearch.config import AsyncElasticsearchSettings +from stac_fastapi.elasticsearch.config import ( + ElasticsearchSettings as SyncElasticsearchSettings, ) -from stac_fastapi.elasticsearch.extensions import filter -from stac_fastapi.elasticsearch.utilities import bbox2polygon from stac_fastapi.types.errors import ConflictError, NotFoundError from stac_fastapi.types.stac import Collection, Item @@ -178,7 +178,7 @@ async def create_collection_index() -> None: None """ - client = AsyncSearchSettings().create_client + client = AsyncElasticsearchSettings().create_client await client.options(ignore_status=400).indices.create( index=f"{COLLECTIONS_INDEX}-000001", @@ -199,7 +199,7 @@ async def create_item_index(collection_id: str): None """ - client = AsyncSearchSettings().create_client + client = AsyncElasticsearchSettings().create_client index_name = index_by_collection_id(collection_id) await client.options(ignore_status=400).indices.create( @@ -217,7 +217,7 @@ async def delete_item_index(collection_id: str): Args: collection_id (str): The ID of the collection whose items index will be deleted. """ - client = AsyncSearchSettings().create_client + client = AsyncElasticsearchSettings().create_client name = index_by_collection_id(collection_id) resolved = await client.indices.resolve_index(name=name) @@ -279,14 +279,12 @@ class Geometry(Protocol): # noqa class DatabaseLogic: """Database logic.""" - client = AsyncSearchSettings().create_client - sync_client = SyncSearchSettings().create_client + client = AsyncElasticsearchSettings().create_client + sync_client = SyncElasticsearchSettings().create_client - item_serializer: Type[serializers.ItemSerializer] = attr.ib( - default=serializers.ItemSerializer - ) - collection_serializer: Type[serializers.CollectionSerializer] = attr.ib( - default=serializers.CollectionSerializer + item_serializer: Type[ItemSerializer] = attr.ib(default=ItemSerializer) + collection_serializer: Type[CollectionSerializer] = attr.ib( + default=CollectionSerializer ) """CORE LOGIC""" diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic/__init__.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic/__init__.py deleted file mode 100644 index 2a8962ca..00000000 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""database logic implementations.""" diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/types/search.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/types/search.py deleted file mode 100644 index 26a2dbb6..00000000 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/types/search.py +++ /dev/null @@ -1,65 +0,0 @@ -"""stac_fastapi.types.search module. - -# TODO: replace with stac-pydantic -""" - -import logging -from typing import Dict, Optional, Set, Union - -from stac_pydantic.api.extensions.fields import FieldsExtension as FieldsBase - -from stac_fastapi.types.config import Settings - -logger = logging.getLogger("uvicorn") -logger.setLevel(logging.INFO) -# Be careful: https://github.com/samuelcolvin/pydantic/issues/1423#issuecomment-642797287 -NumType = Union[float, int] - - -class FieldsExtension(FieldsBase): - """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: - 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), - } diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/version.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/version.py index 1eeef171..6249d737 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/version.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/version.py @@ -1,2 +1,2 @@ """library version.""" -__version__ = "1.0.0" +__version__ = "1.1.0" diff --git a/stac_fastapi/opensearch/README.md b/stac_fastapi/opensearch/README.md new file mode 100644 index 00000000..6b1f8391 --- /dev/null +++ b/stac_fastapi/opensearch/README.md @@ -0,0 +1 @@ +# stac-fastapi-opensearch \ No newline at end of file diff --git a/stac_fastapi/opensearch/pytest.ini b/stac_fastapi/opensearch/pytest.ini new file mode 100644 index 00000000..db0353ef --- /dev/null +++ b/stac_fastapi/opensearch/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +testpaths = tests +addopts = -sv +asyncio_mode = auto \ No newline at end of file diff --git a/stac_fastapi/opensearch/setup.cfg b/stac_fastapi/opensearch/setup.cfg new file mode 100644 index 00000000..9f0be4b7 --- /dev/null +++ b/stac_fastapi/opensearch/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +version = attr: stac_fastapi.opensearch.version.__version__ diff --git a/stac_fastapi/opensearch/setup.py b/stac_fastapi/opensearch/setup.py new file mode 100644 index 00000000..9811c2ad --- /dev/null +++ b/stac_fastapi/opensearch/setup.py @@ -0,0 +1,55 @@ +"""stac_fastapi: opensearch module.""" + +from setuptools import find_namespace_packages, setup + +with open("README.md") as f: + desc = f.read() + +install_requires = [ + "stac-fastapi.core==0.1.0", + "opensearch-py==2.4.2", + "opensearch-py[async]==2.4.2", + "uvicorn", + "starlette", +] + +extra_reqs = { + "dev": [ + "pytest", + "pytest-cov", + "pytest-asyncio", + "pre-commit", + "requests", + "ciso8601", + "httpx", + ], + "docs": ["mkdocs", "mkdocs-material", "pdocs"], + "server": ["uvicorn[standard]==0.19.0"], +} + +setup( + name="stac-fastapi.opensearch", + description="Opensearch stac-fastapi 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", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "License :: OSI Approved :: MIT License", + ], + url="https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch", + license="MIT", + packages=find_namespace_packages(), + zip_safe=False, + install_requires=install_requires, + extras_require=extra_reqs, + entry_points={ + "console_scripts": ["stac-fastapi-opensearch=stac_fastapi.opensearch.app:run"] + }, +) diff --git a/stac_fastapi/opensearch/stac_fastapi/opensearch/__init__.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/__init__.py new file mode 100644 index 00000000..342b8919 --- /dev/null +++ b/stac_fastapi/opensearch/stac_fastapi/opensearch/__init__.py @@ -0,0 +1 @@ +"""opensearch submodule.""" diff --git a/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py new file mode 100644 index 00000000..ebb2921e --- /dev/null +++ b/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py @@ -0,0 +1,109 @@ +"""FastAPI application.""" + +from stac_fastapi.api.app import StacApi +from stac_fastapi.api.models import create_get_request_model, create_post_request_model +from stac_fastapi.core.core import ( + BulkTransactionsClient, + CoreClient, + EsAsyncBaseFiltersClient, + TransactionsClient, +) +from stac_fastapi.core.extensions import QueryExtension +from stac_fastapi.core.session import Session +from stac_fastapi.extensions.core import ( + ContextExtension, + FieldsExtension, + FilterExtension, + SortExtension, + TokenPaginationExtension, + TransactionExtension, +) +from stac_fastapi.extensions.third_party import BulkTransactionExtension +from stac_fastapi.opensearch.config import OpensearchSettings +from stac_fastapi.opensearch.database_logic import ( + DatabaseLogic, + create_collection_index, +) + +settings = OpensearchSettings() +session = Session.create_from_settings(settings) + +filter_extension = FilterExtension(client=EsAsyncBaseFiltersClient()) +filter_extension.conformance_classes.append( + "http://www.opengis.net/spec/cql2/1.0/conf/advanced-comparison-operators" +) + +database_logic = DatabaseLogic() + +extensions = [ + TransactionExtension( + client=TransactionsClient( + database=database_logic, session=session, settings=settings + ), + settings=settings, + ), + BulkTransactionExtension( + client=BulkTransactionsClient( + database=database_logic, + session=session, + settings=settings, + ) + ), + FieldsExtension(), + QueryExtension(), + SortExtension(), + TokenPaginationExtension(), + ContextExtension(), + filter_extension, +] + +post_request_model = create_post_request_model(extensions) + +api = StacApi( + settings=settings, + extensions=extensions, + client=CoreClient( + database=database_logic, session=session, post_request_model=post_request_model + ), + search_get_request_model=create_get_request_model(extensions), + search_post_request_model=post_request_model, +) +app = api.app + + +@app.on_event("startup") +async def _startup_event() -> None: + await create_collection_index() + + +def run() -> None: + """Run app from command line using uvicorn if available.""" + try: + import uvicorn + + uvicorn.run( + "stac_fastapi.opensearch.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/elasticsearch/stac_fastapi/elasticsearch/config/config_opensearch.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/config.py similarity index 96% rename from stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/config/config_opensearch.py rename to stac_fastapi/opensearch/stac_fastapi/opensearch/config.py index 6ea49008..a53859fa 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/config/config_opensearch.py +++ b/stac_fastapi/opensearch/stac_fastapi/opensearch/config.py @@ -55,7 +55,7 @@ def _es_config() -> Dict[str, Any]: _forbidden_fields: Set[str] = {"type"} -class SearchSettings(ApiSettings): +class OpensearchSettings(ApiSettings): """API settings.""" # Fields which are defined by STAC but not included in the database model @@ -68,7 +68,7 @@ def create_client(self): return OpenSearch(**_es_config()) -class AsyncSearchSettings(ApiSettings): +class AsyncOpensearchSettings(ApiSettings): """API settings.""" # Fields which are defined by STAC but not included in the database model diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic/database_logic_opensearch.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/database_logic.py similarity index 98% rename from stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic/database_logic_opensearch.py rename to stac_fastapi/opensearch/stac_fastapi/opensearch/database_logic.py index b5ba29db..a946f82f 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic/database_logic_opensearch.py +++ b/stac_fastapi/opensearch/stac_fastapi/opensearch/database_logic.py @@ -11,13 +11,13 @@ from opensearchpy.helpers.query import Q from opensearchpy.helpers.search import Search -from stac_fastapi.elasticsearch import serializers -from stac_fastapi.elasticsearch.config.config_opensearch import AsyncSearchSettings -from stac_fastapi.elasticsearch.config.config_opensearch import ( - SearchSettings as SyncSearchSettings, +from stac_fastapi.core import serializers +from stac_fastapi.core.extensions import filter +from stac_fastapi.core.utilities import bbox2polygon +from stac_fastapi.opensearch.config import ( + AsyncOpensearchSettings as AsyncSearchSettings, ) -from stac_fastapi.elasticsearch.extensions import filter -from stac_fastapi.elasticsearch.utilities import bbox2polygon +from stac_fastapi.opensearch.config import OpensearchSettings as SyncSearchSettings from stac_fastapi.types.errors import ConflictError, NotFoundError from stac_fastapi.types.stac import Collection, Item diff --git a/stac_fastapi/opensearch/stac_fastapi/opensearch/version.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/version.py new file mode 100644 index 00000000..04a6346d --- /dev/null +++ b/stac_fastapi/opensearch/stac_fastapi/opensearch/version.py @@ -0,0 +1,2 @@ +"""library version.""" +__version__ = "0.1.0" diff --git a/stac_fastapi/elasticsearch/tests/__init__.py b/stac_fastapi/tests/__init__.py similarity index 100% rename from stac_fastapi/elasticsearch/tests/__init__.py rename to stac_fastapi/tests/__init__.py diff --git a/stac_fastapi/elasticsearch/tests/api/__init__.py b/stac_fastapi/tests/api/__init__.py similarity index 100% rename from stac_fastapi/elasticsearch/tests/api/__init__.py rename to stac_fastapi/tests/api/__init__.py diff --git a/stac_fastapi/elasticsearch/tests/api/test_api.py b/stac_fastapi/tests/api/test_api.py similarity index 100% rename from stac_fastapi/elasticsearch/tests/api/test_api.py rename to stac_fastapi/tests/api/test_api.py diff --git a/stac_fastapi/elasticsearch/tests/clients/__init__.py b/stac_fastapi/tests/clients/__init__.py similarity index 100% rename from stac_fastapi/elasticsearch/tests/clients/__init__.py rename to stac_fastapi/tests/clients/__init__.py diff --git a/stac_fastapi/elasticsearch/tests/clients/test_elasticsearch.py b/stac_fastapi/tests/clients/test_elasticsearch.py similarity index 100% rename from stac_fastapi/elasticsearch/tests/clients/test_elasticsearch.py rename to stac_fastapi/tests/clients/test_elasticsearch.py diff --git a/stac_fastapi/elasticsearch/tests/conftest.py b/stac_fastapi/tests/conftest.py similarity index 80% rename from stac_fastapi/elasticsearch/tests/conftest.py rename to stac_fastapi/tests/conftest.py index cfe6194b..01160ee1 100644 --- a/stac_fastapi/elasticsearch/tests/conftest.py +++ b/stac_fastapi/tests/conftest.py @@ -10,26 +10,30 @@ from stac_fastapi.api.app import StacApi from stac_fastapi.api.models import create_get_request_model, create_post_request_model +from stac_fastapi.core.core import ( + BulkTransactionsClient, + CoreClient, + TransactionsClient, +) +from stac_fastapi.core.extensions import QueryExtension if os.getenv("BACKEND", "elasticsearch").lower() == "opensearch": - from stac_fastapi.elasticsearch.config.config_opensearch import AsyncSearchSettings - from stac_fastapi.elasticsearch.database_logic.database_logic_opensearch import ( + from stac_fastapi.opensearch.config import AsyncOpensearchSettings as AsyncSettings + from stac_fastapi.opensearch.config import OpensearchSettings as SearchSettings + from stac_fastapi.opensearch.database_logic import ( + DatabaseLogic, create_collection_index, ) else: - from stac_fastapi.elasticsearch.config.config_elasticsearch import ( - AsyncSearchSettings, + from stac_fastapi.elasticsearch.config import ( + ElasticsearchSettings as SearchSettings, + AsyncElasticsearchSettings as AsyncSettings, ) - from stac_fastapi.elasticsearch.database_logic.database_logic_elasticsearch import ( + from stac_fastapi.elasticsearch.database_logic import ( + DatabaseLogic, create_collection_index, ) -from stac_fastapi.elasticsearch.core import ( - BulkTransactionsClient, - CoreClient, - TransactionsClient, -) -from stac_fastapi.elasticsearch.extensions import QueryExtension from stac_fastapi.extensions.core import ( # FieldsExtension, ContextExtension, FieldsExtension, @@ -66,7 +70,7 @@ def __init__( self.query_params = query_params -class TestSettings(AsyncSearchSettings): +class TestSettings(AsyncSettings): class Config: env_file = ".env.test" @@ -156,27 +160,34 @@ async def ctx(txn_client: TransactionsClient, test_collection, test_item): await delete_collections_and_items(txn_client) +database = DatabaseLogic() +settings = SearchSettings() + + @pytest.fixture def core_client(): - return CoreClient(session=None) + return CoreClient(database=database, session=None) @pytest.fixture def txn_client(): - return TransactionsClient(session=None) + return TransactionsClient(database=database, session=None, settings=settings) @pytest.fixture def bulk_txn_client(): - return BulkTransactionsClient(session=None) + return BulkTransactionsClient(database=database, session=None, settings=settings) @pytest_asyncio.fixture(scope="session") async def app(): - settings = AsyncSearchSettings() + settings = AsyncSettings() extensions = [ TransactionExtension( - client=TransactionsClient(session=None), settings=settings + client=TransactionsClient( + database=database, session=None, settings=settings + ), + settings=settings, ), ContextExtension(), SortExtension(), @@ -191,6 +202,7 @@ async def app(): return StacApi( settings=settings, client=CoreClient( + database=database, session=None, extensions=extensions, post_request_model=post_request_model, diff --git a/stac_fastapi/elasticsearch/tests/data/test_collection.json b/stac_fastapi/tests/data/test_collection.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/data/test_collection.json rename to stac_fastapi/tests/data/test_collection.json diff --git a/stac_fastapi/elasticsearch/tests/data/test_item.json b/stac_fastapi/tests/data/test_item.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/data/test_item.json rename to stac_fastapi/tests/data/test_item.json diff --git a/stac_fastapi/elasticsearch/tests/extensions/cql2/example01.json b/stac_fastapi/tests/extensions/cql2/example01.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/extensions/cql2/example01.json rename to stac_fastapi/tests/extensions/cql2/example01.json diff --git a/stac_fastapi/elasticsearch/tests/extensions/cql2/example04.json b/stac_fastapi/tests/extensions/cql2/example04.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/extensions/cql2/example04.json rename to stac_fastapi/tests/extensions/cql2/example04.json diff --git a/stac_fastapi/elasticsearch/tests/extensions/cql2/example05a.json b/stac_fastapi/tests/extensions/cql2/example05a.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/extensions/cql2/example05a.json rename to stac_fastapi/tests/extensions/cql2/example05a.json diff --git a/stac_fastapi/elasticsearch/tests/extensions/cql2/example06b.json b/stac_fastapi/tests/extensions/cql2/example06b.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/extensions/cql2/example06b.json rename to stac_fastapi/tests/extensions/cql2/example06b.json diff --git a/stac_fastapi/elasticsearch/tests/extensions/cql2/example08.json b/stac_fastapi/tests/extensions/cql2/example08.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/extensions/cql2/example08.json rename to stac_fastapi/tests/extensions/cql2/example08.json diff --git a/stac_fastapi/elasticsearch/tests/extensions/cql2/example09.json b/stac_fastapi/tests/extensions/cql2/example09.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/extensions/cql2/example09.json rename to stac_fastapi/tests/extensions/cql2/example09.json diff --git a/stac_fastapi/elasticsearch/tests/extensions/cql2/example1.json b/stac_fastapi/tests/extensions/cql2/example1.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/extensions/cql2/example1.json rename to stac_fastapi/tests/extensions/cql2/example1.json diff --git a/stac_fastapi/elasticsearch/tests/extensions/cql2/example10.json b/stac_fastapi/tests/extensions/cql2/example10.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/extensions/cql2/example10.json rename to stac_fastapi/tests/extensions/cql2/example10.json diff --git a/stac_fastapi/elasticsearch/tests/extensions/cql2/example14.json b/stac_fastapi/tests/extensions/cql2/example14.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/extensions/cql2/example14.json rename to stac_fastapi/tests/extensions/cql2/example14.json diff --git a/stac_fastapi/elasticsearch/tests/extensions/cql2/example15.json b/stac_fastapi/tests/extensions/cql2/example15.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/extensions/cql2/example15.json rename to stac_fastapi/tests/extensions/cql2/example15.json diff --git a/stac_fastapi/elasticsearch/tests/extensions/cql2/example17.json b/stac_fastapi/tests/extensions/cql2/example17.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/extensions/cql2/example17.json rename to stac_fastapi/tests/extensions/cql2/example17.json diff --git a/stac_fastapi/elasticsearch/tests/extensions/cql2/example18.json b/stac_fastapi/tests/extensions/cql2/example18.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/extensions/cql2/example18.json rename to stac_fastapi/tests/extensions/cql2/example18.json diff --git a/stac_fastapi/elasticsearch/tests/extensions/cql2/example19.json b/stac_fastapi/tests/extensions/cql2/example19.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/extensions/cql2/example19.json rename to stac_fastapi/tests/extensions/cql2/example19.json diff --git a/stac_fastapi/elasticsearch/tests/extensions/cql2/example20.json b/stac_fastapi/tests/extensions/cql2/example20.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/extensions/cql2/example20.json rename to stac_fastapi/tests/extensions/cql2/example20.json diff --git a/stac_fastapi/elasticsearch/tests/extensions/cql2/example21.json b/stac_fastapi/tests/extensions/cql2/example21.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/extensions/cql2/example21.json rename to stac_fastapi/tests/extensions/cql2/example21.json diff --git a/stac_fastapi/elasticsearch/tests/extensions/cql2/example22.json b/stac_fastapi/tests/extensions/cql2/example22.json similarity index 100% rename from stac_fastapi/elasticsearch/tests/extensions/cql2/example22.json rename to stac_fastapi/tests/extensions/cql2/example22.json diff --git a/stac_fastapi/elasticsearch/tests/extensions/test_filter.py b/stac_fastapi/tests/extensions/test_filter.py similarity index 100% rename from stac_fastapi/elasticsearch/tests/extensions/test_filter.py rename to stac_fastapi/tests/extensions/test_filter.py diff --git a/stac_fastapi/elasticsearch/tests/resources/__init__.py b/stac_fastapi/tests/resources/__init__.py similarity index 100% rename from stac_fastapi/elasticsearch/tests/resources/__init__.py rename to stac_fastapi/tests/resources/__init__.py diff --git a/stac_fastapi/elasticsearch/tests/resources/test_collection.py b/stac_fastapi/tests/resources/test_collection.py similarity index 100% rename from stac_fastapi/elasticsearch/tests/resources/test_collection.py rename to stac_fastapi/tests/resources/test_collection.py diff --git a/stac_fastapi/elasticsearch/tests/resources/test_conformance.py b/stac_fastapi/tests/resources/test_conformance.py similarity index 100% rename from stac_fastapi/elasticsearch/tests/resources/test_conformance.py rename to stac_fastapi/tests/resources/test_conformance.py diff --git a/stac_fastapi/elasticsearch/tests/resources/test_item.py b/stac_fastapi/tests/resources/test_item.py similarity index 98% rename from stac_fastapi/elasticsearch/tests/resources/test_item.py rename to stac_fastapi/tests/resources/test_item.py index 5b382873..958d0703 100644 --- a/stac_fastapi/elasticsearch/tests/resources/test_item.py +++ b/stac_fastapi/tests/resources/test_item.py @@ -12,17 +12,25 @@ from geojson_pydantic.geometries import Polygon from pystac.utils import datetime_to_str -from stac_fastapi.elasticsearch.core import CoreClient -from stac_fastapi.elasticsearch.datetime_utils import now_to_rfc3339_str +from stac_fastapi.core.core import CoreClient +from stac_fastapi.core.datetime_utils import now_to_rfc3339_str from stac_fastapi.types.core import LandingPageMixin from ..conftest import create_item, refresh_indices +if os.getenv("BACKEND", "elasticsearch").lower() == "opensearch": + from stac_fastapi.opensearch.database_logic import DatabaseLogic +else: + from stac_fastapi.elasticsearch.database_logic import DatabaseLogic + def rfc3339_str_to_datetime(s: str) -> datetime: return ciso8601.parse_rfc3339(s) +database_logic = DatabaseLogic() + + @pytest.mark.asyncio async def test_create_and_delete_item(app_client, ctx, txn_client): """Test creation and deletion of a single item (transactions extension)""" @@ -773,7 +781,9 @@ async def test_conformance_classes_configurable(): # Update environment to avoid key error on client instantiation os.environ["READER_CONN_STRING"] = "testing" os.environ["WRITER_CONN_STRING"] = "testing" - client = CoreClient(base_conformance_classes=["this is a test"]) + client = CoreClient( + database=database_logic, base_conformance_classes=["this is a test"] + ) assert client.conformance_classes()[0] == "this is a test" diff --git a/stac_fastapi/elasticsearch/tests/resources/test_mgmt.py b/stac_fastapi/tests/resources/test_mgmt.py similarity index 100% rename from stac_fastapi/elasticsearch/tests/resources/test_mgmt.py rename to stac_fastapi/tests/resources/test_mgmt.py