eodag 4.0.0a4__py3-none-any.whl → 4.0.0b1__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.
Files changed (42) hide show
  1. eodag/api/collection.py +65 -1
  2. eodag/api/core.py +65 -19
  3. eodag/api/product/_assets.py +1 -1
  4. eodag/api/product/_product.py +133 -18
  5. eodag/api/product/drivers/__init__.py +3 -1
  6. eodag/api/product/drivers/base.py +3 -1
  7. eodag/api/product/drivers/generic.py +9 -5
  8. eodag/api/product/drivers/sentinel1.py +14 -9
  9. eodag/api/product/drivers/sentinel2.py +14 -7
  10. eodag/api/product/metadata_mapping.py +5 -2
  11. eodag/api/provider.py +1 -0
  12. eodag/api/search_result.py +4 -1
  13. eodag/cli.py +17 -8
  14. eodag/config.py +22 -4
  15. eodag/plugins/apis/ecmwf.py +3 -24
  16. eodag/plugins/apis/usgs.py +3 -24
  17. eodag/plugins/download/aws.py +85 -44
  18. eodag/plugins/download/base.py +117 -41
  19. eodag/plugins/download/http.py +88 -65
  20. eodag/plugins/search/base.py +8 -3
  21. eodag/plugins/search/build_search_result.py +108 -120
  22. eodag/plugins/search/cop_marine.py +3 -1
  23. eodag/plugins/search/qssearch.py +7 -6
  24. eodag/resources/collections.yml +255 -0
  25. eodag/resources/ext_collections.json +1 -1
  26. eodag/resources/ext_product_types.json +1 -1
  27. eodag/resources/providers.yml +62 -25
  28. eodag/resources/user_conf_template.yml +6 -0
  29. eodag/types/__init__.py +22 -16
  30. eodag/types/download_args.py +3 -1
  31. eodag/types/queryables.py +125 -55
  32. eodag/types/stac_extensions.py +408 -0
  33. eodag/types/stac_metadata.py +312 -0
  34. eodag/utils/__init__.py +42 -4
  35. eodag/utils/dates.py +202 -2
  36. eodag/utils/s3.py +4 -4
  37. {eodag-4.0.0a4.dist-info → eodag-4.0.0b1.dist-info}/METADATA +7 -13
  38. {eodag-4.0.0a4.dist-info → eodag-4.0.0b1.dist-info}/RECORD +42 -40
  39. {eodag-4.0.0a4.dist-info → eodag-4.0.0b1.dist-info}/WHEEL +1 -1
  40. {eodag-4.0.0a4.dist-info → eodag-4.0.0b1.dist-info}/entry_points.txt +1 -1
  41. {eodag-4.0.0a4.dist-info → eodag-4.0.0b1.dist-info}/licenses/LICENSE +0 -0
  42. {eodag-4.0.0a4.dist-info → eodag-4.0.0b1.dist-info}/top_level.txt +0 -0
@@ -49,6 +49,7 @@
49
49
  usgs:productId: '$.id'
50
50
  extract: True
51
51
  order_enabled: true
52
+ max_workers: 2
52
53
  products:
53
54
  # datasets list http://kapadia.github.io/usgs/_sources/reference/catalog/ee.txt may be outdated
54
55
  # see also https://dds.cr.usgs.gov/ee-data/coveragemaps/shp/ee/
@@ -589,9 +590,6 @@
589
590
  version: '$.properties.version'
590
591
  created: '$.properties.dhusIngestDate'
591
592
  updated: '$.properties.updated'
592
- sar:instrument_mode:
593
- - 'sensorMode'
594
- - '$.properties.sensorMode'
595
593
 
596
594
  # OpenSearch Parameters for Acquistion Parameters Search (Table 6)
597
595
  start_datetime:
@@ -799,7 +797,7 @@
799
797
  - '$.Attributes.processingCenter'
800
798
  processing:version:
801
799
  - null
802
- - '$.Attributes.processorVersion'
800
+ - '{$.Attributes.processorVersion#to_geojson}'
803
801
  _processor_name:
804
802
  - null
805
803
  - '$.Attributes.processorName'
@@ -1675,12 +1673,9 @@
1675
1673
  CAMS_SOLAR_RADIATION:
1676
1674
  dataset: cams-solar-radiation-timeseries
1677
1675
  metadata_mapping:
1678
- latitude:
1679
- - '{{"location": {{"latitude": {"latitude"}, "longitude": {"longitude"}}}}}'
1680
- - $."latitude"
1681
- longitude:
1682
- - '{{"location": {{"latitude": {"latitude"}, "longitude": {"longitude"}}}}}'
1683
- - $."longitude"
1676
+ geometry:
1677
+ - '{{"location": {{"longitude": {geometry#to_longitude_latitude}["lon"], "latitude": {geometry#to_longitude_latitude}["lat"]}}}}'
1678
+ - $.geometry
1684
1679
  CAMS_GREENHOUSE_EGG4_MONTHLY:
1685
1680
  dataset: cams-global-ghg-reanalysis-egg4-monthly
1686
1681
  metadata_mapping:
@@ -2443,7 +2438,7 @@
2443
2438
  - '$.Attributes.processingCenter'
2444
2439
  processing:version:
2445
2440
  - null
2446
- - '$.Attributes.processorVersion'
2441
+ - '{$.Attributes.processorVersion#to_geojson}'
2447
2442
  _processor_name:
2448
2443
  - null
2449
2444
  - '$.Attributes.processorName'
@@ -2558,6 +2553,7 @@
2558
2553
  extract: true
2559
2554
  order_enabled: false
2560
2555
  archive_depth: 2
2556
+ max_workers: 4
2561
2557
  ssl_verify: true
2562
2558
  auth: !plugin
2563
2559
  type: KeycloakOIDCPasswordAuth
@@ -2970,8 +2966,8 @@
2970
2966
  platform: properties.platform
2971
2967
  metadata_mapping:
2972
2968
  grid:code:
2973
- - '{{"query":{{"s2:mgrs_tile":{{"eq":"{grid:code}"}}}}}}'
2974
- - '$.properties."s2:mgrs_tile"'
2969
+ - '{{"query":{{"s2:mgrs_tile":{{"eq":"{grid:code#replace_str("MGRS-","")}"}}}}}}'
2970
+ - '{$.properties."s2:mgrs_tile"#replace_str(r"^T?(.*)$",r"MGRS-\1")}'
2975
2971
  products:
2976
2972
  S1_SAR_GRD:
2977
2973
  _collection: sentinel-1-grd
@@ -3042,6 +3038,18 @@
3042
3038
  end_datetime:
3043
3039
  - '{{"query":{{"start_datetime":{{"lte":"{end_datetime#to_iso_utc_datetime}"}}}}}}'
3044
3040
  - '$.properties.end_datetime'
3041
+ processing:software:
3042
+ - '{{"query":{{"processing:software":{{"eq":{processing:software}}}}}}}'
3043
+ - '$.properties.processing:software'
3044
+ spatial:cycle_id:
3045
+ - '{{"query":{{"spatial:cycle_id":{{"eq":{spatial:cycle_id}}}}}}}'
3046
+ - '$.properties.spatial:cycle_id'
3047
+ spatial:pass_id:
3048
+ - '{{"query":{{"spatial:pass_id":{{"eq":{spatial:pass_id}}}}}}}'
3049
+ - '$.properties.spatial:pass_id'
3050
+ spatial:scene_id:
3051
+ - '{{"query":{{"spatial:scene_id":{{"eq":{spatial:scene_id}}}}}}}'
3052
+ - '$.properties.spatial:scene_id'
3045
3053
  products:
3046
3054
  GENERIC_COLLECTION:
3047
3055
  _collection: '{collection}'
@@ -3548,7 +3556,7 @@
3548
3556
  prefix: EO:ECMWF:DAT:CAMS
3549
3557
  discover_queryables:
3550
3558
  fetch_url: null
3551
- product_type_fetch_url: null
3559
+ collection_fetch_url: null
3552
3560
  constraints_url: https://ads.atmosphere.copernicus.eu/api/catalogue/v1/collections/{dataset#wekeo_to_cop_collection(EO:ECMWF:DAT:)}/constraints.json
3553
3561
  form_url: https://ads.atmosphere.copernicus.eu/api/catalogue/v1/collections/{dataset#wekeo_to_cop_collection(EO:ECMWF:DAT:)}/form.json
3554
3562
  - collection_selector: # cop_cds
@@ -3574,7 +3582,7 @@
3574
3582
  prefix: EO:ECMWF:DAT:CEMS
3575
3583
  discover_queryables:
3576
3584
  fetch_url: null
3577
- product_type_fetch_url: null
3585
+ collection_fetch_url: null
3578
3586
  constraints_url: https://ewds.climate.copernicus.eu/api/catalogue/v1/collections/{dataset#wekeo_to_cop_collection(EO:ECMWF:DAT:)}/constraints.json
3579
3587
  form_url: https://ewds.climate.copernicus.eu/api/catalogue/v1/collections/{dataset#wekeo_to_cop_collection(EO:ECMWF:DAT:)}/form.json
3580
3588
  metadata_mapping:
@@ -3585,7 +3593,8 @@
3585
3593
  - '{{"startdate": "{start_datetime#to_iso_utc_datetime}"}}'
3586
3594
  - $.properties.startdate
3587
3595
  end_datetime:
3588
- - '{{"enddate": "{end_datetime#to_iso_utc_datetime}"}}'
3596
+ # map 'date' to validate request against copernicus queryables
3597
+ - '{{"enddate": "{end_datetime#to_iso_utc_datetime}", "date": "{start_datetime#to_iso_date}/{end_datetime#to_iso_date}"}}'
3589
3598
  - $.properties.enddate
3590
3599
  product:type:
3591
3600
  - dataset_id
@@ -3760,7 +3769,8 @@
3760
3769
  dataset: EO:ECMWF:DAT:CAMS_SOLAR_RADIATION_TIMESERIES
3761
3770
  metadata_mapping:
3762
3771
  geometry:
3763
- - '{{"longitude": {geometry#to_longitude_latitude}["lon"], "latitude": {geometry#to_longitude_latitude}["lat"]}}'
3772
+ # longitude/latitude to order from wekeo_ecmwf, location to validate against cop_ads constraints
3773
+ - '{{"longitude": {geometry#to_longitude_latitude}["lon"], "latitude": {geometry#to_longitude_latitude}["lat"], "location": {{"longitude": {geometry#to_longitude_latitude}["lon"], "latitude": {geometry#to_longitude_latitude}["lat"]}}}}'
3764
3774
  - '$.null'
3765
3775
  auth: !plugin
3766
3776
  type: TokenAuth
@@ -4050,7 +4060,7 @@
4050
4060
  - '$.Attributes.processingCenter'
4051
4061
  processing:version:
4052
4062
  - null
4053
- - '$.Attributes.processorVersion'
4063
+ - '{$.Attributes.processorVersion#to_geojson}'
4054
4064
  _processor_name:
4055
4065
  - null
4056
4066
  - '$.Attributes.processorName'
@@ -5465,7 +5475,7 @@
5465
5475
  product:type:
5466
5476
  - type
5467
5477
  - '$.properties.productInformation.productType'
5468
- constellation: '$.properties.acquisitionInformation[0].platform.platformShortName'
5478
+ platform: '$.properties.acquisitionInformation[0].platform.platformShortName'
5469
5479
  instruments: '{$.properties.acquisitionInformation[0].instrument.instrumentShortName#split( )}'
5470
5480
  # INSPIRE obligated OpenSearch Parameters for Collection Search (Table 4)
5471
5481
  title: '{$.properties.title#sanitize}'
@@ -5628,6 +5638,15 @@
5628
5638
  metadata_mapping:
5629
5639
  <<: *orbit_zone_tile
5630
5640
  <<: *sentinel_params
5641
+ S3_OL_2_WFRBC003:
5642
+ product:type: OL_2_WFR___
5643
+ _collection: EO:EUM:DAT:0556
5644
+ metadata_mapping:
5645
+ <<: *orbit_zone_tile
5646
+ <<: *sentinel_params
5647
+ platform:
5648
+ - sat
5649
+ - '$.properties.acquisitionInformation[0].platform.platformShortName'
5631
5650
  # S3 SLSTR
5632
5651
  S3_SLSTR_L1RBT:
5633
5652
  product:type: SL_1_RBT___
@@ -5832,6 +5851,16 @@
5832
5851
  _collection: EO:EUM:DAT:0684
5833
5852
  MTG_FCI_OLR:
5834
5853
  _collection: EO:EUM:DAT:0685
5854
+ MSG_SEVIRI_RSS_AMV_CDR_V1:
5855
+ _collection: EO:EUM:DAT:1083
5856
+ MSG_SEVIRI_RSS_HR_IMG_L1_5_V1:
5857
+ _collection: EO:EUM:DAT:0962
5858
+ MSG_SEVIRI_SARAH_CDR_V003:
5859
+ _collection: EO:EUM:DAT:0863
5860
+ MTG_FCI_ACTIVE_FIRE_L2_V1:
5861
+ _collection: EO:EUM:DAT:0682
5862
+ MULT_PMW_IR_GIRAFE_PRECIP_CDR_V001:
5863
+ _collection: EO:EUM:DAT:0921
5835
5864
  GENERIC_COLLECTION:
5836
5865
  _collection: '{collection}'
5837
5866
  download: !plugin
@@ -6077,6 +6106,9 @@
6077
6106
  sat:absolute_orbit:
6078
6107
  - '{{"query":{{"sat:absolute_orbit":{{"eq":{sat:absolute_orbit}}}}}}}'
6079
6108
  - '$.properties."sat:absolute_orbit"'
6109
+ sat:orbit_state:
6110
+ - '{{"query":{{"sat:orbit_state":{{"eq":{sat:orbit_state}}}}}}}'
6111
+ - '{$.properties.sat:orbit_state#to_lower}'
6080
6112
  sat:relative_orbit:
6081
6113
  - '{{"query":{{"sat:relative_orbit":{{"eq":{sat:relative_orbit}}}}}}}'
6082
6114
  - '$.properties."sat:relative_orbit"'
@@ -6104,6 +6136,7 @@
6104
6136
  grid:code:
6105
6137
  - '{{"query":{{"grid:code":{{"contains":"{grid:code#replace_str("MGRS-","T")}"}}}}}}'
6106
6138
  - '{$.properties."grid:code"#replace_str(r"^T?(.*)$",r"MGRS-\1")}'
6139
+ sci:doi: '{$.properties.sci:doi#replace_str(r"^\[\]$","Not Available")}'
6107
6140
  published: '$.properties.datetime'
6108
6141
  eodag:download_link: '$.assets[?(@.roles[0] == "data") & (@.type != "application/xml")].href'
6109
6142
  eodag:quicklook: '$.assets[?(@.roles[0] == "overview")].href.`sub(/^(.*)$/, \\1?scope=gdh)`'
@@ -6129,11 +6162,11 @@
6129
6162
  S2_MSI_L1C:
6130
6163
  _collection: PEPS_S2_L1C
6131
6164
  S2_MSI_L2A_MAJA:
6132
- _collection: MUSCATE_SENTINEL2_SENTINEL2_L2A
6165
+ _collection: THEIA_REFLECTANCE_SENTINEL2_L2A
6133
6166
  S2_MSI_L2B_MAJA_SNOW:
6134
- _collection: MUSCATE_Snow_SENTINEL2_L2B-SNOW
6167
+ _collection: THEIA_SNOW_SENTINEL2_L2B
6135
6168
  S2_MSI_L2B_MAJA_WATER:
6136
- _collection: MUSCATE_WaterQual_SENTINEL2_L2B-WATER
6169
+ _collection: THEIA_WATERQUAL_SENTINEL2_L2B
6137
6170
  GENERIC_COLLECTION:
6138
6171
  _collection: '{collection}'
6139
6172
  download: !plugin
@@ -6207,6 +6240,9 @@
6207
6240
  sat:absolute_orbit:
6208
6241
  - '{{"query":{{"sat:absolute_orbit":{{"eq":{sat:absolute_orbit}}}}}}}'
6209
6242
  - '$.properties."sat:absolute_orbit"'
6243
+ sat:orbit_state:
6244
+ - '{{"query":{{"sat:orbit_state":{{"eq":{sat:orbit_state}}}}}}}'
6245
+ - '{$.properties.sat:orbit_state#to_lower}'
6210
6246
  sat:relative_orbit:
6211
6247
  - '{{"query":{{"sat:relative_orbit":{{"eq":{sat:relative_orbit}}}}}}}'
6212
6248
  - '$.properties."sat:relative_orbit"'
@@ -6234,6 +6270,7 @@
6234
6270
  grid:code:
6235
6271
  - '{{"query":{{"grid:code":{{"contains":"{grid:code#replace_str("MGRS-","T")}"}}}}}}'
6236
6272
  - '{$.properties."grid:code"#replace_str(r"^T?(.*)$",r"MGRS-\1")}'
6273
+ sci:doi: '{$.properties.sci:doi#replace_str(r"^\[\]$","Not Available")}'
6237
6274
  published: '$.properties.datetime'
6238
6275
  eodag:download_link: '$.properties.endpoint_url'
6239
6276
  eodag:quicklook: '$.assets[?(@.roles[0] == "overview")].href.`sub(/^(.*)$/, \\1?scope=gdh)`'
@@ -6259,11 +6296,11 @@
6259
6296
  S2_MSI_L1C:
6260
6297
  _collection: PEPS_S2_L1C
6261
6298
  S2_MSI_L2A_MAJA:
6262
- _collection: MUSCATE_SENTINEL2_SENTINEL2_L2A
6299
+ _collection: THEIA_REFLECTANCE_SENTINEL2_L2A
6263
6300
  S2_MSI_L2B_MAJA_SNOW:
6264
- _collection: MUSCATE_Snow_SENTINEL2_L2B-SNOW
6301
+ _collection: THEIA_SNOW_SENTINEL2_L2B
6265
6302
  S2_MSI_L2B_MAJA_WATER:
6266
- _collection: MUSCATE_WaterQual_SENTINEL2_L2B-WATER
6303
+ _collection: THEIA_WATERQUAL_SENTINEL2_L2B
6267
6304
  GENERIC_COLLECTION:
6268
6305
  _collection: '{collection}'
6269
6306
  download: !plugin
@@ -152,6 +152,12 @@ eumetsat_ds:
152
152
  password:
153
153
  download:
154
154
  output_dir:
155
+ fedeo_ceda:
156
+ priority: # Lower value means lower priority (Default: 0)
157
+ search: # Search parameters configuration
158
+ download:
159
+ extract:
160
+ output_dir:
155
161
  geodes:
156
162
  priority: # Lower value means lower priority (Default: 0)
157
163
  search: # Search parameters configuration
eodag/types/__init__.py CHANGED
@@ -19,24 +19,15 @@
19
19
 
20
20
  from __future__ import annotations
21
21
 
22
- from typing import (
23
- Annotated,
24
- Any,
25
- Literal,
26
- Optional,
27
- Type,
28
- TypedDict,
29
- Union,
30
- get_args,
31
- get_origin,
32
- )
22
+ from typing import Annotated, Any, Literal, Optional, Type, Union, get_args, get_origin
33
23
 
34
24
  from annotated_types import Gt, Lt
35
- from pydantic import BaseModel, ConfigDict, Field, create_model
25
+ from pydantic import AliasChoices, BaseModel, ConfigDict, Field, create_model
36
26
  from pydantic.annotated_handlers import GetJsonSchemaHandler
37
27
  from pydantic.fields import FieldInfo
38
28
  from pydantic.json_schema import JsonSchemaValue
39
29
  from pydantic_core import CoreSchema, PydanticUndefined
30
+ from typing_extensions import TypedDict
40
31
 
41
32
  from eodag.utils import copy_deepcopy
42
33
  from eodag.utils.exceptions import ValidationError
@@ -153,7 +144,8 @@ def json_field_definition_to_python(
153
144
  json_field_definition: dict[str, Any],
154
145
  default_value: Optional[Any] = None,
155
146
  required: Optional[bool] = False,
156
- alias: Optional[str] = None,
147
+ validation_alias: Optional[Union[str, AliasChoices]] = None,
148
+ serialization_alias: Optional[str] = None,
157
149
  ) -> Annotated[Any, FieldInfo]:
158
150
  """Get python field definition from json object
159
151
 
@@ -171,6 +163,8 @@ def json_field_definition_to_python(
171
163
  :param json_field_definition: the json field definition
172
164
  :param default_value: default value of the field
173
165
  :param required: if the field is required
166
+ :param validation_alias: validation alias
167
+ :param serialization_alias: serialization alias
174
168
  :returns: the python field definition
175
169
  """
176
170
  python_type = json_type_to_python(json_field_definition.get("type"))
@@ -181,7 +175,8 @@ def json_field_definition_to_python(
181
175
  pattern=json_field_definition.get("pattern", PydanticUndefined),
182
176
  le=json_field_definition.get("maximum", PydanticUndefined),
183
177
  ge=json_field_definition.get("minimum", PydanticUndefined),
184
- alias=alias or PydanticUndefined,
178
+ validation_alias=validation_alias or PydanticUndefined,
179
+ serialization_alias=serialization_alias or PydanticUndefined,
185
180
  )
186
181
 
187
182
  enum = json_field_definition.get("enum")
@@ -202,6 +197,14 @@ def json_field_definition_to_python(
202
197
  enum = items.get("enum")
203
198
  elif "const" in items:
204
199
  const = items.get("const")
200
+ elif python_type is dict:
201
+ properties = json_field_definition.get("properties")
202
+ if isinstance(properties, dict):
203
+ fields_type: dict = {
204
+ k: json_field_definition_to_python(v, required=required)
205
+ for k, v in properties.items()
206
+ }
207
+ python_type = TypedDict("dictionary", fields_type) # type: ignore
205
208
 
206
209
  if enum:
207
210
  literal = Literal[tuple(sorted(enum))] # type: ignore
@@ -386,7 +389,9 @@ class BaseModelCustomJsonSchema(BaseModel):
386
389
 
387
390
 
388
391
  def annotated_dict_to_model(
389
- model_name: str, annotated_fields: dict[str, Annotated[Any, FieldInfo]]
392
+ model_name: str,
393
+ annotated_fields: dict[str, Annotated[Any, FieldInfo]],
394
+ model_class: Optional[type[BaseModel]] = BaseModelCustomJsonSchema,
390
395
  ) -> BaseModel:
391
396
  """Convert a dictionary of Annotated values to a Pydantic BaseModel.
392
397
 
@@ -411,6 +416,7 @@ def annotated_dict_to_model(
411
416
  :param model_name: name of the model to be created
412
417
  :param annotated_fields: dict containing the parameters and annotated values that should become
413
418
  the properties of the model
419
+ :param model_class: (optiional) base class of the returned model
414
420
  :returns: pydantic model
415
421
  """
416
422
  fields = {}
@@ -423,7 +429,7 @@ def annotated_dict_to_model(
423
429
 
424
430
  custom_model = create_model(
425
431
  model_name,
426
- __base__=BaseModelCustomJsonSchema,
432
+ __base__=model_class,
427
433
  **fields, # type: ignore
428
434
  )
429
435
 
@@ -17,7 +17,9 @@
17
17
  # limitations under the License.
18
18
  from __future__ import annotations
19
19
 
20
- from typing import Optional, TypedDict, Union
20
+ from typing import Optional, Union
21
+
22
+ from typing_extensions import TypedDict
21
23
 
22
24
 
23
25
  class DownloadConf(TypedDict, total=False):
eodag/types/queryables.py CHANGED
@@ -1,22 +1,61 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2025, CS GROUP - France, https://www.csgroup.eu/
3
+ #
4
+ # This file is part of EODAG project
5
+ # https://www.github.com/CS-SI/EODAG
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
1
18
  from __future__ import annotations
2
19
 
20
+ import re
3
21
  from collections import UserDict
4
- from typing import Annotated, Any, Optional, Union
22
+ from typing import TYPE_CHECKING, Annotated, Any, Optional, Union, cast
5
23
 
6
24
  from annotated_types import Lt
7
- from pydantic import BaseModel, ConfigDict, Field
25
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
8
26
  from pydantic.fields import FieldInfo
9
27
  from pydantic.types import PositiveInt
10
28
  from pydantic_core import PydanticUndefined
11
29
  from shapely.geometry.base import BaseGeometry
30
+ from typing_extensions import get_args
12
31
 
13
- from eodag.types import annotated_dict_to_model, model_fields_to_annotated
32
+ from eodag.types import (
33
+ BaseModelCustomJsonSchema,
34
+ annotated_dict_to_model,
35
+ model_fields_to_annotated,
36
+ )
37
+ from eodag.types.stac_extensions import STAC_EXTENSIONS
38
+ from eodag.types.stac_metadata import CommonStacMetadata, create_stac_metadata_model
39
+ from eodag.utils.dates import (
40
+ COMPACT_DATE_PATTERN,
41
+ COMPACT_DATE_RANGE_PATTERN,
42
+ DATE_PATTERN,
43
+ DATE_RANGE_PATTERN,
44
+ datetime_range,
45
+ format_date,
46
+ format_date_range,
47
+ is_range_in_range,
48
+ parse_date,
49
+ )
14
50
  from eodag.utils.repr import remove_class_repr, shorter_type_repr
15
51
 
52
+ if TYPE_CHECKING:
53
+ from eodag.types.stac_extensions import BaseStacExtension
54
+
16
55
  Percentage = Annotated[PositiveInt, Lt(100)]
17
56
 
18
57
 
19
- class CommonQueryables(BaseModel):
58
+ class CommonQueryables(BaseModelCustomJsonSchema):
20
59
  """A class representing search common queryable properties."""
21
60
 
22
61
  collection: Annotated[str, Field()]
@@ -47,11 +86,33 @@ class CommonQueryables(BaseModel):
47
86
  f.__metadata__[0].default = default
48
87
  return f
49
88
 
89
+ @classmethod
90
+ def from_stac_models(
91
+ cls,
92
+ extensions: list[BaseStacExtension] = STAC_EXTENSIONS,
93
+ base_model: type[BaseModel] = CommonStacMetadata,
94
+ ) -> type[Queryables]:
95
+ """Creates Queryables from STAC models.
96
+
97
+ :param extensions: list of STAC extensions to include in the model
98
+ :param base_model: base STAC model to use
99
+ :return: Queryables model
100
+ """
101
+ return cast(
102
+ type[Queryables],
103
+ create_stac_metadata_model(
104
+ base_models=[cls, base_model],
105
+ extensions=extensions,
106
+ class_name="Queryables",
107
+ ),
108
+ )
109
+
50
110
 
51
111
  class Queryables(CommonQueryables):
52
112
  """A class representing all search queryable properties.
53
113
 
54
114
  Parameters default value is set to ``None`` to have them not required.
115
+ Fields described here are queryables-specific and complete StacMetadata fields.
55
116
  """
56
117
 
57
118
  start: Annotated[
@@ -78,60 +139,69 @@ class Queryables(CommonQueryables):
78
139
  description="Read EODAG documentation for all supported geometry format.",
79
140
  ),
80
141
  ]
81
- # common metadata
82
- constellation: Annotated[str, Field(None)]
83
- created: Annotated[str, Field(None)]
84
- description: Annotated[str, Field(None)]
85
- gsd: Annotated[int, Field(None)]
86
142
  id: Annotated[str, Field(None)]
87
- instruments: Annotated[str, Field(None)]
88
- keywords: Annotated[str, Field(None)]
89
- license: Annotated[str, Field(None)]
90
- platform: Annotated[str, Field(None)]
91
- providers: Annotated[str, Field(None)]
92
- title: Annotated[str, Field(None)]
93
- uid: Annotated[str, Field(None)]
94
- updated: Annotated[str, Field(None)]
95
- # eo extension
96
- eo_cloud_cover: Annotated[Percentage, Field(None, alias="eo:cloud_cover")]
97
- eo_snow_cover: Annotated[Percentage, Field(None, alias="eo:snow_cover")]
98
- # grid extension
99
- grid_code: Annotated[
100
- str, Field(None, alias="grid:code", pattern=r"^[A-Z0-9]+-[-_.A-Za-z0-9]+$")
101
- ]
102
- # mgrs extension
103
- mgrs_grid_square: Annotated[str, Field(None, alias="mgrs:grid_square")]
104
- mgrs_latitude_band: Annotated[str, Field(None, alias="mgrs:latitude_band")]
105
- mgrs_utm_zone: Annotated[str, Field(None, alias="mgrs:utm_zone")]
106
- # order extension
107
- order_status: Annotated[str, Field(None, alias="order:status")]
108
- # processing extension
109
- processing_level: Annotated[str, Field(None, alias="processing:level")]
110
- # product extension
111
- product_acquisition_type: Annotated[
112
- str, Field(None, alias="product:acquisition_type")
113
- ]
114
- product_timeliness: Annotated[str, Field(None, alias="product:timeliness")]
115
- product_type: Annotated[str, Field(None, alias="product:type")]
116
- # sar extension
117
- sar_beam_ids: Annotated[str, Field(None, alias="sar:beam_ids")]
118
- sar_frequency_band: Annotated[float, Field(None, alias="sar:frequency_band")]
119
- sar_instrument_mode: Annotated[str, Field(None, alias="sar:instrument_mode")]
120
- sar_polarizations: Annotated[list[str], Field(None, alias="sar:polarizations")]
121
- # sat extension
122
- sat_absolute_orbit: Annotated[int, Field(None, alias="sat:absolute_orbit")]
123
- sat_orbit_cycle: Annotated[int, Field(None, alias="sat:orbit_cycle")]
124
- sat_orbit_state: Annotated[str, Field(None, alias="sat:orbit_state")]
125
- sat_relative_orbit: Annotated[int, Field(None, alias="sat:relative_orbit")]
126
- # sci extension
127
- sci_doi: Annotated[str, Field(None, alias="sci:doi")]
128
- # view extension
129
- view_incidence_angle: Annotated[str, Field(None, alias="view:incidence_angle")]
130
- view_sun_azimuth: Annotated[str, Field(None, alias="view:sun_azimuth")]
131
- view_sun_elevation: Annotated[str, Field(None, alias="view:sun_elevation")]
132
143
 
133
144
  model_config = ConfigDict(arbitrary_types_allowed=True)
134
145
 
146
+ @field_validator("ecmwf_date", mode="plain", check_fields=False)
147
+ @classmethod
148
+ def check_date_range(cls, v: str) -> str:
149
+ """Validate date ranges"""
150
+ if not isinstance(v, str):
151
+ raise ValueError(
152
+ "date must be a string formatted as single date ('yyyy-mm-dd') or range ('yyyy-mm-dd/yyyy-mm-dd')"
153
+ )
154
+ date_regex = [
155
+ re.compile(p)
156
+ for p in (
157
+ DATE_PATTERN,
158
+ COMPACT_DATE_PATTERN,
159
+ DATE_RANGE_PATTERN,
160
+ COMPACT_DATE_RANGE_PATTERN,
161
+ )
162
+ ]
163
+ if not any(r.match(v) is not None for r in date_regex):
164
+ raise ValueError(
165
+ "date must be a string formatted as single date ('yyyy-mm-dd') or range ('yyyy-mm-dd/yyyy-mm-dd')"
166
+ )
167
+ try:
168
+ start, end = parse_date(v)
169
+ except ValueError as e:
170
+ raise ValueError("date must follow 'yyyy-mm-dd' format") from e
171
+ if end < start:
172
+ raise ValueError("date range end must be after start")
173
+ # enumerate dates in range
174
+ v_set: set[str] = {format_date(d) for d in datetime_range(start, end)}
175
+ # is_range_in_range() support only ranges (no single date allowed) in the format 'yyyy-mm-dd/yyyy-mm-dd'
176
+ v_range: str = format_date_range(start, end)
177
+
178
+ field_info = cls.model_fields["ecmwf_date"]
179
+ literals = get_args(field_info.annotation)
180
+
181
+ # Collect missing values to report errors
182
+ missing_values = set(v_set)
183
+
184
+ # date constraint may be intervals. We identify intervals with a "/" in the value.
185
+ # date constraint can be a mixed list of single values (e.g "2023-06-27")
186
+ # and intervals (e.g. "2024-11-12/2025-11-20")
187
+ # collections with mixed values: CAMS_GAC_FORECAST, CAMS_EU_AIR_QUALITY_FORECAST
188
+ for literal in literals:
189
+ literal_start, literal_end = parse_date(literal)
190
+ if "/" in literal:
191
+ # range with separator / or /to/
192
+ literal_range: str = format_date_range(literal_start, literal_end)
193
+ if is_range_in_range(literal_range, v_range):
194
+ return v
195
+ else:
196
+ # convert literal to the format 'yyyy-mm-dd'
197
+ literal_start_str = format_date(literal_start)
198
+ if literal_start_str in v_set:
199
+ missing_values.remove(literal_start_str)
200
+ if not missing_values:
201
+ return v
202
+
203
+ raise ValueError("date not allowed")
204
+
135
205
 
136
206
  class QueryablesDict(UserDict[str, Any]):
137
207
  """Class inheriting from UserDict which contains queryables with their annotated type;
@@ -232,4 +302,4 @@ class QueryablesDict(UserDict[str, Any]):
232
302
  :param model_name: name used for :class:`pydantic.BaseModel` creation
233
303
  :return: pydantic BaseModel of the queryables dict
234
304
  """
235
- return annotated_dict_to_model(model_name, self.data)
305
+ return annotated_dict_to_model(model_name, self.data, Queryables)