stac-fastapi-opensearch 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stac_fastapi/opensearch/__init__.py +1 -0
- stac_fastapi/opensearch/app.py +150 -0
- stac_fastapi/opensearch/config.py +121 -0
- stac_fastapi/opensearch/database_logic.py +959 -0
- stac_fastapi/opensearch/version.py +2 -0
- stac_fastapi_opensearch-4.0.0.dist-info/METADATA +379 -0
- stac_fastapi_opensearch-4.0.0.dist-info/RECORD +10 -0
- stac_fastapi_opensearch-4.0.0.dist-info/WHEEL +5 -0
- stac_fastapi_opensearch-4.0.0.dist-info/entry_points.txt +2 -0
- stac_fastapi_opensearch-4.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""opensearch submodule."""
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""FastAPI application."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
|
|
8
|
+
from stac_fastapi.api.app import StacApi
|
|
9
|
+
from stac_fastapi.api.models import create_get_request_model, create_post_request_model
|
|
10
|
+
from stac_fastapi.core.core import (
|
|
11
|
+
BulkTransactionsClient,
|
|
12
|
+
CoreClient,
|
|
13
|
+
EsAsyncBaseFiltersClient,
|
|
14
|
+
TransactionsClient,
|
|
15
|
+
)
|
|
16
|
+
from stac_fastapi.core.extensions import QueryExtension
|
|
17
|
+
from stac_fastapi.core.extensions.aggregation import (
|
|
18
|
+
EsAggregationExtensionGetRequest,
|
|
19
|
+
EsAggregationExtensionPostRequest,
|
|
20
|
+
EsAsyncAggregationClient,
|
|
21
|
+
)
|
|
22
|
+
from stac_fastapi.core.extensions.fields import FieldsExtension
|
|
23
|
+
from stac_fastapi.core.rate_limit import setup_rate_limit
|
|
24
|
+
from stac_fastapi.core.route_dependencies import get_route_dependencies
|
|
25
|
+
from stac_fastapi.core.session import Session
|
|
26
|
+
from stac_fastapi.extensions.core import (
|
|
27
|
+
AggregationExtension,
|
|
28
|
+
FilterExtension,
|
|
29
|
+
FreeTextExtension,
|
|
30
|
+
SortExtension,
|
|
31
|
+
TokenPaginationExtension,
|
|
32
|
+
TransactionExtension,
|
|
33
|
+
)
|
|
34
|
+
from stac_fastapi.extensions.third_party import BulkTransactionExtension
|
|
35
|
+
from stac_fastapi.opensearch.config import OpensearchSettings
|
|
36
|
+
from stac_fastapi.opensearch.database_logic import (
|
|
37
|
+
DatabaseLogic,
|
|
38
|
+
create_collection_index,
|
|
39
|
+
create_index_templates,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
settings = OpensearchSettings()
|
|
43
|
+
session = Session.create_from_settings(settings)
|
|
44
|
+
|
|
45
|
+
database_logic = DatabaseLogic()
|
|
46
|
+
|
|
47
|
+
filter_extension = FilterExtension(
|
|
48
|
+
client=EsAsyncBaseFiltersClient(database=database_logic)
|
|
49
|
+
)
|
|
50
|
+
filter_extension.conformance_classes.append(
|
|
51
|
+
"http://www.opengis.net/spec/cql2/1.0/conf/advanced-comparison-operators"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
aggregation_extension = AggregationExtension(
|
|
55
|
+
client=EsAsyncAggregationClient(
|
|
56
|
+
database=database_logic, session=session, settings=settings
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
aggregation_extension.POST = EsAggregationExtensionPostRequest
|
|
60
|
+
aggregation_extension.GET = EsAggregationExtensionGetRequest
|
|
61
|
+
|
|
62
|
+
search_extensions = [
|
|
63
|
+
TransactionExtension(
|
|
64
|
+
client=TransactionsClient(
|
|
65
|
+
database=database_logic, session=session, settings=settings
|
|
66
|
+
),
|
|
67
|
+
settings=settings,
|
|
68
|
+
),
|
|
69
|
+
BulkTransactionExtension(
|
|
70
|
+
client=BulkTransactionsClient(
|
|
71
|
+
database=database_logic,
|
|
72
|
+
session=session,
|
|
73
|
+
settings=settings,
|
|
74
|
+
)
|
|
75
|
+
),
|
|
76
|
+
FieldsExtension(),
|
|
77
|
+
QueryExtension(),
|
|
78
|
+
SortExtension(),
|
|
79
|
+
TokenPaginationExtension(),
|
|
80
|
+
filter_extension,
|
|
81
|
+
FreeTextExtension(),
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
extensions = [aggregation_extension] + search_extensions
|
|
85
|
+
|
|
86
|
+
database_logic.extensions = [type(ext).__name__ for ext in extensions]
|
|
87
|
+
|
|
88
|
+
post_request_model = create_post_request_model(search_extensions)
|
|
89
|
+
|
|
90
|
+
api = StacApi(
|
|
91
|
+
title=os.getenv("STAC_FASTAPI_TITLE", "stac-fastapi-opensearch"),
|
|
92
|
+
description=os.getenv("STAC_FASTAPI_DESCRIPTION", "stac-fastapi-opensearch"),
|
|
93
|
+
api_version=os.getenv("STAC_FASTAPI_VERSION", "4.0.0"),
|
|
94
|
+
settings=settings,
|
|
95
|
+
extensions=extensions,
|
|
96
|
+
client=CoreClient(
|
|
97
|
+
database=database_logic, session=session, post_request_model=post_request_model
|
|
98
|
+
),
|
|
99
|
+
search_get_request_model=create_get_request_model(search_extensions),
|
|
100
|
+
search_post_request_model=post_request_model,
|
|
101
|
+
route_dependencies=get_route_dependencies(),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@asynccontextmanager
|
|
106
|
+
async def lifespan(app: FastAPI):
|
|
107
|
+
"""Lifespan handler for FastAPI app. Initializes index templates and collections at startup."""
|
|
108
|
+
await create_index_templates()
|
|
109
|
+
await create_collection_index()
|
|
110
|
+
yield
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
app = api.app
|
|
114
|
+
app.router.lifespan_context = lifespan
|
|
115
|
+
app.root_path = os.getenv("STAC_FASTAPI_ROOT_PATH", "")
|
|
116
|
+
# Add rate limit
|
|
117
|
+
setup_rate_limit(app, rate_limit=os.getenv("STAC_FASTAPI_RATE_LIMIT"))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def run() -> None:
|
|
121
|
+
"""Run app from command line using uvicorn if available."""
|
|
122
|
+
try:
|
|
123
|
+
import uvicorn
|
|
124
|
+
|
|
125
|
+
uvicorn.run(
|
|
126
|
+
"stac_fastapi.opensearch.app:app",
|
|
127
|
+
host=settings.app_host,
|
|
128
|
+
port=settings.app_port,
|
|
129
|
+
log_level="info",
|
|
130
|
+
reload=settings.reload,
|
|
131
|
+
)
|
|
132
|
+
except ImportError:
|
|
133
|
+
raise RuntimeError("Uvicorn must be installed in order to use command")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
run()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def create_handler(app):
|
|
141
|
+
"""Create a handler to use with AWS Lambda if mangum available."""
|
|
142
|
+
try:
|
|
143
|
+
from mangum import Mangum
|
|
144
|
+
|
|
145
|
+
return Mangum(app)
|
|
146
|
+
except ImportError:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
handler = create_handler(app)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""API configuration."""
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import ssl
|
|
5
|
+
from typing import Any, Dict, Set
|
|
6
|
+
|
|
7
|
+
import certifi
|
|
8
|
+
from opensearchpy import AsyncOpenSearch, OpenSearch
|
|
9
|
+
|
|
10
|
+
from stac_fastapi.core.base_settings import ApiBaseSettings
|
|
11
|
+
from stac_fastapi.core.utilities import get_bool_env
|
|
12
|
+
from stac_fastapi.types.config import ApiSettings
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _es_config() -> Dict[str, Any]:
|
|
16
|
+
# Determine the scheme (http or https)
|
|
17
|
+
use_ssl = get_bool_env("ES_USE_SSL", default=True)
|
|
18
|
+
scheme = "https" if use_ssl else "http"
|
|
19
|
+
|
|
20
|
+
# Configure the hosts parameter with the correct scheme
|
|
21
|
+
es_hosts = os.getenv(
|
|
22
|
+
"ES_HOST", "localhost"
|
|
23
|
+
).strip() # Default to localhost if ES_HOST is not set
|
|
24
|
+
es_port = os.getenv("ES_PORT", "9200") # Default to 9200 if ES_PORT is not set
|
|
25
|
+
|
|
26
|
+
# Validate ES_HOST
|
|
27
|
+
if not es_hosts:
|
|
28
|
+
raise ValueError("ES_HOST environment variable is empty or invalid.")
|
|
29
|
+
|
|
30
|
+
hosts = [f"{scheme}://{host.strip()}:{es_port}" for host in es_hosts.split(",")]
|
|
31
|
+
|
|
32
|
+
# Initialize the configuration dictionary
|
|
33
|
+
config: Dict[str, Any] = {
|
|
34
|
+
"hosts": hosts,
|
|
35
|
+
"headers": {"accept": "application/json", "Content-Type": "application/json"},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
http_compress = get_bool_env("ES_HTTP_COMPRESS", default=True)
|
|
39
|
+
if http_compress:
|
|
40
|
+
config["http_compress"] = True
|
|
41
|
+
|
|
42
|
+
# Explicitly exclude SSL settings when not using SSL
|
|
43
|
+
if not use_ssl:
|
|
44
|
+
return config
|
|
45
|
+
|
|
46
|
+
# Include SSL settings if using https
|
|
47
|
+
config["ssl_version"] = ssl.PROTOCOL_SSLv23
|
|
48
|
+
config["verify_certs"] = get_bool_env("ES_VERIFY_CERTS", default=True)
|
|
49
|
+
|
|
50
|
+
# Include CA Certificates if verifying certs
|
|
51
|
+
if config["verify_certs"]:
|
|
52
|
+
config["ca_certs"] = os.getenv("CURL_CA_BUNDLE", certifi.where())
|
|
53
|
+
|
|
54
|
+
# Handle authentication
|
|
55
|
+
if (u := os.getenv("ES_USER")) and (p := os.getenv("ES_PASS")):
|
|
56
|
+
config["http_auth"] = (u, p)
|
|
57
|
+
|
|
58
|
+
if api_key := os.getenv("ES_API_KEY"):
|
|
59
|
+
if isinstance(config["headers"], dict):
|
|
60
|
+
headers = {**config["headers"], "x-api-key": api_key}
|
|
61
|
+
|
|
62
|
+
else:
|
|
63
|
+
config["headers"] = {"x-api-key": api_key}
|
|
64
|
+
|
|
65
|
+
config["headers"] = headers
|
|
66
|
+
|
|
67
|
+
return config
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_forbidden_fields: Set[str] = {"type"}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class OpensearchSettings(ApiSettings, ApiBaseSettings):
|
|
74
|
+
"""
|
|
75
|
+
API settings.
|
|
76
|
+
|
|
77
|
+
Set enable_direct_response via the ENABLE_DIRECT_RESPONSE environment variable.
|
|
78
|
+
If enabled, all API routes use direct response for maximum performance, but ALL FastAPI dependencies (including authentication, custom status codes, and validation) are disabled.
|
|
79
|
+
Default is False for safety.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
forbidden_fields: Set[str] = _forbidden_fields
|
|
83
|
+
indexed_fields: Set[str] = {"datetime"}
|
|
84
|
+
enable_response_models: bool = False
|
|
85
|
+
enable_direct_response: bool = get_bool_env("ENABLE_DIRECT_RESPONSE", default=False)
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def create_client(self):
|
|
89
|
+
"""Create es client."""
|
|
90
|
+
return OpenSearch(**_es_config())
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class AsyncOpensearchSettings(ApiSettings, ApiBaseSettings):
|
|
94
|
+
"""
|
|
95
|
+
API settings.
|
|
96
|
+
|
|
97
|
+
Set enable_direct_response via the ENABLE_DIRECT_RESPONSE environment variable.
|
|
98
|
+
If enabled, all API routes use direct response for maximum performance, but ALL FastAPI dependencies (including authentication, custom status codes, and validation) are disabled.
|
|
99
|
+
Default is False for safety.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
forbidden_fields: Set[str] = _forbidden_fields
|
|
103
|
+
indexed_fields: Set[str] = {"datetime"}
|
|
104
|
+
enable_response_models: bool = False
|
|
105
|
+
enable_direct_response: bool = get_bool_env("ENABLE_DIRECT_RESPONSE", default=False)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def create_client(self):
|
|
109
|
+
"""Create async elasticsearch client."""
|
|
110
|
+
return AsyncOpenSearch(**_es_config())
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# Warn at import if direct response is enabled (applies to either settings class)
|
|
114
|
+
if (
|
|
115
|
+
OpensearchSettings().enable_direct_response
|
|
116
|
+
or AsyncOpensearchSettings().enable_direct_response
|
|
117
|
+
):
|
|
118
|
+
logging.basicConfig(level=logging.WARNING)
|
|
119
|
+
logging.warning(
|
|
120
|
+
"ENABLE_DIRECT_RESPONSE is True: All FastAPI dependencies (including authentication) are DISABLED for all routes!"
|
|
121
|
+
)
|