stac-fastapi-opensearch 6.7.2__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.
@@ -0,0 +1 @@
1
+ """opensearch submodule."""
@@ -0,0 +1,295 @@
1
+ """FastAPI application."""
2
+
3
+ import logging
4
+ import os
5
+ from contextlib import asynccontextmanager
6
+
7
+ from fastapi import FastAPI
8
+
9
+ from stac_fastapi.api.app import StacApi
10
+ from stac_fastapi.api.models import (
11
+ ItemCollectionUri,
12
+ create_get_request_model,
13
+ create_post_request_model,
14
+ create_request_model,
15
+ )
16
+ from stac_fastapi.core.core import (
17
+ BulkTransactionsClient,
18
+ CoreClient,
19
+ TransactionsClient,
20
+ )
21
+ from stac_fastapi.core.extensions import QueryExtension
22
+ from stac_fastapi.core.extensions.aggregation import (
23
+ EsAggregationExtensionGetRequest,
24
+ EsAggregationExtensionPostRequest,
25
+ )
26
+ from stac_fastapi.core.extensions.collections_search import (
27
+ CollectionsSearchEndpointExtension,
28
+ )
29
+ from stac_fastapi.core.extensions.fields import FieldsExtension
30
+ from stac_fastapi.core.rate_limit import setup_rate_limit
31
+ from stac_fastapi.core.route_dependencies import get_route_dependencies
32
+ from stac_fastapi.core.session import Session
33
+ from stac_fastapi.core.utilities import get_bool_env
34
+ from stac_fastapi.extensions.core import (
35
+ AggregationExtension,
36
+ CollectionSearchExtension,
37
+ CollectionSearchFilterExtension,
38
+ CollectionSearchPostExtension,
39
+ FilterExtension,
40
+ FreeTextExtension,
41
+ SortExtension,
42
+ TokenPaginationExtension,
43
+ TransactionExtension,
44
+ )
45
+ from stac_fastapi.extensions.core.fields import FieldsConformanceClasses
46
+ from stac_fastapi.extensions.core.filter import FilterConformanceClasses
47
+ from stac_fastapi.extensions.core.free_text import FreeTextConformanceClasses
48
+ from stac_fastapi.extensions.core.query import QueryConformanceClasses
49
+ from stac_fastapi.extensions.core.sort import SortConformanceClasses
50
+ from stac_fastapi.extensions.third_party import BulkTransactionExtension
51
+ from stac_fastapi.opensearch.config import OpensearchSettings
52
+ from stac_fastapi.opensearch.database_logic import (
53
+ DatabaseLogic,
54
+ create_collection_index,
55
+ create_index_templates,
56
+ )
57
+ from stac_fastapi.sfeos_helpers.aggregation import EsAsyncBaseAggregationClient
58
+ from stac_fastapi.sfeos_helpers.filter import EsAsyncBaseFiltersClient
59
+
60
+ logging.basicConfig(level=logging.INFO)
61
+ logger = logging.getLogger(__name__)
62
+
63
+ TRANSACTIONS_EXTENSIONS = get_bool_env("ENABLE_TRANSACTIONS_EXTENSIONS", default=True)
64
+ ENABLE_COLLECTIONS_SEARCH = get_bool_env("ENABLE_COLLECTIONS_SEARCH", default=True)
65
+ ENABLE_COLLECTIONS_SEARCH_ROUTE = get_bool_env(
66
+ "ENABLE_COLLECTIONS_SEARCH_ROUTE", default=False
67
+ )
68
+ logger.info("TRANSACTIONS_EXTENSIONS is set to %s", TRANSACTIONS_EXTENSIONS)
69
+ logger.info("ENABLE_COLLECTIONS_SEARCH is set to %s", ENABLE_COLLECTIONS_SEARCH)
70
+ logger.info(
71
+ "ENABLE_COLLECTIONS_SEARCH_ROUTE is set to %s", ENABLE_COLLECTIONS_SEARCH_ROUTE
72
+ )
73
+
74
+ settings = OpensearchSettings()
75
+ session = Session.create_from_settings(settings)
76
+
77
+ database_logic = DatabaseLogic()
78
+
79
+ filter_extension = FilterExtension(
80
+ client=EsAsyncBaseFiltersClient(database=database_logic)
81
+ )
82
+ filter_extension.conformance_classes.append(
83
+ FilterConformanceClasses.ADVANCED_COMPARISON_OPERATORS
84
+ )
85
+
86
+ aggregation_extension = AggregationExtension(
87
+ client=EsAsyncBaseAggregationClient(
88
+ database=database_logic, session=session, settings=settings
89
+ )
90
+ )
91
+ aggregation_extension.POST = EsAggregationExtensionPostRequest
92
+ aggregation_extension.GET = EsAggregationExtensionGetRequest
93
+
94
+ fields_extension = FieldsExtension()
95
+ fields_extension.conformance_classes.append(FieldsConformanceClasses.ITEMS)
96
+
97
+ search_extensions = [
98
+ fields_extension,
99
+ QueryExtension(),
100
+ SortExtension(),
101
+ TokenPaginationExtension(),
102
+ filter_extension,
103
+ FreeTextExtension(),
104
+ ]
105
+
106
+
107
+ if TRANSACTIONS_EXTENSIONS:
108
+ search_extensions.insert(
109
+ 0,
110
+ TransactionExtension(
111
+ client=TransactionsClient(
112
+ database=database_logic, session=session, settings=settings
113
+ ),
114
+ settings=settings,
115
+ ),
116
+ )
117
+ search_extensions.insert(
118
+ 1,
119
+ BulkTransactionExtension(
120
+ client=BulkTransactionsClient(
121
+ database=database_logic,
122
+ session=session,
123
+ settings=settings,
124
+ )
125
+ ),
126
+ )
127
+
128
+ extensions = [aggregation_extension] + search_extensions
129
+
130
+ # Collection search related variables
131
+ collections_get_request_model = None
132
+
133
+ if ENABLE_COLLECTIONS_SEARCH or ENABLE_COLLECTIONS_SEARCH_ROUTE:
134
+ # Create collection search extensions
135
+ collection_search_extensions = [
136
+ QueryExtension(conformance_classes=[QueryConformanceClasses.COLLECTIONS]),
137
+ SortExtension(conformance_classes=[SortConformanceClasses.COLLECTIONS]),
138
+ FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]),
139
+ CollectionSearchFilterExtension(
140
+ conformance_classes=[FilterConformanceClasses.COLLECTIONS]
141
+ ),
142
+ FreeTextExtension(conformance_classes=[FreeTextConformanceClasses.COLLECTIONS]),
143
+ ]
144
+
145
+ # Initialize collection search with its extensions
146
+ collection_search_ext = CollectionSearchExtension.from_extensions(
147
+ collection_search_extensions
148
+ )
149
+ collections_get_request_model = collection_search_ext.GET
150
+
151
+ # Create a post request model for collection search
152
+ collection_search_post_request_model = create_post_request_model(
153
+ collection_search_extensions
154
+ )
155
+
156
+ # Create collection search extensions if enabled
157
+ if ENABLE_COLLECTIONS_SEARCH:
158
+ # Initialize collection search POST extension
159
+ collection_search_post_ext = CollectionSearchPostExtension(
160
+ client=CoreClient(
161
+ database=database_logic,
162
+ session=session,
163
+ post_request_model=collection_search_post_request_model,
164
+ landing_page_id=os.getenv("STAC_FASTAPI_LANDING_PAGE_ID", "stac-fastapi"),
165
+ ),
166
+ settings=settings,
167
+ POST=collection_search_post_request_model,
168
+ conformance_classes=[
169
+ "https://api.stacspec.org/v1.0.0-rc.1/collection-search",
170
+ QueryConformanceClasses.COLLECTIONS,
171
+ FilterConformanceClasses.COLLECTIONS,
172
+ FreeTextConformanceClasses.COLLECTIONS,
173
+ SortConformanceClasses.COLLECTIONS,
174
+ FieldsConformanceClasses.COLLECTIONS,
175
+ ],
176
+ )
177
+ extensions.append(collection_search_ext)
178
+ extensions.append(collection_search_post_ext)
179
+
180
+ if ENABLE_COLLECTIONS_SEARCH_ROUTE:
181
+ # Initialize collections-search endpoint extension
182
+ collections_search_endpoint_ext = CollectionsSearchEndpointExtension(
183
+ client=CoreClient(
184
+ database=database_logic,
185
+ session=session,
186
+ post_request_model=collection_search_post_request_model,
187
+ landing_page_id=os.getenv("STAC_FASTAPI_LANDING_PAGE_ID", "stac-fastapi"),
188
+ ),
189
+ settings=settings,
190
+ GET=collections_get_request_model,
191
+ POST=collection_search_post_request_model,
192
+ conformance_classes=[
193
+ "https://api.stacspec.org/v1.0.0-rc.1/collection-search",
194
+ QueryConformanceClasses.COLLECTIONS,
195
+ FilterConformanceClasses.COLLECTIONS,
196
+ FreeTextConformanceClasses.COLLECTIONS,
197
+ SortConformanceClasses.COLLECTIONS,
198
+ FieldsConformanceClasses.COLLECTIONS,
199
+ ],
200
+ )
201
+ extensions.append(collections_search_endpoint_ext)
202
+
203
+
204
+ database_logic.extensions = [type(ext).__name__ for ext in extensions]
205
+
206
+ post_request_model = create_post_request_model(search_extensions)
207
+
208
+ items_get_request_model = create_request_model(
209
+ model_name="ItemCollectionUri",
210
+ base_model=ItemCollectionUri,
211
+ extensions=[
212
+ SortExtension(
213
+ conformance_classes=[SortConformanceClasses.ITEMS],
214
+ ),
215
+ QueryExtension(
216
+ conformance_classes=[QueryConformanceClasses.ITEMS],
217
+ ),
218
+ filter_extension,
219
+ FieldsExtension(conformance_classes=[FieldsConformanceClasses.ITEMS]),
220
+ ],
221
+ request_type="GET",
222
+ )
223
+
224
+ app_config = {
225
+ "title": os.getenv("STAC_FASTAPI_TITLE", "stac-fastapi-opensearch"),
226
+ "description": os.getenv("STAC_FASTAPI_DESCRIPTION", "stac-fastapi-opensearch"),
227
+ "api_version": os.getenv("STAC_FASTAPI_VERSION", "6.0.0"),
228
+ "settings": settings,
229
+ "extensions": extensions,
230
+ "client": CoreClient(
231
+ database=database_logic,
232
+ session=session,
233
+ post_request_model=post_request_model,
234
+ landing_page_id=os.getenv("STAC_FASTAPI_LANDING_PAGE_ID", "stac-fastapi"),
235
+ ),
236
+ "collections_get_request_model": collections_get_request_model,
237
+ "search_get_request_model": create_get_request_model(search_extensions),
238
+ "search_post_request_model": post_request_model,
239
+ "items_get_request_model": items_get_request_model,
240
+ "route_dependencies": get_route_dependencies(),
241
+ }
242
+
243
+ # Add collections_get_request_model if it was created
244
+ if collections_get_request_model:
245
+ app_config["collections_get_request_model"] = collections_get_request_model
246
+
247
+ api = StacApi(**app_config)
248
+
249
+
250
+ @asynccontextmanager
251
+ async def lifespan(app: FastAPI):
252
+ """Lifespan handler for FastAPI app. Initializes index templates and collections at startup."""
253
+ await create_index_templates()
254
+ await create_collection_index()
255
+ yield
256
+
257
+
258
+ app = api.app
259
+ app.router.lifespan_context = lifespan
260
+ app.root_path = os.getenv("STAC_FASTAPI_ROOT_PATH", "")
261
+ # Add rate limit
262
+ setup_rate_limit(app, rate_limit=os.getenv("STAC_FASTAPI_RATE_LIMIT"))
263
+
264
+
265
+ def run() -> None:
266
+ """Run app from command line using uvicorn if available."""
267
+ try:
268
+ import uvicorn
269
+
270
+ uvicorn.run(
271
+ "stac_fastapi.opensearch.app:app",
272
+ host=settings.app_host,
273
+ port=settings.app_port,
274
+ log_level="info",
275
+ reload=settings.reload,
276
+ )
277
+ except ImportError:
278
+ raise RuntimeError("Uvicorn must be installed in order to use command")
279
+
280
+
281
+ if __name__ == "__main__":
282
+ run()
283
+
284
+
285
+ def create_handler(app):
286
+ """Create a handler to use with AWS Lambda if mangum available."""
287
+ try:
288
+ from mangum import Mangum
289
+
290
+ return Mangum(app)
291
+ except ImportError:
292
+ return None
293
+
294
+
295
+ handler = create_handler(app)
@@ -0,0 +1,151 @@
1
+ """API configuration."""
2
+
3
+ import logging
4
+ import os
5
+ import ssl
6
+ from typing import Any, Dict, Set, Union
7
+
8
+ import certifi
9
+ from opensearchpy import AsyncOpenSearch, OpenSearch
10
+
11
+ from stac_fastapi.core.base_settings import ApiBaseSettings
12
+ from stac_fastapi.core.utilities import get_bool_env
13
+ from stac_fastapi.sfeos_helpers.database import validate_refresh
14
+ from stac_fastapi.types.config import ApiSettings
15
+
16
+
17
+ def _es_config() -> Dict[str, Any]:
18
+ # Determine the scheme (http or https)
19
+ use_ssl = get_bool_env("ES_USE_SSL", default=True)
20
+ scheme = "https" if use_ssl else "http"
21
+
22
+ # Configure the hosts parameter with the correct scheme
23
+ es_hosts = os.getenv(
24
+ "ES_HOST", "localhost"
25
+ ).strip() # Default to localhost if ES_HOST is not set
26
+ es_port = os.getenv("ES_PORT", "9200") # Default to 9200 if ES_PORT is not set
27
+
28
+ # Validate ES_HOST
29
+ if not es_hosts:
30
+ raise ValueError("ES_HOST environment variable is empty or invalid.")
31
+
32
+ hosts = [f"{scheme}://{host.strip()}:{es_port}" for host in es_hosts.split(",")]
33
+
34
+ # Initialize the configuration dictionary
35
+ config: Dict[str, Any] = {
36
+ "hosts": hosts,
37
+ "headers": {"accept": "application/json", "Content-Type": "application/json"},
38
+ }
39
+
40
+ http_compress = get_bool_env("ES_HTTP_COMPRESS", default=True)
41
+ if http_compress:
42
+ config["http_compress"] = True
43
+
44
+ # Handle authentication
45
+ if (u := os.getenv("ES_USER")) and (p := os.getenv("ES_PASS")):
46
+ config["http_auth"] = (u, p)
47
+
48
+ if api_key := os.getenv("ES_API_KEY"):
49
+ if isinstance(config["headers"], dict):
50
+ headers = {**config["headers"], "x-api-key": api_key}
51
+
52
+ else:
53
+ config["headers"] = {"x-api-key": api_key}
54
+
55
+ config["headers"] = headers
56
+
57
+ # Include timeout setting if set
58
+ if timeout := os.getenv("ES_TIMEOUT"):
59
+ config["timeout"] = timeout
60
+
61
+ # Explicitly exclude SSL settings when not using SSL
62
+ if not use_ssl:
63
+ return config
64
+
65
+ # Include SSL settings if using https
66
+ config["ssl_version"] = ssl.PROTOCOL_SSLv23
67
+ config["verify_certs"] = get_bool_env("ES_VERIFY_CERTS", default=True)
68
+
69
+ # Include CA Certificates if verifying certs
70
+ if config["verify_certs"]:
71
+ config["ca_certs"] = os.getenv("CURL_CA_BUNDLE", certifi.where())
72
+
73
+ return config
74
+
75
+
76
+ _forbidden_fields: Set[str] = {"type"}
77
+
78
+
79
+ class OpensearchSettings(ApiSettings, ApiBaseSettings):
80
+ """
81
+ API settings.
82
+
83
+ Set enable_direct_response via the ENABLE_DIRECT_RESPONSE environment variable.
84
+ If enabled, all API routes use direct response for maximum performance, but ALL FastAPI dependencies (including authentication, custom status codes, and validation) are disabled.
85
+ Default is False for safety.
86
+ """
87
+
88
+ forbidden_fields: Set[str] = _forbidden_fields
89
+ indexed_fields: Set[str] = {"datetime"}
90
+ enable_response_models: bool = False
91
+ enable_direct_response: bool = get_bool_env("ENABLE_DIRECT_RESPONSE", default=False)
92
+ raise_on_bulk_error: bool = get_bool_env("RAISE_ON_BULK_ERROR", default=False)
93
+
94
+ @property
95
+ def database_refresh(self) -> Union[bool, str]:
96
+ """
97
+ Get the value of the DATABASE_REFRESH environment variable.
98
+
99
+ Returns:
100
+ Union[bool, str]: The value of DATABASE_REFRESH, which can be True, False, or "wait_for".
101
+ """
102
+ value = os.getenv("DATABASE_REFRESH", "false")
103
+ return validate_refresh(value)
104
+
105
+ @property
106
+ def create_client(self):
107
+ """Create es client."""
108
+ return OpenSearch(**_es_config())
109
+
110
+
111
+ class AsyncOpensearchSettings(ApiSettings, ApiBaseSettings):
112
+ """
113
+ API settings.
114
+
115
+ Set enable_direct_response via the ENABLE_DIRECT_RESPONSE environment variable.
116
+ If enabled, all API routes use direct response for maximum performance, but ALL FastAPI dependencies (including authentication, custom status codes, and validation) are disabled.
117
+ Default is False for safety.
118
+ """
119
+
120
+ forbidden_fields: Set[str] = _forbidden_fields
121
+ indexed_fields: Set[str] = {"datetime"}
122
+ enable_response_models: bool = False
123
+ enable_direct_response: bool = get_bool_env("ENABLE_DIRECT_RESPONSE", default=False)
124
+ raise_on_bulk_error: bool = get_bool_env("RAISE_ON_BULK_ERROR", default=False)
125
+
126
+ @property
127
+ def database_refresh(self) -> Union[bool, str]:
128
+ """
129
+ Get the value of the DATABASE_REFRESH environment variable.
130
+
131
+ Returns:
132
+ Union[bool, str]: The value of DATABASE_REFRESH, which can be True, False, or "wait_for".
133
+ """
134
+ value = os.getenv("DATABASE_REFRESH", "false")
135
+ return validate_refresh(value)
136
+
137
+ @property
138
+ def create_client(self):
139
+ """Create async elasticsearch client."""
140
+ return AsyncOpenSearch(**_es_config())
141
+
142
+
143
+ # Warn at import if direct response is enabled (applies to either settings class)
144
+ if (
145
+ OpensearchSettings().enable_direct_response
146
+ or AsyncOpensearchSettings().enable_direct_response
147
+ ):
148
+ logging.basicConfig(level=logging.WARNING)
149
+ logging.warning(
150
+ "ENABLE_DIRECT_RESPONSE is True: All FastAPI dependencies (including authentication) are DISABLED for all routes!"
151
+ )