stac-fastapi-core 4.0.0a1__py3-none-any.whl → 4.1.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/core/core.py CHANGED
@@ -334,7 +334,7 @@ class CoreClient(AsyncBaseCoreClient):
334
334
  search=search,
335
335
  limit=limit,
336
336
  sort=None,
337
- token=token, # type: ignore
337
+ token=token,
338
338
  collection_ids=[collection_id],
339
339
  )
340
340
 
@@ -633,7 +633,7 @@ class CoreClient(AsyncBaseCoreClient):
633
633
  items, maybe_count, next_token = await self.database.execute_search(
634
634
  search=search,
635
635
  limit=limit,
636
- token=search_request.token, # type: ignore
636
+ token=search_request.token,
637
637
  sort=sort,
638
638
  collection_ids=search_request.collections,
639
639
  )
@@ -676,43 +676,65 @@ class TransactionsClient(AsyncBaseTransactionsClient):
676
676
  @overrides
677
677
  async def create_item(
678
678
  self, collection_id: str, item: Union[Item, ItemCollection], **kwargs
679
- ) -> Optional[stac_types.Item]:
680
- """Create an item in the collection.
679
+ ) -> Union[stac_types.Item, str]:
680
+ """
681
+ Create an item or a feature collection of items in the specified collection.
681
682
 
682
683
  Args:
683
- collection_id (str): The id of the collection to add the item to.
684
- item (stac_types.Item): The item to be added to the collection.
685
- kwargs: Additional keyword arguments.
684
+ collection_id (str): The ID of the collection to add the item(s) to.
685
+ item (Union[Item, ItemCollection]): A single item or a collection of items to be added.
686
+ **kwargs: Additional keyword arguments, such as `request` and `refresh`.
686
687
 
687
688
  Returns:
688
- stac_types.Item: The created item.
689
+ Union[stac_types.Item, str]: The created item if a single item is added, or a summary string
690
+ indicating the number of items successfully added and errors if a collection of items is added.
689
691
 
690
692
  Raises:
691
- NotFound: If the specified collection is not found in the database.
692
- ConflictError: If the item in the specified collection already exists.
693
-
693
+ NotFoundError: If the specified collection is not found in the database.
694
+ ConflictError: If an item with the same ID already exists in the collection.
694
695
  """
695
- item = item.model_dump(mode="json")
696
- base_url = str(kwargs["request"].base_url)
696
+ request = kwargs.get("request")
697
+ base_url = str(request.base_url)
698
+
699
+ # Convert Pydantic model to dict for uniform processing
700
+ item_dict = item.model_dump(mode="json")
697
701
 
698
- # If a feature collection is posted
699
- if item["type"] == "FeatureCollection":
702
+ # Handle FeatureCollection (bulk insert)
703
+ if item_dict["type"] == "FeatureCollection":
700
704
  bulk_client = BulkTransactionsClient(
701
705
  database=self.database, settings=self.settings
702
706
  )
707
+ features = item_dict["features"]
703
708
  processed_items = [
704
- bulk_client.preprocess_item(item, base_url, BulkTransactionMethod.INSERT) for item in item["features"] # type: ignore
709
+ bulk_client.preprocess_item(
710
+ feature, base_url, BulkTransactionMethod.INSERT
711
+ )
712
+ for feature in features
705
713
  ]
706
-
707
- await self.database.bulk_async(
708
- collection_id, processed_items, refresh=kwargs.get("refresh", False)
714
+ attempted = len(processed_items)
715
+ success, errors = await self.database.bulk_async(
716
+ collection_id,
717
+ processed_items,
718
+ refresh=kwargs.get("refresh", False),
709
719
  )
720
+ if errors:
721
+ logger.error(
722
+ f"Bulk async operation encountered errors for collection {collection_id}: {errors} (attempted {attempted})"
723
+ )
724
+ else:
725
+ logger.info(
726
+ f"Bulk async operation succeeded with {success} actions for collection {collection_id}."
727
+ )
728
+ return f"Successfully added {success} Items. {attempted - success} errors occurred."
710
729
 
711
- return None
712
- else:
713
- item = await self.database.prep_create_item(item=item, base_url=base_url)
714
- await self.database.create_item(item, refresh=kwargs.get("refresh", False))
715
- return ItemSerializer.db_to_stac(item, base_url)
730
+ # Handle single item
731
+ await self.database.create_item(
732
+ item_dict,
733
+ refresh=kwargs.get("refresh", False),
734
+ base_url=base_url,
735
+ exist_ok=False,
736
+ )
737
+ return ItemSerializer.db_to_stac(item_dict, base_url)
716
738
 
717
739
  @overrides
718
740
  async def update_item(
@@ -735,12 +757,12 @@ class TransactionsClient(AsyncBaseTransactionsClient):
735
757
  """
736
758
  item = item.model_dump(mode="json")
737
759
  base_url = str(kwargs["request"].base_url)
738
- now = datetime_type.now(timezone.utc).isoformat().replace("+00:00", "Z")
760
+ now = datetime_type.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
739
761
  item["properties"]["updated"] = now
740
762
 
741
- await self.database.check_collection_exists(collection_id)
742
- await self.delete_item(item_id=item_id, collection_id=collection_id)
743
- await self.create_item(collection_id=collection_id, item=Item(**item), **kwargs)
763
+ await self.database.create_item(
764
+ item, refresh=kwargs.get("refresh", False), base_url=base_url, exist_ok=True
765
+ )
744
766
 
745
767
  return ItemSerializer.db_to_stac(item, base_url)
746
768
 
@@ -873,7 +895,7 @@ class BulkTransactionsClient(BaseBulkTransactionsClient):
873
895
  The preprocessed item.
874
896
  """
875
897
  exist_ok = method == BulkTransactionMethod.UPSERT
876
- return self.database.sync_prep_create_item(
898
+ return self.database.bulk_sync_prep_create_item(
877
899
  item=item, base_url=base_url, exist_ok=exist_ok
878
900
  )
879
901
 
@@ -897,19 +919,32 @@ class BulkTransactionsClient(BaseBulkTransactionsClient):
897
919
  else:
898
920
  base_url = ""
899
921
 
900
- processed_items = [
901
- self.preprocess_item(item, base_url, items.method)
902
- for item in items.items.values()
903
- ]
922
+ processed_items = []
923
+ for item in items.items.values():
924
+ try:
925
+ validated = Item(**item) if not isinstance(item, Item) else item
926
+ processed_items.append(
927
+ self.preprocess_item(
928
+ validated.model_dump(mode="json"), base_url, items.method
929
+ )
930
+ )
931
+ except ValidationError:
932
+ # Immediately raise on the first invalid item (strict mode)
933
+ raise
904
934
 
905
- # not a great way to get the collection_id-- should be part of the method signature
906
935
  collection_id = processed_items[0]["collection"]
907
-
908
- self.database.bulk_sync(
909
- collection_id, processed_items, refresh=kwargs.get("refresh", False)
936
+ attempted = len(processed_items)
937
+ success, errors = self.database.bulk_sync(
938
+ collection_id,
939
+ processed_items,
940
+ refresh=kwargs.get("refresh", False),
910
941
  )
942
+ if errors:
943
+ logger.error(f"Bulk sync operation encountered errors: {errors}")
944
+ else:
945
+ logger.info(f"Bulk sync operation succeeded with {success} actions.")
911
946
 
912
- return f"Successfully added {len(processed_items)} Items."
947
+ return f"Successfully added/updated {success} Items. {attempted - success} errors occurred."
913
948
 
914
949
 
915
950
  _DEFAULT_QUERYABLES: Dict[str, Dict[str, Any]] = {
@@ -96,7 +96,13 @@ ES_MAPPINGS_DYNAMIC_TEMPLATES = [
96
96
  },
97
97
  # Default all other strings not otherwise specified to keyword
98
98
  {"strings": {"match_mapping_type": "string", "mapping": {"type": "keyword"}}},
99
- {"numerics": {"match_mapping_type": "long", "mapping": {"type": "float"}}},
99
+ {"long_to_double": {"match_mapping_type": "long", "mapping": {"type": "double"}}},
100
+ {
101
+ "double_to_double": {
102
+ "match_mapping_type": "double",
103
+ "mapping": {"type": "double"},
104
+ }
105
+ },
100
106
  ]
101
107
 
102
108
  ES_ITEMS_MAPPINGS = {
@@ -10,7 +10,7 @@ from enum import auto
10
10
  from types import DynamicClassAttribute
11
11
  from typing import Any, Callable, Dict, Optional
12
12
 
13
- from pydantic import BaseModel, root_validator
13
+ from pydantic import BaseModel, model_validator
14
14
  from stac_pydantic.utils import AutoValueEnum
15
15
 
16
16
  from stac_fastapi.extensions.core.query import QueryExtension as QueryExtensionBase
@@ -63,7 +63,7 @@ class QueryExtensionPostRequest(BaseModel):
63
63
 
64
64
  query: Optional[Dict[str, Dict[Operator, Any]]] = None
65
65
 
66
- @root_validator(pre=True)
66
+ @model_validator(mode="before")
67
67
  def validate_query_fields(cls, values: Dict) -> Dict:
68
68
  """Validate query fields."""
69
69
  ...
@@ -3,6 +3,8 @@
3
3
  This module contains functions for transforming geospatial coordinates,
4
4
  such as converting bounding boxes to polygon representations.
5
5
  """
6
+ import logging
7
+ import os
6
8
  from typing import Any, Dict, List, Optional, Set, Union
7
9
 
8
10
  from stac_fastapi.types.stac import Item
@@ -10,6 +12,33 @@ from stac_fastapi.types.stac import Item
10
12
  MAX_LIMIT = 10000
11
13
 
12
14
 
15
+ def get_bool_env(name: str, default: bool = False) -> bool:
16
+ """
17
+ Retrieve a boolean value from an environment variable.
18
+
19
+ Args:
20
+ name (str): The name of the environment variable.
21
+ default (bool, optional): The default value to use if the variable is not set or unrecognized. Defaults to False.
22
+
23
+ Returns:
24
+ bool: The boolean value parsed from the environment variable.
25
+ """
26
+ value = os.getenv(name, str(default).lower())
27
+ true_values = ("true", "1", "yes", "y")
28
+ false_values = ("false", "0", "no", "n")
29
+ if value.lower() in true_values:
30
+ return True
31
+ elif value.lower() in false_values:
32
+ return False
33
+ else:
34
+ logger = logging.getLogger(__name__)
35
+ logger.warning(
36
+ f"Environment variable '{name}' has unrecognized value '{value}'. "
37
+ f"Expected one of {true_values + false_values}. Using default: {default}"
38
+ )
39
+ return default
40
+
41
+
13
42
  def bbox2polygon(b0: float, b1: float, b2: float, b3: float) -> List[List[List[float]]]:
14
43
  """Transform a bounding box represented by its four coordinates `b0`, `b1`, `b2`, and `b3` into a polygon.
15
44
 
@@ -1,2 +1,2 @@
1
1
  """library version."""
2
- __version__ = "4.0.0a1"
2
+ __version__ = "4.1.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stac-fastapi-core
3
- Version: 4.0.0a1
3
+ Version: 4.1.0
4
4
  Summary: Core library for the Elasticsearch and Opensearch stac-fastapi backends.
5
5
  Home-page: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch
6
6
  License: MIT
@@ -19,9 +19,9 @@ Requires-Dist: fastapi~=0.109.0
19
19
  Requires-Dist: attrs>=23.2.0
20
20
  Requires-Dist: pydantic<3.0.0,>=2.4.1
21
21
  Requires-Dist: stac-pydantic~=3.1.0
22
- Requires-Dist: stac-fastapi.api==5.1.1
23
- Requires-Dist: stac-fastapi.extensions==5.1.1
24
- Requires-Dist: stac-fastapi.types==5.1.1
22
+ Requires-Dist: stac-fastapi.api==5.2.0
23
+ Requires-Dist: stac-fastapi.extensions==5.2.0
24
+ Requires-Dist: stac-fastapi.types==5.2.0
25
25
  Requires-Dist: orjson~=3.9.0
26
26
  Requires-Dist: overrides~=7.4.0
27
27
  Requires-Dist: geojson-pydantic~=1.0.0
@@ -60,8 +60,18 @@ Requires-Dist: slowapi~=0.1.9
60
60
  - There is [Postman](https://documenter.getpostman.com/view/12888943/2s8ZDSdRHA) documentation here for examples on how to run some of the API routes locally - after starting the elasticsearch backend via the compose.yml file.
61
61
  - The `/examples` folder shows an example of running stac-fastapi-elasticsearch from PyPI in docker without needing any code from the repository. There is also a Postman collection here that you can load into Postman for testing the API routes.
62
62
 
63
- - For changes, see the [Changelog](CHANGELOG.md)
64
- - We are always welcoming contributions. For the development notes: [Contributing](CONTRIBUTING.md)
63
+
64
+ ### Performance Note
65
+
66
+ The `enable_direct_response` option is provided by the stac-fastapi core library (introduced in stac-fastapi 5.2.0) and is available in this project starting from v4.0.0.
67
+
68
+ **You can now control this setting via the `ENABLE_DIRECT_RESPONSE` environment variable.**
69
+
70
+ When enabled (`ENABLE_DIRECT_RESPONSE=true`), endpoints return Starlette Response objects directly, bypassing FastAPI's default serialization for improved performance. **However, all FastAPI dependencies (including authentication, custom status codes, and validation) are disabled for all routes.**
71
+
72
+ This mode is best suited for public or read-only APIs where authentication and custom logic are not required. Default is `false` for safety.
73
+
74
+ See: [issue #347](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/issues/347)
65
75
 
66
76
 
67
77
  ### To install from PyPI:
@@ -105,8 +115,9 @@ If you wish to use a different version, put the following in a
105
115
  file named `.env` in the same directory you run Docker Compose from:
106
116
 
107
117
  ```shell
108
- ELASTICSEARCH_VERSION=7.17.1
109
- OPENSEARCH_VERSION=2.11.0
118
+ ELASTICSEARCH_VERSION=8.11.0
119
+ OPENSEARCH_VERSION=2.11.1
120
+ ENABLE_DIRECT_RESPONSE=false
110
121
  ```
111
122
  The most recent Elasticsearch 7.x versions should also work. See the [opensearch-py docs](https://github.com/opensearch-project/opensearch-py/blob/main/COMPATIBILITY.md) for compatibility information.
112
123
 
@@ -131,8 +142,10 @@ You can customize additional settings in your `.env` file:
131
142
  | `RELOAD` | Enable auto-reload for development. | `true` | Optional |
132
143
  | `STAC_FASTAPI_RATE_LIMIT` | API rate limit per client. | `200/minute` | Optional |
133
144
  | `BACKEND` | Tests-related variable | `elasticsearch` or `opensearch` based on the backend | Optional |
134
- | `ELASTICSEARCH_VERSION` | ElasticSearch version | `7.17.1` | Optional |
135
- | `OPENSEARCH_VERSION` | OpenSearch version | `2.11.0` | Optional |
145
+ | `ELASTICSEARCH_VERSION` | Version of Elasticsearch to use. | `8.11.0` | Optional |
146
+ | `ENABLE_DIRECT_RESPONSE` | Enable direct response for maximum performance (disables all FastAPI dependencies, including authentication, custom status codes, and validation) | `false` | Optional |
147
+ | `OPENSEARCH_VERSION` | OpenSearch version | `2.11.1` | Optional
148
+ | `RAISE_ON_BULK_ERROR` | Controls whether bulk insert operations raise exceptions on errors. If set to `true`, the operation will stop and raise an exception when an error occurs. If set to `false`, errors will be logged, and the operation will continue. **Note:** STAC Item and ItemCollection validation errors will always raise, regardless of this flag. | `false` | Optional |
136
149
 
137
150
  > [!NOTE]
138
151
  > The variables `ES_HOST`, `ES_PORT`, `ES_USE_SSL`, and `ES_VERIFY_CERTS` apply to both Elasticsearch and OpenSearch backends, so there is no need to rename the key names to `OS_` even if you're using OpenSearch.
@@ -2,24 +2,24 @@ stac_fastapi/core/__init__.py,sha256=8izV3IWRGdXmDOK1hIPQAanbWs9EI04PJCGgqG1ZGIs
2
2
  stac_fastapi/core/base_database_logic.py,sha256=GfxPMtg2gHAZM44haIgi_9J-IG1et_FYA5xRBosJpJA,1608
3
3
  stac_fastapi/core/base_settings.py,sha256=R3_Sx7n5XpGMs3zAwFJD7y008WvGU_uI2xkaabm82Kg,239
4
4
  stac_fastapi/core/basic_auth.py,sha256=RhFv3RVSHF6OaqnaaU2DO4ncJ_S5nB1q8UNpnVJJsrk,2155
5
- stac_fastapi/core/core.py,sha256=o2mcv6sZg9naELNHCX29G_VlRe-GekPJPJNMaJE1I88,39830
6
- stac_fastapi/core/database_logic.py,sha256=2DDHZ0ja1emXmrRzoW_Me38FPb30GjRFz2Dhe9zMMpI,6904
5
+ stac_fastapi/core/core.py,sha256=6mJBCSufrkOmJwjcLB2HYSmY9ocG946QPyw0zgZK6gI,41334
6
+ stac_fastapi/core/database_logic.py,sha256=KXBNL47QcqYsmtRrHjKaH3r_A4Wl4DUpumSDiuj4uVU,7051
7
7
  stac_fastapi/core/datetime_utils.py,sha256=ICUHPgvbH-xumIKdKVtcXogaCNEsxoaVqhWlVoCx-Ug,1481
8
8
  stac_fastapi/core/rate_limit.py,sha256=Gu8dAaJReGsj1L91U6m2tflU6RahpXDRs2-AYSKoybA,1318
9
9
  stac_fastapi/core/route_dependencies.py,sha256=zefYlfQTMW292vdqmtquW4UswtBHwH5Pm-8UynyZbJQ,5522
10
10
  stac_fastapi/core/serializers.py,sha256=pJjpwA6BOHjCXBCmwVTH7MOmTjY9NXF1-i_E0yB60Zg,6228
11
11
  stac_fastapi/core/session.py,sha256=Qr080UU_7qKtIv0qZAuOV7oNUQUzT5Yn00h-m_aoCvY,473
12
- stac_fastapi/core/utilities.py,sha256=viUM0JW4fRHZqbGHLzxOEAPEEoT9D4pl-geJ8PM5qh4,5771
13
- stac_fastapi/core/version.py,sha256=I6yN3SJF9c8gQmc5k4THOt7Vh8jd_Q50-0Zl7daL29k,47
12
+ stac_fastapi/core/utilities.py,sha256=493rYGimjWykrkWnRia1Aquc4Jvlyvio21VZcEmdxPo,6743
13
+ stac_fastapi/core/version.py,sha256=3xU5aBmxgAcPizRlef_YROCW9ULsGOA4flSyd9AQog4,45
14
14
  stac_fastapi/core/extensions/__init__.py,sha256=2MCo0UoInkgItIM8id-rbeygzn_qUOvTGfr8jFXZjHQ,167
15
15
  stac_fastapi/core/extensions/aggregation.py,sha256=H5Yzvs-QH60P8jJm08Ng5FDvMU1o77WoNNZ2mKxUFjI,23062
16
16
  stac_fastapi/core/extensions/fields.py,sha256=NCT5XHvfaf297eDPNaIFsIzvJnbbUTpScqF0otdx0NA,1066
17
17
  stac_fastapi/core/extensions/filter.py,sha256=F7ECGQO-nDnc8TFmL5FK1TJMeWx5TmKMjwb8b4kRgOc,6394
18
- stac_fastapi/core/extensions/query.py,sha256=orFUi4F1hXtY8I2LhXcyDFI8x_v3mNUDRsc_oV1cw2s,1796
18
+ stac_fastapi/core/extensions/query.py,sha256=Xmo8pfZEZKPudZEjjozv3R0wLOP0ayjC9E67sBOXqWY,1803
19
19
  stac_fastapi/core/models/__init__.py,sha256=g-D1DiGfmC9Bg27DW9JzkN6fAvscv75wyhyiZ6NzvIk,48
20
20
  stac_fastapi/core/models/links.py,sha256=3jk4t2wA3RGTq9_BbzFsMKvMbgDBajQy4vKZFSHt7E8,6666
21
21
  stac_fastapi/core/models/search.py,sha256=7SgAUyzHGXBXSqB4G6cwq9FMwoAS00momb7jvBkjyow,27
22
- stac_fastapi_core-4.0.0a1.dist-info/METADATA,sha256=e4U44khYsQZMJB1vcFULJ8bjZSSEVEfgXb86U1pGGBw,19093
23
- stac_fastapi_core-4.0.0a1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
24
- stac_fastapi_core-4.0.0a1.dist-info/top_level.txt,sha256=vqn-D9-HsRPTTxy0Vk_KkDmTiMES4owwBQ3ydSZYb2s,13
25
- stac_fastapi_core-4.0.0a1.dist-info/RECORD,,
22
+ stac_fastapi_core-4.1.0.dist-info/METADATA,sha256=GZZtIG4nc-v9LRmEN1XAdcqRMF1IrVMBzfuRDoc9vDc,20524
23
+ stac_fastapi_core-4.1.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
24
+ stac_fastapi_core-4.1.0.dist-info/top_level.txt,sha256=vqn-D9-HsRPTTxy0Vk_KkDmTiMES4owwBQ3ydSZYb2s,13
25
+ stac_fastapi_core-4.1.0.dist-info/RECORD,,