Repository: CS-SI/eodag
Pull request #1473 (2025-01-15 10:40:38)

fix(plugins): remove default queryables form value
Fixes: #1450

When a queryable is called with an empty value (`""`, `None`, `[]`, etc.) in `dag.list_queryables()`, the returned default value of this queryable must be cleared in order to let other queryables depending on it list all their available values. Now, no default value is returned and the queryable appears as required.

Moreover, if a queryable was called in `dag.list_queryables()` while it was neither a default value of the provider configuration nor in the provider constraints file but it was in the provider queryables form, an error was raised. This is possible now.

`fetch_data()` method has been exposed instead of being initialized in `ECMWFSearch` class initialization to make the code cleaner and allow the mock of this method in tests.

github-actions[bot]: ## Test Results
    4 files  ±0      4 suites  ±0   6m 26s ⏱️ -3s
  587 tests ±0    584 ✅ ±0   3 💤 ±0  0 ❌ ±0 
2 348 runs  ±0  2 250 ✅ ±0  98 💤 ±0  0 ❌ ±0 

Results for commit 8a8bb185. ± Comparison against base commit 4233b05b.

[test-results]:data:application/gzip;base64,H4sIAL+Rh2cC/02MzQ6DIBAGX8Vw7gER3KUv0wCuCak/DcKp6bsXrFKPM9/uvNnoJ9rYvZG3hm3JxwpDCib6dcnYYZ9FnmIZFcJJjy05tyv5V0//Kj9VjMZPWfAqKIQ1HCakpTRFJ/GgsymE4lX9mrreXJo7X5NunWcfMzA0aG2LqiPoB963hNpqDQAESljHHTgtDI3s8wV+gV/3CAEAAA==

github-actions[bot]: ![badge](https://raw.githubusercontent.com/CS-SI/eodag/_xml_coverage_reports/data/default-queryables-form-value-deletion/./badge.svg)


## Code Coverage (Ubuntu)

<details>


```
Filename                                     Stmts    Miss  Cover    Missing
-----------------------------------------  -------  ------  -------  -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
__init__.py                                      8       0  100.00%
cli.py                                         323      64  80.19%   62-63, 87, 674-737, 839-890, 894
config.py                                      430      27  93.72%   83-85, 94, 102, 106-108, 179, 190, 695-697, 810-813, 856-857, 866-867, 972, 1035-1040, 1042
crunch.py                                        5       5  0.00%    20-24
api/__init__.py                                  0       0  100.00%
api/core.py                                    782      74  90.54%   317-318, 368, 658, 702-705, 743, 787, 821, 866-871, 897, 988, 1056, 1194, 1279-1291, 1327, 1329, 1357, 1361-1372, 1385-1391, 1474-1477, 1510-1530, 1582, 1599-1603, 1615-1618, 1954, 1978-1984, 2235, 2239-2243, 2252-2254, 2298-2299, 2328-2329
api/search_result.py                            58       4  93.10%   92, 101, 108, 122
api/product/__init__.py                          6       0  100.00%
api/product/_assets.py                          48       5  89.58%   75, 147, 155, 158-162
api/product/_product.py                        188      20  89.36%   70-72, 238-239, 314, 343, 400, 414-417, 430, 454-457, 500-506
api/product/metadata_mapping.py                695      76  89.06%   131-133, 230, 262-263, 309-310, 320-332, 334, 345, 351-363, 410-411, 448, 469-472, 495, 503-504, 590-591, 615-616, 622-625, 640-641, 790, 836, 989, 998-1002, 1019-1024, 1151, 1165-1185, 1205, 1210, 1339, 1353, 1378, 1424, 1476, 1499-1500, 1516-1520, 1536, 1544
api/product/drivers/__init__.py                  6       0  100.00%
api/product/drivers/base.py                      6       1  83.33%   38
plugins/__init__.py                              0       0  100.00%
plugins/base.py                                 21       2  90.48%   48, 55
plugins/manager.py                             172      14  91.86%   116-121, 171, 209, 231, 235, 259, 399-402, 414-415
plugins/apis/__init__.py                         0       0  100.00%
plugins/apis/base.py                             4       0  100.00%
plugins/apis/ecmwf.py                           97      10  89.69%   170-172, 219-220, 246-248, 301-302
plugins/apis/usgs.py                           180      26  85.56%   155, 257, 291, 326-328, 333, 359-360, 365, 395-402, 413-418, 440-446, 477
plugins/authentication/__init__.py               6       1  83.33%   31
plugins/authentication/aws_auth.py              19       0  100.00%
plugins/authentication/base.py                  17       2  88.24%   43, 56
plugins/authentication/generic.py               14       2  85.71%   51, 61
plugins/authentication/header.py                19       0  100.00%
plugins/authentication/keycloak.py              46       7  84.78%   151-154, 175-180
plugins/authentication/oauth.py                 13       7  46.15%   42-44, 48-51
plugins/authentication/openid_connect.py       206      27  86.89%   80-81, 93-111, 149, 155-183, 191, 323-326, 352
plugins/authentication/qsauth.py                34       1  97.06%   91
plugins/authentication/sas_auth.py              47       1  97.87%   76
plugins/authentication/token.py                100      16  84.00%   141, 170, 172, 207-220, 276-280
plugins/authentication/token_exchange.py        36      14  61.11%   75, 92-120
plugins/crunch/__init__.py                       0       0  100.00%
plugins/crunch/base.py                          10       1  90.00%   43
plugins/crunch/filter_date.py                   59      14  76.27%   52-57, 69, 78, 87, 90, 102-104, 113-115, 122
plugins/crunch/filter_latest_intersect.py       47       8  82.98%   52-53, 69, 78-81, 83, 90-93
plugins/crunch/filter_latest_tpl_name.py        31       1  96.77%   83
plugins/crunch/filter_overlap.py                66      18  72.73%   28-30, 66-69, 76-79, 85, 93, 104-120
plugins/crunch/filter_property.py               30       5  83.33%   55-60, 63-64
plugins/download/__init__.py                     0       0  100.00%
plugins/download/aws.py                        491     163  66.80%   282, 295, 362-365, 379-383, 425-427, 431, 463-464, 470-474, 504, 536, 540, 547, 577-585, 589, 621-629, 640-642, 673-747, 765-823, 834-839, 851-864, 889, 904-906, 909, 919-927, 935-948, 958-989, 996-1008, 1046, 1072, 1117-1119, 1339
plugins/download/base.py                       253      42  83.40%   136, 164, 244-247, 300-301, 349-353, 359-360, 402, 405-419, 431, 435, 499-503, 533-534, 559-567, 569-573, 616, 638, 660, 668
plugins/download/creodias_s3.py                 25       9  64.00%   55-69
plugins/download/http.py                       526     103  80.42%   235, 331-334, 337, 344-349, 367-382, 399, 411, 459, 466-472, 490, 504, 518, 526-528, 544-549, 560, 578, 614-617, 646, 650, 744-811, 829, 859-868, 904, 929-930, 949-954, 960, 963, 980, 983, 998-999, 1026-1027, 1034, 1096, 1111, 1170-1171, 1177, 1187, 1223, 1259, 1279, 1314-1316
plugins/download/s3rest.py                     116      24  79.31%   118, 152, 159, 194, 221-228, 231-233, 237, 248-254, 262-263, 266-270, 293, 314-317
plugins/search/__init__.py                      22       0  100.00%
plugins/search/base.py                         146      11  92.47%   100, 104, 128-134, 274, 294, 433
plugins/search/build_search_result.py          349      48  86.25%   248, 276-277, 313, 316, 390-393, 482-499, 527, 562, 564, 589-596, 628-630, 634, 658, 693, 744, 773-774, 789-804, 847, 872, 875, 883, 995-996, 1002, 1013, 1071, 1122
plugins/search/cop_marine.py                   244      47  80.74%   56, 64-66, 76-77, 82, 87-88, 104, 106, 109, 175-176, 228, 234, 238, 242, 255, 266-267, 275, 303, 307, 322, 326, 330, 334, 338-342, 348-351, 354-368, 385, 434-438, 443, 455
plugins/search/creodias_s3.py                   57       3  94.74%   59, 77, 116
plugins/search/csw.py                          105      81  22.86%   98-99, 103-104, 112-160, 166-179, 187-219, 237-278
plugins/search/data_request_search.py          202      69  65.84%   188-191, 207, 218, 222-223, 234, 239, 244, 251, 264-267, 321-322, 326, 336-342, 347, 373-376, 384-395, 412, 414, 421-424, 426-427, 445-449, 482, 492, 503, 516, 522-537, 542
plugins/search/qssearch.py                     733      89  87.86%   453, 511, 525, 529-535, 563-567, 680-692, 737-740, 811-812, 860, 879, 886, 898, 955, 976, 979-980, 989-990, 999-1000, 1009-1010, 1037, 1112-1117, 1121-1130, 1164, 1186, 1263, 1353, 1438-1439, 1449, 1526-1530, 1592, 1595, 1601-1602, 1623, 1651-1663, 1670, 1702-1704, 1714-1720, 1750, 1773, 1778-1779, 1794, 1810, 1895-1898, 1903-1906, 1915, 1945-1949, 1955
plugins/search/static_stac_search.py            75      13  82.67%   98-124, 140, 153
rest/__init__.py                                 4       2  50.00%   21-22
rest/cache.py                                   33       7  78.79%   35-37, 53-55, 59, 68
rest/config.py                                  25       0  100.00%
rest/constants.py                                6       0  100.00%
rest/core.py                                   260      66  74.62%   258, 266, 284-301, 316-352, 446, 483-522, 705, 712-765
rest/errors.py                                  69       5  92.75%   106, 116, 127, 143-144
rest/server.py                                 192      24  87.50%   94, 117-119, 293-298, 326, 522-524, 541-546, 575, 577, 581-582, 586-587
rest/stac.py                                   319      63  80.25%   308, 330, 382-385, 412-439, 470-472, 495, 527-528, 610-650, 672-688, 780-784, 791, 845-846, 907, 997-999
rest/types/__init__.py                           0       0  100.00%
rest/types/collections_search.py                13      13  0.00%    18-44
rest/types/eodag_search.py                     180       6  96.67%   225-229, 282, 285, 353, 385
rest/types/queryables.py                        57       5  91.23%   93-98, 162
rest/types/stac_search.py                      125       7  94.40%   138, 184, 199-201, 209, 213
rest/utils/__init__.py                          94      12  87.23%   111-112, 131-133, 185, 195-209
rest/utils/cql_evaluate.py                      48       5  89.58%   69, 76, 90, 97, 105
rest/utils/rfc3339.py                           23       3  86.96%   48, 60, 62
types/__init__.py                              128      42  67.19%   66, 79-83, 94-106, 134-136, 143-148, 189, 208, 230, 240-256, 261, 263, 285, 290, 298, 308
types/bbox.py                                   43      19  55.81%   46-61, 72-74, 85-87, 99-101, 113-115, 123
types/download_args.py                           9       0  100.00%
types/queryables.py                            108       1  99.07%   63
types/search_args.py                            70      18  74.29%   60-64, 71-88, 103
types/whoosh.py                                 82      16  80.49%   131-134, 138-145, 157-163, 176-178
utils/__init__.py                              520      47  90.96%   85, 201-202, 211-238, 241, 256, 336-340, 413-417, 436-438, 451-465, 544, 559, 599-600, 964-967, 975-976, 1014-1015, 1062-1063, 1197
utils/exceptions.py                             46       0  100.00%
utils/import_system.py                          28      19  32.14%   64-78, 89-99
utils/logging.py                                28       1  96.43%   41
utils/notebook.py                               44      23  47.73%   25-29, 36-41, 58-62, 72-78, 83-87
utils/repr.py                                   38       8  78.95%   51, 53, 57, 98, 122-129
utils/requests.py                               55      29  47.27%   51-52, 64, 85-96, 107-124, 128
utils/rest.py                                   36       1  97.22%   55
utils/stac_reader.py                           111      45  59.46%   56-57, 63-85, 95-97, 101, 137, 153-156, 203-212, 222-252
TOTAL                                         9967    1642  83.53%
```

</details>


### Diff against develop

```
Filename                                 Stmts    Miss  Cover
-------------------------------------  -------  ------  -------
plugins/search/build_search_result.py       -2      -1  +0.21%
TOTAL                                       -2      -1  +0.01%
```


Results for commit: 8a8bb1853e76d061e89b99777e752bc0c7c92aef


_Minimum allowed coverage is `70%`_


:recycle: This comment has been updated with latest results


<!-- Sticky Pull Request Commentartifacts/unit-test-results-python3.13-ubuntu-latest/coverage.xml -->
github-actions[bot]: ![badge](https://raw.githubusercontent.com/CS-SI/eodag/_xml_coverage_reports_win/data/default-queryables-form-value-deletion/./badge.svg)


## Code Coverage (Windows)

<details>


```
Filename                                     Stmts    Miss  Cover    Missing
-----------------------------------------  -------  ------  -------  ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
__init__.py                                      8       0  100.00%
cli.py                                         323      64  80.19%   62-63, 87, 674-737, 839-890, 894
config.py                                      430      28  93.49%   83-85, 94, 102, 106-108, 179, 190, 695-697, 810-813, 856-857, 866-867, 972, 1003, 1035-1040, 1042
crunch.py                                        5       5  0.00%    20-24
api/__init__.py                                  0       0  100.00%
api/core.py                                    782      74  90.54%   317-318, 368, 658, 702-705, 743, 787, 821, 866-871, 897, 988, 1056, 1194, 1279-1291, 1327, 1329, 1357, 1361-1372, 1385-1391, 1474-1477, 1510-1530, 1582, 1599-1603, 1615-1618, 1954, 1978-1984, 2235, 2239-2243, 2252-2254, 2298-2299, 2328-2329
api/search_result.py                            58       4  93.10%   92, 101, 108, 122
api/product/__init__.py                          6       0  100.00%
api/product/_assets.py                          48       5  89.58%   75, 147, 155, 158-162
api/product/_product.py                        188      20  89.36%   70-72, 238-239, 314, 343, 400, 414-417, 430, 454-457, 500-506
api/product/metadata_mapping.py                695      77  88.92%   131-133, 230, 262-263, 309-310, 320-332, 334, 345, 351-363, 410-411, 448, 469-472, 495, 503-504, 590-591, 615-616, 622-625, 640-641, 790, 836, 989, 998-1002, 1019-1024, 1151, 1165-1185, 1205, 1210, 1339, 1353, 1378, 1424, 1476, 1499-1500, 1503, 1516-1520, 1536, 1544
api/product/drivers/__init__.py                  6       0  100.00%
api/product/drivers/base.py                      6       1  83.33%   38
plugins/__init__.py                              0       0  100.00%
plugins/base.py                                 21       3  85.71%   48, 55, 68
plugins/manager.py                             172      14  91.86%   116-121, 171, 209, 231, 235, 259, 399-402, 414-415
plugins/apis/__init__.py                         0       0  100.00%
plugins/apis/base.py                             4       0  100.00%
plugins/apis/ecmwf.py                           97      10  89.69%   170-172, 219-220, 246-248, 301-302
plugins/apis/usgs.py                           180      26  85.56%   155, 257, 291, 326-328, 333, 359-360, 365, 395-402, 413-418, 440-446, 477
plugins/authentication/__init__.py               6       1  83.33%   31
plugins/authentication/aws_auth.py              19       0  100.00%
plugins/authentication/base.py                  17       2  88.24%   43, 56
plugins/authentication/generic.py               14       2  85.71%   51, 61
plugins/authentication/header.py                19       0  100.00%
plugins/authentication/keycloak.py              46       7  84.78%   151-154, 175-180
plugins/authentication/oauth.py                 13       7  46.15%   42-44, 48-51
plugins/authentication/openid_connect.py       206      27  86.89%   80-81, 93-111, 149, 155-183, 191, 323-326, 352
plugins/authentication/qsauth.py                34       1  97.06%   91
plugins/authentication/sas_auth.py              47       1  97.87%   76
plugins/authentication/token.py                100      16  84.00%   141, 170, 172, 207-220, 276-280
plugins/authentication/token_exchange.py        36      14  61.11%   75, 92-120
plugins/crunch/__init__.py                       0       0  100.00%
plugins/crunch/base.py                          10       1  90.00%   43
plugins/crunch/filter_date.py                   59      14  76.27%   52-57, 69, 78, 87, 90, 102-104, 113-115, 122
plugins/crunch/filter_latest_intersect.py       47      33  29.79%   49-54, 67-112
plugins/crunch/filter_latest_tpl_name.py        31       1  96.77%   83
plugins/crunch/filter_overlap.py                66      18  72.73%   28-30, 66-69, 76-79, 85, 93, 104-120
plugins/crunch/filter_property.py               30       5  83.33%   55-60, 63-64
plugins/download/__init__.py                     0       0  100.00%
plugins/download/aws.py                        491     163  66.80%   282, 295, 362-365, 379-383, 425-427, 431, 463-464, 470-474, 504, 536, 540, 547, 577-585, 589, 621-629, 640-642, 673-747, 765-823, 834-839, 851-864, 889, 904-906, 909, 919-927, 935-948, 958-989, 996-1008, 1046, 1072, 1117-1119, 1339
plugins/download/base.py                       253      44  82.61%   136, 164, 231-233, 244-247, 300-301, 349-353, 359-360, 402, 405-419, 431, 435, 499-503, 533-534, 559-567, 569-573, 616, 638, 660, 668
plugins/download/creodias_s3.py                 25       9  64.00%   55-69
plugins/download/http.py                       526     103  80.42%   235, 331-334, 337, 344-349, 367-382, 399, 411, 459, 466-472, 490, 504, 518, 526-528, 544-549, 560, 578, 614-617, 646, 650, 744-811, 829, 859-868, 904, 929-930, 949-954, 960, 963, 980, 983, 998-999, 1026-1027, 1034, 1096, 1111, 1170-1171, 1177, 1187, 1223, 1259, 1279, 1314-1316
plugins/download/s3rest.py                     116      24  79.31%   118, 152, 159, 194, 221-228, 231-233, 237, 248-254, 262-263, 266-270, 293, 314-317
plugins/search/__init__.py                      22       0  100.00%
plugins/search/base.py                         146      11  92.47%   100, 104, 128-134, 274, 294, 433
plugins/search/build_search_result.py          349      49  85.96%   248, 276-277, 313, 316, 390-393, 482-499, 527, 562, 564, 589-596, 628-630, 634, 658, 693, 744, 758, 773-774, 789-804, 847, 872, 875, 883, 995-996, 1002, 1013, 1071, 1122
plugins/search/cop_marine.py                   244      47  80.74%   56, 64-66, 76-77, 82, 87-88, 104, 106, 109, 175-176, 228, 234, 238, 242, 255, 266-267, 275, 303, 307, 322, 326, 330, 334, 338-342, 348-351, 354-368, 385, 434-438, 443, 455
plugins/search/creodias_s3.py                   57       3  94.74%   59, 77, 116
plugins/search/csw.py                          105      81  22.86%   98-99, 103-104, 112-160, 166-179, 187-219, 237-278
plugins/search/data_request_search.py          202      69  65.84%   188-191, 207, 218, 222-223, 234, 239, 244, 251, 264-267, 321-322, 326, 336-342, 347, 373-376, 384-395, 412, 414, 421-424, 426-427, 445-449, 482, 492, 503, 516, 522-537, 542
plugins/search/qssearch.py                     733     125  82.95%   453, 511, 525, 529-535, 563-567, 680-692, 737-740, 811-812, 860, 879, 886, 898, 955, 976, 979-980, 989-990, 999-1000, 1009-1010, 1037, 1112-1117, 1121-1130, 1164, 1186, 1263, 1353, 1438-1439, 1449, 1526-1530, 1592, 1595, 1601-1602, 1623, 1651-1663, 1670, 1702-1704, 1714-1720, 1750, 1773, 1778-1779, 1794, 1810, 1878-1988
plugins/search/static_stac_search.py            75      13  82.67%   98-124, 140, 153
rest/__init__.py                                 4       2  50.00%   21-22
rest/cache.py                                   33      22  33.33%   35-37, 44-70
rest/config.py                                  25       1  96.00%   35
rest/constants.py                                6       0  100.00%
rest/core.py                                   260     153  41.15%   157, 159, 161, 164-165, 179-189, 198-199, 205, 208, 249-303, 316-352, 383-420, 435-451, 467-476, 483-522, 539, 581-666, 705, 712-765
rest/errors.py                                  69      49  28.99%   60, 65-100, 105-108, 115-118, 126-147, 155-160, 175-181
rest/server.py                                 192     192  0.00%    18-600
rest/stac.py                                   319      68  78.68%   242, 308, 330, 382-385, 412-439, 470-472, 495, 527-528, 610-650, 672-688, 715, 780-784, 791, 845-846, 852, 907, 945, 978, 997-999
rest/types/__init__.py                           0       0  100.00%
rest/types/collections_search.py                13      13  0.00%    18-44
rest/types/eodag_search.py                     180      20  88.89%   225-229, 262-264, 282, 285, 291, 295, 353, 371-386
rest/types/queryables.py                        57      13  77.19%   50-51, 58-59, 66-67, 93-98, 107-108, 162
rest/types/stac_search.py                      125      11  91.20%   136-138, 184, 199-201, 209, 213, 261, 264
rest/utils/__init__.py                          94      30  68.09%   82-88, 108, 111-112, 131-133, 146, 153, 178-186, 193-214
rest/utils/cql_evaluate.py                      48       5  89.58%   69, 76, 90, 97, 105
rest/utils/rfc3339.py                           23       5  78.26%   43-44, 48, 60, 62
types/__init__.py                              128      44  65.62%   66, 70, 79-83, 94-106, 134-136, 143-148, 189, 203, 208, 230, 240-256, 261, 263, 285, 290, 298, 308
types/bbox.py                                   43      19  55.81%   46-61, 72-74, 85-87, 99-101, 113-115, 123
types/download_args.py                           9       0  100.00%
types/queryables.py                            108       1  99.07%   63
types/search_args.py                            70      18  74.29%   60-64, 71-88, 103
types/whoosh.py                                 82      16  80.49%   131-134, 138-145, 157-163, 176-178
utils/__init__.py                              520      47  90.96%   85, 201-202, 211-238, 241, 256, 336-340, 413-417, 436-438, 451-465, 544, 559, 599-600, 964-967, 975-976, 1014-1015, 1062-1063, 1197
utils/exceptions.py                             46       0  100.00%
utils/import_system.py                          28      19  32.14%   64-78, 89-99
utils/logging.py                                28       1  96.43%   41
utils/notebook.py                               44      23  47.73%   25-29, 36-41, 58-62, 72-78, 83-87
utils/repr.py                                   38       8  78.95%   51, 53, 57, 98, 122-129
utils/requests.py                               55      29  47.27%   51-52, 64, 85-96, 107-124, 128
utils/rest.py                                   36       1  97.22%   55
utils/stac_reader.py                           111      45  59.46%   56-57, 63-85, 95-97, 101, 137, 153-156, 203-212, 222-252
TOTAL                                         9967    2077  79.16%
```

</details>


### Diff against develop

```
Filename                                 Stmts    Miss  Cover
-------------------------------------  -------  ------  --------
plugins/search/build_search_result.py       -2      -1  +0.21%
TOTAL                                       -2      -1  +100.00%
```


Results for commit: 8a8bb1853e76d061e89b99777e752bc0c7c92aef


_Minimum allowed coverage is `70%`_


:recycle: This comment has been updated with latest results


<!-- Sticky Pull Request Commentartifacts/unit-test-results-python3.13-windows-latest/coverage.xml -->

Patch:
diff --git a/eodag/plugins/search/build_search_result.py b/eodag/plugins/search/build_search_result.py
index ec368849..16444bd2 100644
--- a/eodag/plugins/search/build_search_result.py
+++ b/eodag/plugins/search/build_search_result.py
@@ -341,9 +341,6 @@ class ECMWFSearch(PostJsonSearch):
     """
 
     def __init__(self, provider: str, config: PluginConfig) -> None:
-        # cache fetching method
-        self.fetch_data = functools.lru_cache()(self._fetch_data)
-
         config.metadata_mapping = {
             **keywords_to_mdt(ECMWF_KEYWORDS + COP_DS_KEYWORDS, "ecmwf"),
             **config.metadata_mapping,
@@ -576,13 +573,13 @@ class ECMWFSearch(PostJsonSearch):
             getattr(self.config, "discover_queryables", {}).get("constraints_url", ""),
             **kwargs,
         )
-        constraints: list[dict[str, Any]] = self.fetch_data(constraints_url)
+        constraints: list[dict[str, Any]] = self._fetch_data(constraints_url)
 
         form_url = format_metadata(
             getattr(self.config, "discover_queryables", {}).get("form_url", ""),
             **kwargs,
         )
-        form = self.fetch_data(form_url)
+        form: list[dict[str, Any]] = self._fetch_data(form_url)
 
         formated_kwargs = self.format_as_provider_keyword(
             product_type, processed_kwargs
@@ -639,7 +636,7 @@ class ECMWFSearch(PostJsonSearch):
                 return self.queryables_from_metadata_mapping(product_type)
             if "{" in values_url:
                 values_url = values_url.format(productType=provider_product_type)
-            data = self.fetch_data(values_url)
+            data = self._fetch_data(values_url)
             available_values = data["constraints"]
             required_keywords = data.get("required", [])
 
@@ -656,7 +653,9 @@ class ECMWFSearch(PostJsonSearch):
                     "completionTimeFromAscendingNode",
                     "geom",
                 }
-                and keyword.replace("ecmwf:", "") not in available_values
+                and keyword not in [f["name"] for f in form]
+                and keyword.replace("ecmwf:", "")
+                not in set(list(available_values.keys()) + [f["name"] for f in form])
             ):
                 raise ValidationError(f"{keyword} is not a queryable parameter")
 
@@ -874,9 +873,6 @@ class ECMWFSearch(PostJsonSearch):
                 if fields and (comment := fields[0].get("comment")):
                     prop["description"] = comment
 
-                if d := details.get("default"):
-                    default = default or (d[0] if fields else d)
-
             if name == "area" and isinstance(default, dict):
                 default = list(default.values())
 
@@ -979,7 +975,7 @@ class ECMWFSearch(PostJsonSearch):
             else None
         )
         timeout = getattr(self.config, "timeout", DEFAULT_SEARCH_TIMEOUT)
-        return fetch_json(url, auth=auth, timeout=timeout)
+        return functools.lru_cache()(fetch_json)(url, auth=auth, timeout=timeout)
 
     def normalize_results(
         self, results: RawSearchResult, **kwargs: Any


Test patch:
diff --git a/tests/resources/form.json b/tests/resources/form.json
index b18d35fe..738a3554 100644
--- a/tests/resources/form.json
+++ b/tests/resources/form.json
@@ -73,8 +73,20 @@
     "name": "data_format",
     "type": "StringChoiceWidget",
     "details": {
-      "values": ["grib", "netcdf"]
+      "values": ["grib", "netcdf"],
+      "default": ["grib"]
     },
-    "id": 8
+    "id": 8,
+    "required": true
+  },
+  {
+    "name": "download_format",
+    "type": "StringChoiceWidget",
+    "details": {
+      "values": ["unarchived", "zip"],
+      "default": ["unarchived"]
+    },
+    "id": 9,
+    "required": true
   }
 ]
diff --git a/tests/units/test_search_plugins.py b/tests/units/test_search_plugins.py
index b447acb0..e05e2f05 100644
--- a/tests/units/test_search_plugins.py
+++ b/tests/units/test_search_plugins.py
@@ -40,7 +40,7 @@ from typing_extensions import get_args
 from eodag.api.product import AssetsDict
 from eodag.api.product.metadata_mapping import get_queryable_from_provider
 from eodag.utils import deepcopy
-from eodag.utils.exceptions import UnsupportedProductType
+from eodag.utils.exceptions import UnsupportedProductType, ValidationError
 from tests.context import (
     DEFAULT_MISSION_START_DATE,
     DEFAULT_SEARCH_TIMEOUT,
@@ -2343,8 +2343,11 @@ class TestSearchPluginECMWFSearch(unittest.TestCase):
             except Exception:
                 assert eoproduct.properties[param] == self.custom_query_params[param]
 
-    @mock.patch("eodag.utils.requests.requests.sessions.Session.get", autospec=True)
-    def test_plugins_search_ecmwfsearch_discover_queryables(self, mock_requests_get):
+    @mock.patch(
+        "eodag.plugins.search.build_search_result.ECMWFSearch._fetch_data",
+        autospec=True,
+    )
+    def test_plugins_search_ecmwfsearch_discover_queryables_ok(self, mock__fetch_data):
         constraints_path = os.path.join(TEST_RESOURCES_PATH, "constraints.json")
         with open(constraints_path) as f:
             constraints = json.load(f)
@@ -2353,7 +2356,7 @@ class TestSearchPluginECMWFSearch(unittest.TestCase):
         form_path = os.path.join(TEST_RESOURCES_PATH, "form.json")
         with open(form_path) as f:
             form = json.load(f)
-        mock_requests_get.return_value.json.side_effect = [constraints, form]
+        mock__fetch_data.side_effect = [constraints, form]
         product_type_config = {"missionStartDate": "2001-01-01T00:00:00Z"}
         setattr(self.search_plugin.config, "product_type_config", product_type_config)
 
@@ -2375,32 +2378,49 @@ class TestSearchPluginECMWFSearch(unittest.TestCase):
         default_values.pop("metadata_mapping", None)
         params = deepcopy(default_values)
         params["productType"] = "CAMS_EU_AIR_QUALITY_RE"
+        # set a parameter among the required ones of the form file with a default value in this form but not among the
+        # ones of the constraints file to an empty value to check if its associated queryable has no default value
+        eodag_formatted_data_format = "ecmwf:data_format"
+        provider_data_format = eodag_formatted_data_format.replace("ecmwf:", "")
+        self.assertIn(eodag_formatted_data_format, default_values)
+        self.assertIn(provider_data_format, [param["name"] for param in form])
+        data_format_in_form = [
+            param for param in form if param["name"] == provider_data_format
+        ][0]
+        self.assertTrue(data_format_in_form.get("required", False))
+        self.assertIsNotNone(
+            data_format_in_form.get("details", {}).get("default", None)
+        )
+        for constraint in constraints:
+            self.assertNotIn(provider_data_format, constraint)
+        params[eodag_formatted_data_format] = ""
+
+        # use a parameter among the ones of the form file but not among the ones of the constraints file
+        # and of provider default configuration to check if an error is raised, which is supposed to not happen
+        eodag_formatted_download_format = "ecmwf:download_format"
+        provider_download_format = eodag_formatted_download_format.replace("ecmwf:", "")
+        self.assertNotIn(eodag_formatted_download_format, default_values)
+        self.assertIn(provider_download_format, [param["name"] for param in form])
+        for constraint in constraints:
+            self.assertNotIn(provider_data_format, constraint)
+        params[eodag_formatted_download_format] = "foo"
 
         queryables = self.search_plugin.discover_queryables(**params)
+        # no error was raised, as expected
         self.assertIsNotNone(queryables)
 
-        mock_requests_get.assert_has_calls(
+        mock__fetch_data.assert_has_calls(
             [
                 call(
                     mock.ANY,
                     "https://ads.atmosphere.copernicus.eu/api/catalogue/v1/collections/"
                     "cams-europe-air-quality-reanalyses/constraints.json",
-                    headers=USER_AGENT,
-                    auth=None,
-                    timeout=DEFAULT_SEARCH_TIMEOUT,
                 ),
-                call().raise_for_status(),
-                call().json(),
                 call(
                     mock.ANY,
                     "https://ads.atmosphere.copernicus.eu/api/catalogue/v1/collections/"
                     "cams-europe-air-quality-reanalyses/form.json",
-                    headers=USER_AGENT,
-                    auth=None,
-                    timeout=DEFAULT_SEARCH_TIMEOUT,
                 ),
-                call().raise_for_status(),
-                call().json(),
             ]
         )
 
@@ -2420,7 +2440,15 @@ class TestSearchPluginECMWFSearch(unittest.TestCase):
             "CAMS_EU_AIR_QUALITY_RE"
         ].items():
             queryable = queryables.get(property)
-            if queryable is not None:
+            # a special case for eodag_formatted_data_format queryable is required
+            # as its default value has been overwritten by an empty value
+            if queryable is not None and property == eodag_formatted_data_format:
+                self.assertEqual(
+                    PydanticUndefined, queryable.__metadata__[0].get_default()
+                )
+                # queryables with empty default values are required
+                self.assertTrue(queryable.__metadata__[0].is_required())
+            elif queryable is not None:
                 self.assertEqual(default_value, queryable.__metadata__[0].get_default())
                 # queryables with default values are not required
                 self.assertFalse(queryable.__metadata__[0].is_required())
@@ -2442,7 +2470,8 @@ class TestSearchPluginECMWFSearch(unittest.TestCase):
             )
 
         # reset mock
-        mock_requests_get.reset_mock()
+        mock__fetch_data.reset_mock()
+        mock__fetch_data.side_effect = [constraints, form]
         # with additional param
         params = deepcopy(default_values)
         params["productType"] = "CAMS_EU_AIR_QUALITY_RE"
@@ -2450,16 +2479,68 @@ class TestSearchPluginECMWFSearch(unittest.TestCase):
         queryables = self.search_plugin.discover_queryables(**params)
         self.assertIsNotNone(queryables)
 
-        # mock not called because cached values are used
-        mock_requests_get.assert_not_called()
+        # cached values are not used to make the set of unit tests work then the mock is called again
+        mock__fetch_data.assert_has_calls(
+            [
+                call(
+                    mock.ANY,
+                    "https://ads.atmosphere.copernicus.eu/api/catalogue/v1/collections/"
+                    "cams-europe-air-quality-reanalyses/constraints.json",
+                ),
+                call(
+                    mock.ANY,
+                    "https://ads.atmosphere.copernicus.eu/api/catalogue/v1/collections/"
+                    "cams-europe-air-quality-reanalyses/form.json",
+                ),
+            ]
+        )
 
-        self.assertEqual(11, len(queryables))
+        self.assertEqual(12, len(queryables))
         # default properties called in function arguments are added and must be default values of the queryables
         queryable = queryables.get("ecmwf:variable")
         if queryable is not None:
             self.assertEqual("a", queryable.__metadata__[0].get_default())
             self.assertFalse(queryable.__metadata__[0].is_required())
 
+    @mock.patch(
+        "eodag.plugins.search.build_search_result.ECMWFSearch._fetch_data",
+        autospec=True,
+    )
+    def test_plugins_search_ecmwfsearch_discover_queryables_ko(self, mock__fetch_data):
+        constraints_path = os.path.join(TEST_RESOURCES_PATH, "constraints.json")
+        with open(constraints_path) as f:
+            constraints = json.load(f)
+        form_path = os.path.join(TEST_RESOURCES_PATH, "form.json")
+        with open(form_path) as f:
+            form = json.load(f)
+        mock__fetch_data.side_effect = [constraints, form]
+
+        default_values = deepcopy(
+            getattr(self.search_plugin.config, "products", {}).get(
+                "CAMS_EU_AIR_QUALITY_RE", {}
+            )
+        )
+        default_values.pop("metadata_mapping", None)
+        params = deepcopy(default_values)
+        params["productType"] = "CAMS_EU_AIR_QUALITY_RE"
+
+        # use a wrong parameter, e.g. it is not among the ones of the form file, not among
+        # the ones of the constraints file and not among the ones of default provider configuration
+        wrong_queryable = "foo"
+        self.assertNotIn(wrong_queryable, default_values)
+        self.assertNotIn(wrong_queryable, [param["name"] for param in form])
+        for constraint in constraints:
+            self.assertNotIn(wrong_queryable, constraint)
+        params[wrong_queryable] = "bar"
+
+        # Test the function, expecting ValidationError to be raised
+        with self.assertRaises(ValidationError) as context:
+            self.search_plugin.discover_queryables(**params)
+        self.assertEqual(
+            f"{wrong_queryable} is not a queryable parameter for {self.provider}",
+            str(context.exception),
+        )
+
     @mock.patch("eodag.utils.requests.requests.sessions.Session.get", autospec=True)
     def test_plugins_search_ecmwf_search_wekeo_discover_queryables(
         self, mock_requests_get