eodag 4.0.0a5__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 (38) hide show
  1. eodag/api/collection.py +65 -1
  2. eodag/api/core.py +48 -16
  3. eodag/api/product/_assets.py +1 -1
  4. eodag/api/product/_product.py +108 -15
  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 +7 -7
  14. eodag/config.py +22 -4
  15. eodag/plugins/download/aws.py +3 -1
  16. eodag/plugins/download/http.py +4 -10
  17. eodag/plugins/search/base.py +8 -3
  18. eodag/plugins/search/build_search_result.py +108 -120
  19. eodag/plugins/search/cop_marine.py +3 -1
  20. eodag/plugins/search/qssearch.py +7 -6
  21. eodag/resources/collections.yml +255 -0
  22. eodag/resources/ext_collections.json +1 -1
  23. eodag/resources/ext_product_types.json +1 -1
  24. eodag/resources/providers.yml +60 -25
  25. eodag/resources/user_conf_template.yml +6 -0
  26. eodag/types/__init__.py +22 -16
  27. eodag/types/download_args.py +3 -1
  28. eodag/types/queryables.py +125 -55
  29. eodag/types/stac_extensions.py +408 -0
  30. eodag/types/stac_metadata.py +312 -0
  31. eodag/utils/__init__.py +42 -4
  32. eodag/utils/dates.py +202 -2
  33. {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/METADATA +7 -13
  34. {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/RECORD +38 -36
  35. {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/WHEEL +1 -1
  36. {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/entry_points.txt +1 -1
  37. {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/licenses/LICENSE +0 -0
  38. {eodag-4.0.0a5.dist-info → eodag-4.0.0b1.dist-info}/top_level.txt +0 -0
@@ -590,9 +590,6 @@
590
590
  version: '$.properties.version'
591
591
  created: '$.properties.dhusIngestDate'
592
592
  updated: '$.properties.updated'
593
- sar:instrument_mode:
594
- - 'sensorMode'
595
- - '$.properties.sensorMode'
596
593
 
597
594
  # OpenSearch Parameters for Acquistion Parameters Search (Table 6)
598
595
  start_datetime:
@@ -800,7 +797,7 @@
800
797
  - '$.Attributes.processingCenter'
801
798
  processing:version:
802
799
  - null
803
- - '$.Attributes.processorVersion'
800
+ - '{$.Attributes.processorVersion#to_geojson}'
804
801
  _processor_name:
805
802
  - null
806
803
  - '$.Attributes.processorName'
@@ -1676,12 +1673,9 @@
1676
1673
  CAMS_SOLAR_RADIATION:
1677
1674
  dataset: cams-solar-radiation-timeseries
1678
1675
  metadata_mapping:
1679
- latitude:
1680
- - '{{"location": {{"latitude": {"latitude"}, "longitude": {"longitude"}}}}}'
1681
- - $."latitude"
1682
- longitude:
1683
- - '{{"location": {{"latitude": {"latitude"}, "longitude": {"longitude"}}}}}'
1684
- - $."longitude"
1676
+ geometry:
1677
+ - '{{"location": {{"longitude": {geometry#to_longitude_latitude}["lon"], "latitude": {geometry#to_longitude_latitude}["lat"]}}}}'
1678
+ - $.geometry
1685
1679
  CAMS_GREENHOUSE_EGG4_MONTHLY:
1686
1680
  dataset: cams-global-ghg-reanalysis-egg4-monthly
1687
1681
  metadata_mapping:
@@ -2444,7 +2438,7 @@
2444
2438
  - '$.Attributes.processingCenter'
2445
2439
  processing:version:
2446
2440
  - null
2447
- - '$.Attributes.processorVersion'
2441
+ - '{$.Attributes.processorVersion#to_geojson}'
2448
2442
  _processor_name:
2449
2443
  - null
2450
2444
  - '$.Attributes.processorName'
@@ -2972,8 +2966,8 @@
2972
2966
  platform: properties.platform
2973
2967
  metadata_mapping:
2974
2968
  grid:code:
2975
- - '{{"query":{{"s2:mgrs_tile":{{"eq":"{grid:code}"}}}}}}'
2976
- - '$.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")}'
2977
2971
  products:
2978
2972
  S1_SAR_GRD:
2979
2973
  _collection: sentinel-1-grd
@@ -3044,6 +3038,18 @@
3044
3038
  end_datetime:
3045
3039
  - '{{"query":{{"start_datetime":{{"lte":"{end_datetime#to_iso_utc_datetime}"}}}}}}'
3046
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'
3047
3053
  products:
3048
3054
  GENERIC_COLLECTION:
3049
3055
  _collection: '{collection}'
@@ -3550,7 +3556,7 @@
3550
3556
  prefix: EO:ECMWF:DAT:CAMS
3551
3557
  discover_queryables:
3552
3558
  fetch_url: null
3553
- product_type_fetch_url: null
3559
+ collection_fetch_url: null
3554
3560
  constraints_url: https://ads.atmosphere.copernicus.eu/api/catalogue/v1/collections/{dataset#wekeo_to_cop_collection(EO:ECMWF:DAT:)}/constraints.json
3555
3561
  form_url: https://ads.atmosphere.copernicus.eu/api/catalogue/v1/collections/{dataset#wekeo_to_cop_collection(EO:ECMWF:DAT:)}/form.json
3556
3562
  - collection_selector: # cop_cds
@@ -3576,7 +3582,7 @@
3576
3582
  prefix: EO:ECMWF:DAT:CEMS
3577
3583
  discover_queryables:
3578
3584
  fetch_url: null
3579
- product_type_fetch_url: null
3585
+ collection_fetch_url: null
3580
3586
  constraints_url: https://ewds.climate.copernicus.eu/api/catalogue/v1/collections/{dataset#wekeo_to_cop_collection(EO:ECMWF:DAT:)}/constraints.json
3581
3587
  form_url: https://ewds.climate.copernicus.eu/api/catalogue/v1/collections/{dataset#wekeo_to_cop_collection(EO:ECMWF:DAT:)}/form.json
3582
3588
  metadata_mapping:
@@ -3587,7 +3593,8 @@
3587
3593
  - '{{"startdate": "{start_datetime#to_iso_utc_datetime}"}}'
3588
3594
  - $.properties.startdate
3589
3595
  end_datetime:
3590
- - '{{"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}"}}'
3591
3598
  - $.properties.enddate
3592
3599
  product:type:
3593
3600
  - dataset_id
@@ -3762,7 +3769,8 @@
3762
3769
  dataset: EO:ECMWF:DAT:CAMS_SOLAR_RADIATION_TIMESERIES
3763
3770
  metadata_mapping:
3764
3771
  geometry:
3765
- - '{{"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"]}}}}'
3766
3774
  - '$.null'
3767
3775
  auth: !plugin
3768
3776
  type: TokenAuth
@@ -4052,7 +4060,7 @@
4052
4060
  - '$.Attributes.processingCenter'
4053
4061
  processing:version:
4054
4062
  - null
4055
- - '$.Attributes.processorVersion'
4063
+ - '{$.Attributes.processorVersion#to_geojson}'
4056
4064
  _processor_name:
4057
4065
  - null
4058
4066
  - '$.Attributes.processorName'
@@ -5467,7 +5475,7 @@
5467
5475
  product:type:
5468
5476
  - type
5469
5477
  - '$.properties.productInformation.productType'
5470
- constellation: '$.properties.acquisitionInformation[0].platform.platformShortName'
5478
+ platform: '$.properties.acquisitionInformation[0].platform.platformShortName'
5471
5479
  instruments: '{$.properties.acquisitionInformation[0].instrument.instrumentShortName#split( )}'
5472
5480
  # INSPIRE obligated OpenSearch Parameters for Collection Search (Table 4)
5473
5481
  title: '{$.properties.title#sanitize}'
@@ -5630,6 +5638,15 @@
5630
5638
  metadata_mapping:
5631
5639
  <<: *orbit_zone_tile
5632
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'
5633
5650
  # S3 SLSTR
5634
5651
  S3_SLSTR_L1RBT:
5635
5652
  product:type: SL_1_RBT___
@@ -5834,6 +5851,16 @@
5834
5851
  _collection: EO:EUM:DAT:0684
5835
5852
  MTG_FCI_OLR:
5836
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
5837
5864
  GENERIC_COLLECTION:
5838
5865
  _collection: '{collection}'
5839
5866
  download: !plugin
@@ -6079,6 +6106,9 @@
6079
6106
  sat:absolute_orbit:
6080
6107
  - '{{"query":{{"sat:absolute_orbit":{{"eq":{sat:absolute_orbit}}}}}}}'
6081
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}'
6082
6112
  sat:relative_orbit:
6083
6113
  - '{{"query":{{"sat:relative_orbit":{{"eq":{sat:relative_orbit}}}}}}}'
6084
6114
  - '$.properties."sat:relative_orbit"'
@@ -6106,6 +6136,7 @@
6106
6136
  grid:code:
6107
6137
  - '{{"query":{{"grid:code":{{"contains":"{grid:code#replace_str("MGRS-","T")}"}}}}}}'
6108
6138
  - '{$.properties."grid:code"#replace_str(r"^T?(.*)$",r"MGRS-\1")}'
6139
+ sci:doi: '{$.properties.sci:doi#replace_str(r"^\[\]$","Not Available")}'
6109
6140
  published: '$.properties.datetime'
6110
6141
  eodag:download_link: '$.assets[?(@.roles[0] == "data") & (@.type != "application/xml")].href'
6111
6142
  eodag:quicklook: '$.assets[?(@.roles[0] == "overview")].href.`sub(/^(.*)$/, \\1?scope=gdh)`'
@@ -6131,11 +6162,11 @@
6131
6162
  S2_MSI_L1C:
6132
6163
  _collection: PEPS_S2_L1C
6133
6164
  S2_MSI_L2A_MAJA:
6134
- _collection: MUSCATE_SENTINEL2_SENTINEL2_L2A
6165
+ _collection: THEIA_REFLECTANCE_SENTINEL2_L2A
6135
6166
  S2_MSI_L2B_MAJA_SNOW:
6136
- _collection: MUSCATE_Snow_SENTINEL2_L2B-SNOW
6167
+ _collection: THEIA_SNOW_SENTINEL2_L2B
6137
6168
  S2_MSI_L2B_MAJA_WATER:
6138
- _collection: MUSCATE_WaterQual_SENTINEL2_L2B-WATER
6169
+ _collection: THEIA_WATERQUAL_SENTINEL2_L2B
6139
6170
  GENERIC_COLLECTION:
6140
6171
  _collection: '{collection}'
6141
6172
  download: !plugin
@@ -6209,6 +6240,9 @@
6209
6240
  sat:absolute_orbit:
6210
6241
  - '{{"query":{{"sat:absolute_orbit":{{"eq":{sat:absolute_orbit}}}}}}}'
6211
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}'
6212
6246
  sat:relative_orbit:
6213
6247
  - '{{"query":{{"sat:relative_orbit":{{"eq":{sat:relative_orbit}}}}}}}'
6214
6248
  - '$.properties."sat:relative_orbit"'
@@ -6236,6 +6270,7 @@
6236
6270
  grid:code:
6237
6271
  - '{{"query":{{"grid:code":{{"contains":"{grid:code#replace_str("MGRS-","T")}"}}}}}}'
6238
6272
  - '{$.properties."grid:code"#replace_str(r"^T?(.*)$",r"MGRS-\1")}'
6273
+ sci:doi: '{$.properties.sci:doi#replace_str(r"^\[\]$","Not Available")}'
6239
6274
  published: '$.properties.datetime'
6240
6275
  eodag:download_link: '$.properties.endpoint_url'
6241
6276
  eodag:quicklook: '$.assets[?(@.roles[0] == "overview")].href.`sub(/^(.*)$/, \\1?scope=gdh)`'
@@ -6261,11 +6296,11 @@
6261
6296
  S2_MSI_L1C:
6262
6297
  _collection: PEPS_S2_L1C
6263
6298
  S2_MSI_L2A_MAJA:
6264
- _collection: MUSCATE_SENTINEL2_SENTINEL2_L2A
6299
+ _collection: THEIA_REFLECTANCE_SENTINEL2_L2A
6265
6300
  S2_MSI_L2B_MAJA_SNOW:
6266
- _collection: MUSCATE_Snow_SENTINEL2_L2B-SNOW
6301
+ _collection: THEIA_SNOW_SENTINEL2_L2B
6267
6302
  S2_MSI_L2B_MAJA_WATER:
6268
- _collection: MUSCATE_WaterQual_SENTINEL2_L2B-WATER
6303
+ _collection: THEIA_WATERQUAL_SENTINEL2_L2B
6269
6304
  GENERIC_COLLECTION:
6270
6305
  _collection: '{collection}'
6271
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)