eodag 3.9.1__py3-none-any.whl → 4.0.0a1__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.
- eodag/api/core.py +378 -419
- eodag/api/product/__init__.py +3 -3
- eodag/api/product/_product.py +68 -40
- eodag/api/product/drivers/__init__.py +3 -5
- eodag/api/product/drivers/base.py +1 -18
- eodag/api/product/metadata_mapping.py +151 -215
- eodag/api/search_result.py +13 -7
- eodag/cli.py +72 -395
- eodag/config.py +46 -50
- eodag/plugins/apis/base.py +2 -2
- eodag/plugins/apis/ecmwf.py +20 -21
- eodag/plugins/apis/usgs.py +37 -33
- eodag/plugins/authentication/aws_auth.py +36 -1
- eodag/plugins/authentication/base.py +18 -3
- eodag/plugins/authentication/sas_auth.py +15 -0
- eodag/plugins/crunch/filter_date.py +3 -3
- eodag/plugins/crunch/filter_latest_intersect.py +2 -2
- eodag/plugins/crunch/filter_latest_tpl_name.py +1 -1
- eodag/plugins/download/aws.py +45 -41
- eodag/plugins/download/base.py +13 -14
- eodag/plugins/download/http.py +65 -65
- eodag/plugins/manager.py +28 -29
- eodag/plugins/search/__init__.py +3 -4
- eodag/plugins/search/base.py +128 -77
- eodag/plugins/search/build_search_result.py +105 -107
- eodag/plugins/search/cop_marine.py +44 -47
- eodag/plugins/search/csw.py +33 -33
- eodag/plugins/search/qssearch.py +335 -354
- eodag/plugins/search/stac_list_assets.py +1 -1
- eodag/plugins/search/static_stac_search.py +31 -31
- eodag/resources/{product_types.yml → collections.yml} +2353 -2429
- eodag/resources/ext_collections.json +1 -0
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/providers.yml +2432 -2714
- eodag/resources/stac_provider.yml +46 -90
- eodag/types/queryables.py +55 -91
- eodag/types/search_args.py +1 -1
- eodag/utils/__init__.py +94 -21
- eodag/utils/exceptions.py +6 -6
- eodag/utils/free_text_search.py +3 -3
- {eodag-3.9.1.dist-info → eodag-4.0.0a1.dist-info}/METADATA +11 -88
- eodag-4.0.0a1.dist-info/RECORD +92 -0
- {eodag-3.9.1.dist-info → eodag-4.0.0a1.dist-info}/entry_points.txt +0 -4
- eodag/plugins/authentication/oauth.py +0 -60
- eodag/plugins/download/creodias_s3.py +0 -64
- eodag/plugins/download/s3rest.py +0 -351
- eodag/plugins/search/data_request_search.py +0 -565
- eodag/resources/stac.yml +0 -294
- eodag/resources/stac_api.yml +0 -2105
- eodag/rest/__init__.py +0 -24
- eodag/rest/cache.py +0 -70
- eodag/rest/config.py +0 -67
- eodag/rest/constants.py +0 -26
- eodag/rest/core.py +0 -764
- eodag/rest/errors.py +0 -210
- eodag/rest/server.py +0 -604
- eodag/rest/server.wsgi +0 -6
- eodag/rest/stac.py +0 -1032
- eodag/rest/templates/README +0 -1
- eodag/rest/types/__init__.py +0 -18
- eodag/rest/types/collections_search.py +0 -44
- eodag/rest/types/eodag_search.py +0 -386
- eodag/rest/types/queryables.py +0 -174
- eodag/rest/types/stac_search.py +0 -272
- eodag/rest/utils/__init__.py +0 -207
- eodag/rest/utils/cql_evaluate.py +0 -119
- eodag/rest/utils/rfc3339.py +0 -64
- eodag-3.9.1.dist-info/RECORD +0 -115
- {eodag-3.9.1.dist-info → eodag-4.0.0a1.dist-info}/WHEEL +0 -0
- {eodag-3.9.1.dist-info → eodag-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
- {eodag-3.9.1.dist-info → eodag-4.0.0a1.dist-info}/top_level.txt +0 -0
eodag/plugins/search/base.py
CHANGED
|
@@ -21,10 +21,10 @@ import logging
|
|
|
21
21
|
from typing import TYPE_CHECKING, Annotated, get_args
|
|
22
22
|
|
|
23
23
|
import orjson
|
|
24
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
24
25
|
from pydantic.fields import Field, FieldInfo
|
|
25
26
|
|
|
26
27
|
from eodag.api.product.metadata_mapping import (
|
|
27
|
-
DEFAULT_METADATA_MAPPING,
|
|
28
28
|
NOT_AVAILABLE,
|
|
29
29
|
NOT_MAPPED,
|
|
30
30
|
mtd_cfg_as_conversion_and_querypath,
|
|
@@ -35,10 +35,12 @@ from eodag.types import model_fields_to_annotated
|
|
|
35
35
|
from eodag.types.queryables import Queryables, QueryablesDict
|
|
36
36
|
from eodag.types.search_args import SortByList
|
|
37
37
|
from eodag.utils import (
|
|
38
|
-
|
|
38
|
+
GENERIC_COLLECTION,
|
|
39
39
|
copy_deepcopy,
|
|
40
40
|
deepcopy,
|
|
41
41
|
format_dict_items,
|
|
42
|
+
format_pydantic_error,
|
|
43
|
+
get_collection_dates,
|
|
42
44
|
string_to_jsonpath,
|
|
43
45
|
update_nested_dict,
|
|
44
46
|
)
|
|
@@ -52,7 +54,6 @@ if TYPE_CHECKING:
|
|
|
52
54
|
|
|
53
55
|
from eodag.api.product import EOProduct
|
|
54
56
|
from eodag.config import PluginConfig
|
|
55
|
-
from eodag.types import S3SessionKwargs
|
|
56
57
|
|
|
57
58
|
logger = logging.getLogger("eodag.search.base")
|
|
58
59
|
|
|
@@ -64,18 +65,17 @@ class Search(PluginTopic):
|
|
|
64
65
|
:param config: An EODAG plugin configuration
|
|
65
66
|
"""
|
|
66
67
|
|
|
67
|
-
auth: Union[AuthBase,
|
|
68
|
+
auth: Union[AuthBase, S3ServiceResource]
|
|
68
69
|
next_page_url: Optional[str]
|
|
69
70
|
next_page_query_obj: Optional[dict[str, Any]]
|
|
70
71
|
total_items_nb: int
|
|
71
72
|
need_count: bool
|
|
72
|
-
_request: Any # needed by deprecated load_stac_items
|
|
73
73
|
|
|
74
74
|
def __init__(self, provider: str, config: PluginConfig) -> None:
|
|
75
75
|
super(Search, self).__init__(provider, config)
|
|
76
76
|
# Prepare the metadata mapping
|
|
77
77
|
# Do a shallow copy, the structure is flat enough for this to be sufficient
|
|
78
|
-
metas: dict[str, Any] =
|
|
78
|
+
metas: dict[str, Any] = {}
|
|
79
79
|
# Update the defaults with the mapping value. This will add any new key
|
|
80
80
|
# added by the provider mapping that is not in the default metadata
|
|
81
81
|
if self.config.metadata_mapping:
|
|
@@ -85,6 +85,9 @@ class Search(PluginTopic):
|
|
|
85
85
|
self.config.metadata_mapping,
|
|
86
86
|
result_type=getattr(self.config, "result_type", "json"),
|
|
87
87
|
)
|
|
88
|
+
# set default metadata prefix for discover_metadata if not already set
|
|
89
|
+
if hasattr(self.config, "discover_metadata"):
|
|
90
|
+
self.config.discover_metadata.setdefault("metadata_prefix", provider)
|
|
88
91
|
|
|
89
92
|
def clear(self) -> None:
|
|
90
93
|
"""Method used to clear a search context between two searches."""
|
|
@@ -103,8 +106,8 @@ class Search(PluginTopic):
|
|
|
103
106
|
"""
|
|
104
107
|
raise NotImplementedError("A Search plugin must implement a method named query")
|
|
105
108
|
|
|
106
|
-
def
|
|
107
|
-
"""Fetch
|
|
109
|
+
def discover_collections(self, **kwargs: Any) -> Optional[dict[str, Any]]:
|
|
110
|
+
"""Fetch collections list from provider using `discover_collections` conf"""
|
|
108
111
|
return None
|
|
109
112
|
|
|
110
113
|
def discover_queryables(
|
|
@@ -112,7 +115,7 @@ class Search(PluginTopic):
|
|
|
112
115
|
) -> Optional[dict[str, Annotated[Any, FieldInfo]]]:
|
|
113
116
|
"""Fetch queryables list from provider using :attr:`~eodag.config.PluginConfig.discover_queryables` conf
|
|
114
117
|
|
|
115
|
-
:param kwargs: additional filters for queryables (``
|
|
118
|
+
:param kwargs: additional filters for queryables (``collection`` and other search
|
|
116
119
|
arguments)
|
|
117
120
|
:returns: fetched queryable parameters dict
|
|
118
121
|
"""
|
|
@@ -121,15 +124,15 @@ class Search(PluginTopic):
|
|
|
121
124
|
)
|
|
122
125
|
|
|
123
126
|
def _get_defaults_as_queryables(
|
|
124
|
-
self,
|
|
127
|
+
self, collection: str
|
|
125
128
|
) -> dict[str, Annotated[Any, FieldInfo]]:
|
|
126
129
|
"""
|
|
127
|
-
Return given
|
|
130
|
+
Return given collection default settings as queryables
|
|
128
131
|
|
|
129
|
-
:param
|
|
132
|
+
:param collection: given collection
|
|
130
133
|
:returns: queryable parameters dict
|
|
131
134
|
"""
|
|
132
|
-
defaults = deepcopy(self.config.products.get(
|
|
135
|
+
defaults = deepcopy(self.config.products.get(collection, {}))
|
|
133
136
|
defaults.pop("metadata_mapping", None)
|
|
134
137
|
|
|
135
138
|
queryables: dict[str, Annotated[Any, FieldInfo]] = {}
|
|
@@ -137,53 +140,51 @@ class Search(PluginTopic):
|
|
|
137
140
|
queryables[parameter] = Annotated[type(value), Field(default=value)]
|
|
138
141
|
return queryables
|
|
139
142
|
|
|
140
|
-
def
|
|
141
|
-
|
|
142
|
-
) -> Optional[str]:
|
|
143
|
-
"""Get the provider product type from eodag product type
|
|
143
|
+
def map_collection(self, collection: Optional[str], **kwargs: Any) -> Optional[str]:
|
|
144
|
+
"""Get the provider collection from eodag collection
|
|
144
145
|
|
|
145
|
-
:param
|
|
146
|
-
:returns: provider
|
|
146
|
+
:param collection: eodag collection
|
|
147
|
+
:returns: provider collection
|
|
147
148
|
"""
|
|
148
|
-
if
|
|
149
|
+
if collection is None:
|
|
149
150
|
return None
|
|
150
|
-
logger.debug("Mapping eodag
|
|
151
|
-
return self.config.products.get(
|
|
152
|
-
"
|
|
151
|
+
logger.debug("Mapping eodag collection to provider collection")
|
|
152
|
+
return self.config.products.get(collection, {}).get(
|
|
153
|
+
"_collection", GENERIC_COLLECTION
|
|
153
154
|
)
|
|
154
155
|
|
|
155
|
-
def
|
|
156
|
-
self,
|
|
156
|
+
def get_collection_def_params(
|
|
157
|
+
self, collection: str, format_variables: Optional[dict[str, Any]] = None
|
|
157
158
|
) -> dict[str, Any]:
|
|
158
|
-
"""Get the provider
|
|
159
|
+
"""Get the provider collection definition parameters and specific settings
|
|
159
160
|
|
|
160
|
-
:param
|
|
161
|
-
:returns: The
|
|
161
|
+
:param collection: the desired collection
|
|
162
|
+
:returns: The collection definition parameters
|
|
162
163
|
"""
|
|
163
|
-
if
|
|
164
|
-
return self.config.products[
|
|
165
|
-
elif
|
|
164
|
+
if collection in self.config.products.keys():
|
|
165
|
+
return self.config.products[collection]
|
|
166
|
+
elif GENERIC_COLLECTION in self.config.products.keys():
|
|
166
167
|
logger.debug(
|
|
167
|
-
"Getting generic provider
|
|
168
|
-
|
|
168
|
+
"Getting generic provider collection definition parameters for %s",
|
|
169
|
+
collection,
|
|
169
170
|
)
|
|
170
171
|
return {
|
|
171
172
|
k: v
|
|
172
173
|
for k, v in format_dict_items(
|
|
173
|
-
self.config.products[
|
|
174
|
-
**(format_variables or {}),
|
|
174
|
+
self.config.products[GENERIC_COLLECTION],
|
|
175
|
+
**({"collection": collection} | (format_variables or {})),
|
|
175
176
|
).items()
|
|
176
177
|
if v
|
|
177
178
|
}
|
|
178
179
|
else:
|
|
179
180
|
return {}
|
|
180
181
|
|
|
181
|
-
def
|
|
182
|
+
def get_collection_cfg_value(self, key: str, default: Any = None) -> Any:
|
|
182
183
|
"""
|
|
183
|
-
Get the value of a configuration option specific to the current
|
|
184
|
+
Get the value of a configuration option specific to the current collection.
|
|
184
185
|
|
|
185
186
|
This method retrieves the value of a configuration option from the
|
|
186
|
-
``
|
|
187
|
+
``collection_config`` attribute. If the option is not found, the provided
|
|
187
188
|
default value is returned.
|
|
188
189
|
|
|
189
190
|
:param key: The configuration option key.
|
|
@@ -194,21 +195,39 @@ class Search(PluginTopic):
|
|
|
194
195
|
:return: The value of the specified configuration option or the default value.
|
|
195
196
|
:rtype: Any
|
|
196
197
|
"""
|
|
197
|
-
|
|
198
|
-
non_none_cfg = {k: v for k, v in
|
|
198
|
+
collection_cfg = getattr(self.config, "collection_config", {})
|
|
199
|
+
non_none_cfg = {k: v for k, v in collection_cfg.items() if v}
|
|
199
200
|
|
|
200
201
|
return non_none_cfg.get(key, default)
|
|
201
202
|
|
|
203
|
+
def get_collection_cfg_dates(
|
|
204
|
+
self, start_default: Optional[str] = None, end_default: Optional[str] = None
|
|
205
|
+
) -> tuple[Optional[str], Optional[str]]:
|
|
206
|
+
"""
|
|
207
|
+
Get start and end dates from the collection configuration.
|
|
208
|
+
|
|
209
|
+
Extracts dates from the extent.temporal.interval structure in the collection
|
|
210
|
+
configuration, falling back to provided defaults if dates are not available.
|
|
211
|
+
|
|
212
|
+
:param start_default: Default value to return for start date if not found in config
|
|
213
|
+
:param end_default: Default value to return for end date if not found in config
|
|
214
|
+
:returns: Tuple of (mission_start_date, mission_end_date) as ISO strings or defaults
|
|
215
|
+
"""
|
|
216
|
+
collection_cfg = getattr(self.config, "collection_config", {})
|
|
217
|
+
col_start, col_end = get_collection_dates(collection_cfg)
|
|
218
|
+
|
|
219
|
+
return col_start or start_default, col_end or end_default
|
|
220
|
+
|
|
202
221
|
def get_metadata_mapping(
|
|
203
|
-
self,
|
|
222
|
+
self, collection: Optional[str] = None
|
|
204
223
|
) -> dict[str, Union[str, list[str]]]:
|
|
205
|
-
"""Get the plugin metadata mapping configuration (
|
|
224
|
+
"""Get the plugin metadata mapping configuration (collection specific if exists)
|
|
206
225
|
|
|
207
|
-
:param
|
|
208
|
-
:returns: The
|
|
226
|
+
:param collection: the desired collection
|
|
227
|
+
:returns: The collection specific metadata-mapping
|
|
209
228
|
"""
|
|
210
|
-
if
|
|
211
|
-
return self.config.products.get(
|
|
229
|
+
if collection:
|
|
230
|
+
return self.config.products.get(collection, {}).get(
|
|
212
231
|
"metadata_mapping", self.config.metadata_mapping
|
|
213
232
|
)
|
|
214
233
|
return self.config.metadata_mapping
|
|
@@ -327,68 +346,68 @@ class Search(PluginTopic):
|
|
|
327
346
|
sort_by_qs += parsed_sort_by_tpl
|
|
328
347
|
return (sort_by_qs, sort_by_qp)
|
|
329
348
|
|
|
330
|
-
def
|
|
331
|
-
self,
|
|
349
|
+
def _get_collection_queryables(
|
|
350
|
+
self, collection: Optional[str], alias: Optional[str], filters: dict[str, Any]
|
|
332
351
|
) -> QueryablesDict:
|
|
333
352
|
default_values: dict[str, Any] = deepcopy(
|
|
334
|
-
getattr(self.config, "products", {}).get(
|
|
353
|
+
getattr(self.config, "products", {}).get(collection, {})
|
|
335
354
|
)
|
|
336
355
|
default_values.pop("metadata_mapping", None)
|
|
337
356
|
try:
|
|
338
|
-
filters["
|
|
357
|
+
filters["collection"] = collection
|
|
339
358
|
queryables = self.discover_queryables(**{**default_values, **filters}) or {}
|
|
340
359
|
except NotImplementedError as e:
|
|
341
360
|
if str(e):
|
|
342
361
|
logger.debug(str(e))
|
|
343
|
-
queryables = self.queryables_from_metadata_mapping(
|
|
362
|
+
queryables = self.queryables_from_metadata_mapping(collection, alias)
|
|
344
363
|
|
|
345
364
|
return QueryablesDict(**queryables)
|
|
346
365
|
|
|
347
366
|
def list_queryables(
|
|
348
367
|
self,
|
|
349
368
|
filters: dict[str, Any],
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
369
|
+
available_collections: list[Any],
|
|
370
|
+
collection_configs: dict[str, dict[str, Any]],
|
|
371
|
+
collection: Optional[str] = None,
|
|
353
372
|
alias: Optional[str] = None,
|
|
354
373
|
) -> QueryablesDict:
|
|
355
374
|
"""
|
|
356
375
|
Get queryables
|
|
357
376
|
|
|
358
377
|
:param filters: Additional filters for queryables.
|
|
359
|
-
:param
|
|
360
|
-
:param
|
|
361
|
-
:param
|
|
362
|
-
:param alias: (optional) alias of the
|
|
378
|
+
:param available_collections: list of available collections
|
|
379
|
+
:param collection_configs: dict containing the collection information for all used collections
|
|
380
|
+
:param collection: (optional) The collection.
|
|
381
|
+
:param alias: (optional) alias of the collection
|
|
363
382
|
|
|
364
383
|
:return: A dictionary containing the queryable properties, associating parameters to their
|
|
365
384
|
annotated type.
|
|
366
385
|
"""
|
|
367
386
|
additional_info = (
|
|
368
|
-
"Please select a
|
|
369
|
-
if not
|
|
387
|
+
"Please select a collection to get the possible values of the parameters!"
|
|
388
|
+
if not collection
|
|
370
389
|
else ""
|
|
371
390
|
)
|
|
372
391
|
discover_metadata = getattr(self.config, "discover_metadata", {})
|
|
373
392
|
auto_discovery = discover_metadata.get("auto_discovery", False)
|
|
374
393
|
|
|
375
|
-
if
|
|
394
|
+
if collection or getattr(self.config, "discover_queryables", {}).get(
|
|
376
395
|
"fetch_url", ""
|
|
377
396
|
):
|
|
378
|
-
if
|
|
379
|
-
self.config.
|
|
380
|
-
queryables = self.
|
|
397
|
+
if collection:
|
|
398
|
+
self.config.collection_config = collection_configs[collection]
|
|
399
|
+
queryables = self._get_collection_queryables(collection, alias, filters)
|
|
381
400
|
queryables.additional_information = additional_info
|
|
382
401
|
queryables.additional_properties = auto_discovery
|
|
383
402
|
|
|
384
403
|
return queryables
|
|
385
404
|
else:
|
|
386
405
|
all_queryables: dict[str, Any] = {}
|
|
387
|
-
for pt in
|
|
388
|
-
self.config.
|
|
389
|
-
pt_queryables = self.
|
|
406
|
+
for pt in available_collections:
|
|
407
|
+
self.config.collection_config = collection_configs[pt]
|
|
408
|
+
pt_queryables = self._get_collection_queryables(pt, None, filters)
|
|
390
409
|
all_queryables.update(pt_queryables)
|
|
391
|
-
# reset defaults because they may vary between
|
|
410
|
+
# reset defaults because they may vary between collections
|
|
392
411
|
for k, v in all_queryables.items():
|
|
393
412
|
v.__metadata__[0].default = getattr(
|
|
394
413
|
Queryables.model_fields.get(k, Field(None)), "default", None
|
|
@@ -399,17 +418,46 @@ class Search(PluginTopic):
|
|
|
399
418
|
**all_queryables,
|
|
400
419
|
)
|
|
401
420
|
|
|
421
|
+
def validate(
|
|
422
|
+
self,
|
|
423
|
+
search_params: dict[str, Any],
|
|
424
|
+
auth: Optional[Union[AuthBase, S3ServiceResource]],
|
|
425
|
+
) -> None:
|
|
426
|
+
"""Validate a search request.
|
|
427
|
+
|
|
428
|
+
:param search_params: Arguments of the search request
|
|
429
|
+
:param auth: Authentication object
|
|
430
|
+
:raises: :class:`~eodag.utils.exceptions.ValidationError`
|
|
431
|
+
"""
|
|
432
|
+
logger.debug("Validate request")
|
|
433
|
+
# attach authentication if required
|
|
434
|
+
if getattr(self.config, "need_auth", False) and auth:
|
|
435
|
+
self.auth = auth
|
|
436
|
+
try:
|
|
437
|
+
collection = search_params.get("collection")
|
|
438
|
+
if not collection:
|
|
439
|
+
raise ValidationError("Field required: collection")
|
|
440
|
+
self.list_queryables(
|
|
441
|
+
filters=search_params,
|
|
442
|
+
available_collections=[collection],
|
|
443
|
+
collection_configs={collection: self.config.collection_config},
|
|
444
|
+
collection=collection,
|
|
445
|
+
alias=collection,
|
|
446
|
+
).get_model().model_validate(search_params)
|
|
447
|
+
except PydanticValidationError as e:
|
|
448
|
+
raise ValidationError(format_pydantic_error(e)) from e
|
|
449
|
+
|
|
402
450
|
def queryables_from_metadata_mapping(
|
|
403
|
-
self,
|
|
451
|
+
self, collection: Optional[str] = None, alias: Optional[str] = None
|
|
404
452
|
) -> dict[str, Annotated[Any, FieldInfo]]:
|
|
405
453
|
"""
|
|
406
|
-
Extract queryable parameters from
|
|
407
|
-
:param
|
|
408
|
-
:param alias: (optional) alias of the
|
|
454
|
+
Extract queryable parameters from collection metadata mapping.
|
|
455
|
+
:param collection: collection id (optional)
|
|
456
|
+
:param alias: (optional) alias of the collection
|
|
409
457
|
:returns: dict of annotated queryables
|
|
410
458
|
"""
|
|
411
459
|
metadata_mapping: dict[str, Any] = deepcopy(
|
|
412
|
-
self.get_metadata_mapping(
|
|
460
|
+
self.get_metadata_mapping(collection)
|
|
413
461
|
)
|
|
414
462
|
|
|
415
463
|
queryables: dict[str, Annotated[Any, FieldInfo]] = {}
|
|
@@ -423,10 +471,13 @@ class Search(PluginTopic):
|
|
|
423
471
|
eodag_queryables = copy_deepcopy(
|
|
424
472
|
model_fields_to_annotated(Queryables.model_fields)
|
|
425
473
|
)
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
474
|
+
queryables["collection"] = eodag_queryables.pop("collection")
|
|
475
|
+
# add default value for collection
|
|
476
|
+
if collection_or_alias := alias or collection:
|
|
477
|
+
queryables["collection"] = Annotated[
|
|
478
|
+
str, Field(default=collection_or_alias)
|
|
479
|
+
]
|
|
480
|
+
|
|
430
481
|
for k, v in eodag_queryables.items():
|
|
431
482
|
eodag_queryable_field_info = (
|
|
432
483
|
get_args(v)[1] if len(get_args(v)) > 1 else None
|