stac-fastapi-core 4.1.0__py3-none-any.whl → 5.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.
@@ -1,4 +1,4 @@
1
- """Filter extension logic for es conversion."""
1
+ """Filter extension logic for conversion."""
2
2
 
3
3
  # """
4
4
  # Implements Filter Extension.
@@ -10,48 +10,67 @@
10
10
  # defines the LIKE, IN, and BETWEEN operators.
11
11
 
12
12
  # Basic Spatial Operators (http://www.opengis.net/spec/cql2/1.0/conf/basic-spatial-operators)
13
- # defines the intersects operator (S_INTERSECTS).
13
+ # defines spatial operators (S_INTERSECTS, S_CONTAINS, S_WITHIN, S_DISJOINT).
14
14
  # """
15
15
 
16
- import re
17
16
  from enum import Enum
18
17
  from typing import Any, Dict
19
18
 
20
- _cql2_like_patterns = re.compile(r"\\.|[%_]|\\$")
21
- _valid_like_substitutions = {
22
- "\\\\": "\\",
23
- "\\%": "%",
24
- "\\_": "_",
25
- "%": "*",
26
- "_": "?",
19
+ DEFAULT_QUERYABLES: Dict[str, Dict[str, Any]] = {
20
+ "id": {
21
+ "description": "ID",
22
+ "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id",
23
+ },
24
+ "collection": {
25
+ "description": "Collection",
26
+ "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/then/properties/collection",
27
+ },
28
+ "geometry": {
29
+ "description": "Geometry",
30
+ "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/1/oneOf/0/properties/geometry",
31
+ },
32
+ "datetime": {
33
+ "description": "Acquisition Timestamp",
34
+ "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/datetime",
35
+ },
36
+ "created": {
37
+ "description": "Creation Timestamp",
38
+ "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/created",
39
+ },
40
+ "updated": {
41
+ "description": "Creation Timestamp",
42
+ "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/updated",
43
+ },
44
+ "cloud_cover": {
45
+ "description": "Cloud Cover",
46
+ "$ref": "https://stac-extensions.github.io/eo/v1.0.0/schema.json#/definitions/fields/properties/eo:cloud_cover",
47
+ },
48
+ "cloud_shadow_percentage": {
49
+ "title": "Cloud Shadow Percentage",
50
+ "description": "Cloud Shadow Percentage",
51
+ "type": "number",
52
+ "minimum": 0,
53
+ "maximum": 100,
54
+ },
55
+ "nodata_pixel_percentage": {
56
+ "title": "No Data Pixel Percentage",
57
+ "description": "No Data Pixel Percentage",
58
+ "type": "number",
59
+ "minimum": 0,
60
+ "maximum": 100,
61
+ },
27
62
  }
63
+ """Queryables that are present in all collections."""
28
64
 
65
+ OPTIONAL_QUERYABLES: Dict[str, Dict[str, Any]] = {
66
+ "platform": {
67
+ "$enum": True,
68
+ "description": "Satellite platform identifier",
69
+ },
70
+ }
71
+ """Queryables that are present in some collections."""
29
72
 
30
- def _replace_like_patterns(match: re.Match) -> str:
31
- pattern = match.group()
32
- try:
33
- return _valid_like_substitutions[pattern]
34
- except KeyError:
35
- raise ValueError(f"'{pattern}' is not a valid escape sequence")
36
-
37
-
38
- def cql2_like_to_es(string: str) -> str:
39
- """
40
- Convert CQL2 "LIKE" characters to Elasticsearch "wildcard" characters.
41
-
42
- Args:
43
- string (str): The string containing CQL2 wildcard characters.
44
-
45
- Returns:
46
- str: The converted string with Elasticsearch compatible wildcards.
47
-
48
- Raises:
49
- ValueError: If an invalid escape sequence is encountered.
50
- """
51
- return _cql2_like_patterns.sub(
52
- repl=_replace_like_patterns,
53
- string=string,
54
- )
73
+ ALL_QUERYABLES: Dict[str, Dict[str, Any]] = DEFAULT_QUERYABLES | OPTIONAL_QUERYABLES
55
74
 
56
75
 
57
76
  class LogicalOp(str, Enum):
@@ -82,121 +101,10 @@ class AdvancedComparisonOp(str, Enum):
82
101
  IN = "in"
83
102
 
84
103
 
85
- class SpatialIntersectsOp(str, Enum):
86
- """Enumeration for spatial intersection operator as per CQL2 standards."""
104
+ class SpatialOp(str, Enum):
105
+ """Enumeration for spatial operators as per CQL2 standards."""
87
106
 
88
107
  S_INTERSECTS = "s_intersects"
89
-
90
-
91
- queryables_mapping = {
92
- "id": "id",
93
- "collection": "collection",
94
- "geometry": "geometry",
95
- "datetime": "properties.datetime",
96
- "created": "properties.created",
97
- "updated": "properties.updated",
98
- "cloud_cover": "properties.eo:cloud_cover",
99
- "cloud_shadow_percentage": "properties.s2:cloud_shadow_percentage",
100
- "nodata_pixel_percentage": "properties.s2:nodata_pixel_percentage",
101
- }
102
-
103
-
104
- def to_es_field(field: str) -> str:
105
- """
106
- Map a given field to its corresponding Elasticsearch field according to a predefined mapping.
107
-
108
- Args:
109
- field (str): The field name from a user query or filter.
110
-
111
- Returns:
112
- str: The mapped field name suitable for Elasticsearch queries.
113
- """
114
- return queryables_mapping.get(field, field)
115
-
116
-
117
- def to_es(query: Dict[str, Any]) -> Dict[str, Any]:
118
- """
119
- Transform a simplified CQL2 query structure to an Elasticsearch compatible query DSL.
120
-
121
- Args:
122
- query (Dict[str, Any]): The query dictionary containing 'op' and 'args'.
123
-
124
- Returns:
125
- Dict[str, Any]: The corresponding Elasticsearch query in the form of a dictionary.
126
- """
127
- if query["op"] in [LogicalOp.AND, LogicalOp.OR, LogicalOp.NOT]:
128
- bool_type = {
129
- LogicalOp.AND: "must",
130
- LogicalOp.OR: "should",
131
- LogicalOp.NOT: "must_not",
132
- }[query["op"]]
133
- return {"bool": {bool_type: [to_es(sub_query) for sub_query in query["args"]]}}
134
-
135
- elif query["op"] in [
136
- ComparisonOp.EQ,
137
- ComparisonOp.NEQ,
138
- ComparisonOp.LT,
139
- ComparisonOp.LTE,
140
- ComparisonOp.GT,
141
- ComparisonOp.GTE,
142
- ]:
143
- range_op = {
144
- ComparisonOp.LT: "lt",
145
- ComparisonOp.LTE: "lte",
146
- ComparisonOp.GT: "gt",
147
- ComparisonOp.GTE: "gte",
148
- }
149
-
150
- field = to_es_field(query["args"][0]["property"])
151
- value = query["args"][1]
152
- if isinstance(value, dict) and "timestamp" in value:
153
- value = value["timestamp"]
154
- if query["op"] == ComparisonOp.EQ:
155
- return {"range": {field: {"gte": value, "lte": value}}}
156
- elif query["op"] == ComparisonOp.NEQ:
157
- return {
158
- "bool": {
159
- "must_not": [{"range": {field: {"gte": value, "lte": value}}}]
160
- }
161
- }
162
- else:
163
- return {"range": {field: {range_op[query["op"]]: value}}}
164
- else:
165
- if query["op"] == ComparisonOp.EQ:
166
- return {"term": {field: value}}
167
- elif query["op"] == ComparisonOp.NEQ:
168
- return {"bool": {"must_not": [{"term": {field: value}}]}}
169
- else:
170
- return {"range": {field: {range_op[query["op"]]: value}}}
171
-
172
- elif query["op"] == ComparisonOp.IS_NULL:
173
- field = to_es_field(query["args"][0]["property"])
174
- return {"bool": {"must_not": {"exists": {"field": field}}}}
175
-
176
- elif query["op"] == AdvancedComparisonOp.BETWEEN:
177
- field = to_es_field(query["args"][0]["property"])
178
- gte, lte = query["args"][1], query["args"][2]
179
- if isinstance(gte, dict) and "timestamp" in gte:
180
- gte = gte["timestamp"]
181
- if isinstance(lte, dict) and "timestamp" in lte:
182
- lte = lte["timestamp"]
183
- return {"range": {field: {"gte": gte, "lte": lte}}}
184
-
185
- elif query["op"] == AdvancedComparisonOp.IN:
186
- field = to_es_field(query["args"][0]["property"])
187
- values = query["args"][1]
188
- if not isinstance(values, list):
189
- raise ValueError(f"Arg {values} is not a list")
190
- return {"terms": {field: values}}
191
-
192
- elif query["op"] == AdvancedComparisonOp.LIKE:
193
- field = to_es_field(query["args"][0]["property"])
194
- pattern = cql2_like_to_es(query["args"][1])
195
- return {"wildcard": {field: {"value": pattern, "case_insensitive": True}}}
196
-
197
- elif query["op"] == SpatialIntersectsOp.S_INTERSECTS:
198
- field = to_es_field(query["args"][0]["property"])
199
- geometry = query["args"][1]
200
- return {"geo_shape": {field: {"shape": geometry, "relation": "intersects"}}}
201
-
202
- return {}
108
+ S_CONTAINS = "s_contains"
109
+ S_WITHIN = "s_within"
110
+ S_DISJOINT = "s_disjoint"
@@ -2,11 +2,11 @@
2
2
 
3
3
  import importlib
4
4
  import inspect
5
- import json
6
5
  import logging
7
6
  import os
8
7
  from typing import List
9
8
 
9
+ import orjson
10
10
  from fastapi import Depends
11
11
  from jsonschema import validate
12
12
 
@@ -84,14 +84,14 @@ route_dependencies_schema = {
84
84
 
85
85
  def get_route_dependencies_conf(route_dependencies_env: str) -> list:
86
86
  """Get Route dependencies configuration from file or environment variable."""
87
- if os.path.exists(route_dependencies_env):
88
- with open(route_dependencies_env, encoding="utf-8") as route_dependencies_file:
89
- route_dependencies_conf = json.load(route_dependencies_file)
87
+ if os.path.isfile(route_dependencies_env):
88
+ with open(route_dependencies_env, "rb") as f:
89
+ route_dependencies_conf = orjson.loads(f.read())
90
90
 
91
91
  else:
92
92
  try:
93
- route_dependencies_conf = json.loads(route_dependencies_env)
94
- except json.JSONDecodeError as exception:
93
+ route_dependencies_conf = orjson.loads(route_dependencies_env)
94
+ except orjson.JSONDecodeError as exception:
95
95
  _LOGGER.error("Invalid JSON format for route dependencies. %s", exception)
96
96
  raise
97
97
 
@@ -12,20 +12,35 @@ from stac_fastapi.types.stac import Item
12
12
  MAX_LIMIT = 10000
13
13
 
14
14
 
15
- def get_bool_env(name: str, default: bool = False) -> bool:
15
+ def get_bool_env(name: str, default: Union[bool, str] = False) -> bool:
16
16
  """
17
17
  Retrieve a boolean value from an environment variable.
18
18
 
19
19
  Args:
20
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.
21
+ default (Union[bool, str], optional): The default value to use if the variable is not set or unrecognized. Defaults to False.
22
22
 
23
23
  Returns:
24
24
  bool: The boolean value parsed from the environment variable.
25
25
  """
26
- value = os.getenv(name, str(default).lower())
27
26
  true_values = ("true", "1", "yes", "y")
28
27
  false_values = ("false", "0", "no", "n")
28
+
29
+ # Normalize the default value
30
+ if isinstance(default, bool):
31
+ default_str = "true" if default else "false"
32
+ elif isinstance(default, str):
33
+ default_str = default.lower()
34
+ else:
35
+ logger = logging.getLogger(__name__)
36
+ logger.warning(
37
+ f"The `default` parameter must be a boolean or string, got {type(default).__name__}. "
38
+ f"Falling back to `False`."
39
+ )
40
+ default_str = "false"
41
+
42
+ # Retrieve and normalize the environment variable value
43
+ value = os.getenv(name, default_str)
29
44
  if value.lower() in true_values:
30
45
  return True
31
46
  elif value.lower() in false_values:
@@ -34,9 +49,9 @@ def get_bool_env(name: str, default: bool = False) -> bool:
34
49
  logger = logging.getLogger(__name__)
35
50
  logger.warning(
36
51
  f"Environment variable '{name}' has unrecognized value '{value}'. "
37
- f"Expected one of {true_values + false_values}. Using default: {default}"
52
+ f"Expected one of {true_values + false_values}. Using default: {default_str}"
38
53
  )
39
- return default
54
+ return default_str in true_values
40
55
 
41
56
 
42
57
  def bbox2polygon(b0: float, b1: float, b2: float, b3: float) -> List[List[List[float]]]:
@@ -1,2 +1,2 @@
1
1
  """library version."""
2
- __version__ = "4.1.0"
2
+ __version__ = "5.0.0"