Repository: CS-SI/eodag
Pull request #1071 (2024-03-22 14:44:48)

feat: provider groups
in EODAG library:
- add a `by_group` option on `core.available_providers`. If set, we return the unique list of groups instead of the list of provider names.
- update `list_product_types` to accept group name as provider names. Calling `list_product_types` with a group name set in the provider name will return the union of product types from all the providers of this group.
- update `get_search_plugin` to accept group names as provider names. Calling `get_search_plugin` with a group name as provider name will yield all the plugins belonging to the group.

in EODAG server:
- use group when set instead of provider name

> `group` is an optional field

Example:

I have 2 provider configuration in EODAG:

- earth_search
- earth_search_cog

I configure both with `group` **earth_search**. 

- [x] When I `list  the product types` for provider **earth_search**, EODAG return the merged list of product types from earth_search and earth_search_cog. 
- [x] In server mode, the returned product types display **earth_search** as provider name.

- [x] When I `search` for products with provider **earth_search**, EDOAG search on both provider using the existing fallback mechanism.

- [x] When I ask for `queryables `with provider **earth_search** the response is the intersect of queryables from earth_search and earth_search_cog. 
- [x] `available_providers` returns only one entry for both: **earth_search**.

github-actions[bot]: ![badge](https://raw.githubusercontent.com/CS-SI/eodag/_xml_coverage_reports/data/feat%2Fproviders-group-2/./badge.svg)


## Code Coverage (Ubuntu)

<details>


```
Filename                                     Stmts    Miss  Cover    Missing
-----------------------------------------  -------  ------  -------  ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
__init__.py                                     11       0  100.00%
cli.py                                         300      47  84.33%   61, 645-684, 786-837, 841
config.py                                      317      27  91.48%   80-82, 91, 99, 103-105, 182, 194, 390-392, 456-459, 506-507, 516-517, 596, 665-670, 672
crunch.py                                        6       6  0.00%    18-24
api/__init__.py                                  1       0  100.00%
api/core.py                                    725      76  89.52%   86-95, 368, 578, 622-625, 663, 768, 772-777, 803, 898, 968, 1085-1090, 1176-1188, 1228, 1230, 1234, 1255-1257, 1261-1272, 1285-1291, 1381-1384, 1413-1433, 1481, 1487-1490, 1499, 1850, 1883-1889, 2154, 2158-2161, 2175-2177, 2212
api/search_result.py                            44       6  86.36%   33-35, 70, 79, 86, 100
api/product/__init__.py                          5       0  100.00%
api/product/_assets.py                          44       5  88.64%   27-29, 79, 129
api/product/_product.py                        208      32  84.62%   48-55, 59-61, 165-172, 256-257, 347, 383, 444, 458-461, 474, 498-501, 544-550
api/product/metadata_mapping.py                648      87  86.57%   66-68, 129-131, 232, 264-265, 317-329, 331, 342, 346-362, 401-404, 441, 462-465, 479, 487-488, 561-562, 586-587, 593-596, 611-612, 776, 822, 879-880, 923-928, 1059, 1073-1093, 1113, 1118, 1228, 1250, 1264, 1277-1296, 1335, 1387, 1425-1429, 1448
api/product/drivers/__init__.py                  6       0  100.00%
api/product/drivers/base.py                      8       2  75.00%   23, 41
plugins/__init__.py                              1       0  100.00%
plugins/base.py                                 23       3  86.96%   25, 48, 55
plugins/manager.py                             126      14  88.89%   49-51, 95-100, 146, 187, 209, 234, 273-274
plugins/apis/__init__.py                         1       0  100.00%
plugins/apis/base.py                             4       0  100.00%
plugins/apis/cds.py                            206      49  76.21%   72-80, 126-129, 176-188, 244, 309, 317-322, 335-342, 360-362, 383-385, 395-397, 411-412, 457, 460, 470, 487, 515, 517
plugins/apis/ecmwf.py                           99      15  84.85%   47-55, 156-158, 205-206, 232-234
plugins/apis/usgs.py                           168      36  78.57%   58-63, 124, 197, 231, 266-268, 273, 299-300, 305, 335-342, 351-356, 378-384, 386-392, 415
plugins/authentication/__init__.py               7       1  85.71%   31
plugins/authentication/aws_auth.py              20       2  90.00%   25-27
plugins/authentication/base.py                  19       2  89.47%   26, 47
plugins/authentication/generic.py               16       3  81.25%   28, 40, 50
plugins/authentication/header.py                17       1  94.12%   27
plugins/authentication/keycloak.py              88      17  80.68%   32-34, 159-160, 190-212, 238-243
plugins/authentication/oauth.py                 15       8  46.67%   25, 32-34, 38-41
plugins/authentication/openid_connect.py       103      64  37.86%   39-41, 138-150, 154-172, 180-222, 228-237, 246-286, 291-299, 304-305
plugins/authentication/qsauth.py                36       2  94.44%   32, 83
plugins/authentication/sas_auth.py              49       2  95.92%   32, 76
plugins/authentication/token.py                 75       8  89.33%   35-37, 102, 127, 154-157
plugins/crunch/__init__.py                       1       0  100.00%
plugins/crunch/base.py                          10       2  80.00%   25, 38
plugins/crunch/filter_date.py                   62      15  75.81%   30, 53-58, 72, 81, 90, 93, 105-107, 116-118, 125
plugins/crunch/filter_latest_intersect.py       50      10  80.00%   32-34, 51-52, 71, 80-83, 85, 92-95
plugins/crunch/filter_latest_tpl_name.py        33       2  93.94%   28, 86
plugins/crunch/filter_overlap.py                68      17  75.00%   28-30, 33, 82-85, 91, 99, 110-126
plugins/crunch/filter_property.py               33       8  75.76%   29, 60-65, 68-69, 85-89
plugins/download/__init__.py                     1       0  100.00%
plugins/download/aws.py                        491     165  66.40%   77-83, 272, 285, 352-355, 369-373, 419-421, 425, 458-459, 465-469, 502, 537, 541, 548, 578-586, 590, 628-636, 643-645, 686-760, 778-839, 850-855, 871-884, 913, 928-930, 933, 943-951, 959-972, 982-1001, 1008-1020, 1061, 1087, 1132-1134, 1354
plugins/download/base.py                       261      57  78.16%   58-64, 145, 180, 319-320, 340-346, 377-381, 387-388, 432, 435-449, 461, 465, 538-542, 572-573, 581-598, 605-613, 615-619, 666, 689, 711, 719
plugins/download/creodias_s3.py                 17       9  47.06%   44-58
plugins/download/http.py                       422      97  77.01%   81-88, 122, 171, 192-199, 209-222, 262-267, 318, 351, 378-380, 390-398, 420, 459-463, 520, 593-649, 663, 687-689, 719-720, 728, 731, 747, 771, 794-795, 802, 863-869, 924-925, 931, 941, 1007, 1025-1041
plugins/download/s3rest.py                     117      27  76.92%   55-58, 124, 165, 199, 229-236, 239-241, 245, 258-264, 272-273, 276-280, 303, 324-327
plugins/search/__init__.py                       1       0  100.00%
plugins/search/base.py                         127       9  92.91%   49-54, 108, 112, 275, 295, 378
plugins/search/build_search_result.py           56       6  89.29%   67, 112-113, 121, 132, 176
plugins/search/creodias_s3.py                   55       3  94.55%   56, 74, 108
plugins/search/csw.py                          107      83  22.43%   43-45, 57-58, 62-63, 74-122, 128-141, 149-181, 199-240
plugins/search/data_request_search.py          198      60  69.70%   52, 89-92, 108, 120, 139, 144, 149, 156, 169-172, 226-227, 231, 241-247, 252, 281, 291-302, 319, 321, 328-329, 331-332, 350-354, 387, 394, 405, 418, 424-436, 441
plugins/search/qssearch.py                     521      53  89.83%   87, 354-355, 449-455, 482-485, 558-559, 600, 618, 633, 686, 707, 710-711, 720, 731, 740, 763, 823-828, 832-833, 861, 932, 951-968, 1007, 1141-1142, 1163, 1205-1206, 1216-1222, 1263, 1277, 1297, 1385
plugins/search/static_stac_search.py            47       3  93.62%   39-40, 82
rest/__init__.py                                 1       0  100.00%
rest/core.py                                   169      10  94.08%   243, 514, 516, 519-521, 593, 600-604
rest/server.py                                 298      54  81.88%   80-81, 105, 128-129, 240-242, 258, 298-299, 311-327, 409-414, 444, 601-608, 639, 682-683, 771-773, 790-795, 825, 827, 831-832, 836-837
rest/stac.py                                   427      93  78.22%   58-60, 242-244, 276, 289-298, 317-323, 368, 405-407, 430, 465-466, 552-591, 636, 656-657, 829, 894-896, 1115, 1125-1137, 1150-1172, 1186-1231, 1390-1391
rest/types/__init__.py                           1       0  100.00%
rest/types/eodag_search.py                     185       9  95.14%   52-55, 231-235, 288, 291, 359
rest/types/stac_queryables.py                   30       2  93.33%   28, 114
rest/types/stac_search.py                      122      10  91.80%   48-51, 167, 182-184, 192, 196
rest/utils/__init__.py                         116      14  87.93%   53, 108-109, 128-130, 180, 190-204, 236
rest/utils/cql_evaluate.py                      48       5  89.58%   69, 76, 90, 97, 105
rest/utils/rfc3339.py                           31       3  90.32%   78, 90, 92
types/__init__.py                               76       6  92.11%   53, 87, 152, 172, 177, 185
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                             81       0  100.00%
types/search_args.py                            70      18  74.29%   60-64, 71-88, 104
utils/__init__.py                              495      43  91.31%   83, 88, 109-111, 189-190, 199-226, 229, 243, 325-329, 405-409, 430-432, 514, 519, 529, 567-568, 964-967, 975-976, 1017-1018, 1098, 1182, 1200, 1373
utils/constraints.py                           123      42  65.85%   31, 84-93, 134, 139, 143, 154, 177-178, 189-197, 206, 220-236, 245-256
utils/exceptions.py                             37       2  94.59%   23, 93
utils/import_system.py                          30      20  33.33%   27, 67-81, 93-103
utils/logging.py                                29       1  96.55%   123
utils/notebook.py                               44      23  47.73%   25-29, 36-41, 58-62, 72-78, 83-87
utils/stac_reader.py                            91      28  69.23%   55-56, 63-86, 93-95, 99, 141, 155-158
TOTAL                                         8630    1543  82.12%
```

</details>


### Diff against develop

```
Filename              Stmts    Miss  Cover
------------------  -------  ------  -------
config.py                +4       0  +0.11%
api/core.py              +4       0  +0.06%
plugins/manager.py       -1       0  -0.09%
rest/stac.py             -9      -2  +0.01%
TOTAL                    -2      -2  +0.02%
```


Results for commit: e2e4b7d5e88757ec33f51547641ec00f3dfb484d


_Minimum allowed coverage is `70%`_


:recycle: This comment has been updated with latest results


<!-- Sticky Pull Request Commentartifacts/unit-test-results-python3.12-ubuntu-latest/coverage.xml -->
github-actions[bot]: ![badge](https://raw.githubusercontent.com/CS-SI/eodag/_xml_coverage_reports_win/data/feat%2Fproviders-group-2/./badge.svg)


## Code Coverage (Windows)

<details>


```
Filename                                     Stmts    Miss  Cover    Missing
-----------------------------------------  -------  ------  -------  ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
__init__.py                                     11       0  100.00%
cli.py                                         300      47  84.33%   61, 645-684, 786-837, 841
config.py                                      317      28  91.17%   80-82, 91, 99, 103-105, 182, 194, 390-392, 456-459, 506-507, 516-517, 596, 630, 665-670, 672
crunch.py                                        6       6  0.00%    18-24
api/__init__.py                                  1       0  100.00%
api/core.py                                    725      76  89.52%   86-95, 368, 578, 622-625, 663, 768, 772-777, 803, 898, 968, 1085-1090, 1176-1188, 1228, 1230, 1234, 1255-1257, 1261-1272, 1285-1291, 1381-1384, 1413-1433, 1481, 1487-1490, 1499, 1850, 1883-1889, 2154, 2158-2161, 2175-2177, 2212
api/search_result.py                            44       6  86.36%   33-35, 70, 79, 86, 100
api/product/__init__.py                          5       0  100.00%
api/product/_assets.py                          44       5  88.64%   27-29, 79, 129
api/product/_product.py                        208      32  84.62%   48-55, 59-61, 165-172, 256-257, 347, 383, 444, 458-461, 474, 498-501, 544-550
api/product/metadata_mapping.py                648      88  86.42%   66-68, 129-131, 232, 264-265, 317-329, 331, 342, 346-362, 401-404, 441, 462-465, 479, 487-488, 561-562, 586-587, 593-596, 611-612, 776, 822, 879-880, 923-928, 1059, 1073-1093, 1113, 1118, 1228, 1250, 1264, 1277-1296, 1335, 1387, 1410, 1425-1429, 1448
api/product/drivers/__init__.py                  6       0  100.00%
api/product/drivers/base.py                      8       2  75.00%   23, 41
plugins/__init__.py                              1       0  100.00%
plugins/base.py                                 23       4  82.61%   25, 48, 55, 68
plugins/manager.py                             126      14  88.89%   49-51, 95-100, 146, 187, 209, 234, 273-274
plugins/apis/__init__.py                         1       0  100.00%
plugins/apis/base.py                             4       0  100.00%
plugins/apis/cds.py                            206      56  72.82%   72-80, 126-129, 176-188, 244, 309, 317-322, 335-342, 360-362, 383-385, 395-397, 411-412, 457, 460, 470, 487, 507-522
plugins/apis/ecmwf.py                           99      15  84.85%   47-55, 156-158, 205-206, 232-234
plugins/apis/usgs.py                           168      36  78.57%   58-63, 124, 197, 231, 266-268, 273, 299-300, 305, 335-342, 351-356, 378-384, 386-392, 415
plugins/authentication/__init__.py               7       1  85.71%   31
plugins/authentication/aws_auth.py              20       2  90.00%   25-27
plugins/authentication/base.py                  19       3  84.21%   26, 34, 47
plugins/authentication/generic.py               16       3  81.25%   28, 40, 50
plugins/authentication/header.py                17       1  94.12%   27
plugins/authentication/keycloak.py              88      17  80.68%   32-34, 159-160, 190-212, 238-243
plugins/authentication/oauth.py                 15       8  46.67%   25, 32-34, 38-41
plugins/authentication/openid_connect.py       103      64  37.86%   39-41, 138-150, 154-172, 180-222, 228-237, 246-286, 291-299, 304-305
plugins/authentication/qsauth.py                36       2  94.44%   32, 83
plugins/authentication/sas_auth.py              49       2  95.92%   32, 76
plugins/authentication/token.py                 75       8  89.33%   35-37, 102, 127, 154-157
plugins/crunch/__init__.py                       1       0  100.00%
plugins/crunch/base.py                          10       2  80.00%   25, 38
plugins/crunch/filter_date.py                   62      15  75.81%   30, 53-58, 72, 81, 90, 93, 105-107, 116-118, 125
plugins/crunch/filter_latest_intersect.py       50      35  30.00%   32-34, 48-53, 69-114
plugins/crunch/filter_latest_tpl_name.py        33       2  93.94%   28, 86
plugins/crunch/filter_overlap.py                68      17  75.00%   28-30, 33, 82-85, 91, 99, 110-126
plugins/crunch/filter_property.py               33       8  75.76%   29, 60-65, 68-69, 85-89
plugins/download/__init__.py                     1       0  100.00%
plugins/download/aws.py                        491     165  66.40%   77-83, 272, 285, 352-355, 369-373, 419-421, 425, 458-459, 465-469, 502, 537, 541, 548, 578-586, 590, 628-636, 643-645, 686-760, 778-839, 850-855, 871-884, 913, 928-930, 933, 943-951, 959-972, 982-1001, 1008-1020, 1061, 1087, 1132-1134, 1354
plugins/download/base.py                       261      59  77.39%   58-64, 145, 180, 250-252, 319-320, 340-346, 377-381, 387-388, 432, 435-449, 461, 465, 538-542, 572-573, 581-598, 605-613, 615-619, 666, 689, 711, 719
plugins/download/creodias_s3.py                 17       9  47.06%   44-58
plugins/download/http.py                       422      97  77.01%   81-88, 122, 171, 192-199, 209-222, 262-267, 318, 351, 378-380, 390-398, 420, 459-463, 520, 593-649, 663, 687-689, 719-720, 728, 731, 747, 771, 794-795, 802, 863-869, 924-925, 931, 941, 1007, 1025-1041
plugins/download/s3rest.py                     117      27  76.92%   55-58, 124, 165, 199, 229-236, 239-241, 245, 258-264, 272-273, 276-280, 303, 324-327
plugins/search/__init__.py                       1       0  100.00%
plugins/search/base.py                         127       9  92.91%   49-54, 108, 112, 275, 295, 378
plugins/search/build_search_result.py           56       6  89.29%   67, 112-113, 121, 132, 176
plugins/search/creodias_s3.py                   55       3  94.55%   56, 74, 108
plugins/search/csw.py                          107      83  22.43%   43-45, 57-58, 62-63, 74-122, 128-141, 149-181, 199-240
plugins/search/data_request_search.py          198      60  69.70%   52, 89-92, 108, 120, 139, 144, 149, 156, 169-172, 226-227, 231, 241-247, 252, 281, 291-302, 319, 321, 328-329, 331-332, 350-354, 387, 394, 405, 418, 424-436, 441
plugins/search/qssearch.py                     521      77  85.22%   87, 354-355, 449-455, 482-485, 558-559, 600, 618, 633, 686, 707, 710-711, 720, 731, 740, 763, 823-828, 832-833, 861, 932, 951-968, 1007, 1141-1142, 1163, 1205-1206, 1216-1222, 1263, 1277, 1297, 1345-1414
plugins/search/static_stac_search.py            47       3  93.62%   39-40, 82
rest/__init__.py                                 1       0  100.00%
rest/core.py                                   169      61  63.91%   140, 142, 144, 150-151, 168-176, 183-189, 235-264, 418-445, 463, 513-552, 593, 600-604
rest/server.py                                 298     298  0.00%    18-850
rest/stac.py                                   427     152  64.40%   58-60, 228, 242-244, 276, 289-298, 317-323, 368, 405-407, 430, 465-466, 552-591, 636, 644-645, 649-657, 771, 829, 894-896, 913-915, 923-925, 938-940, 954-971, 981-1002, 1012-1034, 1042-1059, 1082-1105, 1115, 1125-1137, 1150-1172, 1186-1231, 1384-1410
rest/types/__init__.py                           1       0  100.00%
rest/types/eodag_search.py                     185      18  90.27%   52-55, 231-235, 268-270, 288, 291, 297, 301, 359, 371-374
rest/types/stac_queryables.py                   30       6  80.00%   28, 53-58, 114
rest/types/stac_search.py                      122      12  90.16%   48-51, 167, 182-184, 192, 196, 240, 243
rest/utils/__init__.py                         116      31  73.28%   53, 79-85, 105, 108-109, 128-130, 147, 173-181, 188-209, 236
rest/utils/cql_evaluate.py                      48       5  89.58%   69, 76, 90, 97, 105
rest/utils/rfc3339.py                           31       5  83.87%   73-74, 78, 90, 92
types/__init__.py                               76      11  85.53%   53, 87, 125, 152, 162-164, 172, 177, 185, 195
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                             81       0  100.00%
types/search_args.py                            70      18  74.29%   60-64, 71-88, 104
utils/__init__.py                              495      43  91.31%   83, 88, 109-111, 189-190, 199-226, 229, 243, 325-329, 405-409, 430-432, 514, 519, 529, 567-568, 964-967, 975-976, 1017-1018, 1098, 1182, 1200, 1373
utils/constraints.py                           123      42  65.85%   31, 84-93, 134, 139, 143, 154, 177-178, 189-197, 206, 220-236, 245-256
utils/exceptions.py                             37       2  94.59%   23, 93
utils/import_system.py                          30      20  33.33%   27, 67-81, 93-103
utils/logging.py                                29       1  96.55%   123
utils/notebook.py                               44      23  47.73%   25-29, 36-41, 58-62, 72-78, 83-87
utils/stac_reader.py                            91      28  69.23%   55-56, 63-86, 93-95, 99, 141, 155-158
TOTAL                                         8630    1998  76.85%
```

</details>


### Diff against develop

```
Filename              Stmts    Miss  Cover
------------------  -------  ------  -------
config.py                +4       0  +0.12%
api/core.py              +4       0  +0.06%
plugins/manager.py       -1       0  -0.09%
rest/stac.py             -9      -2  -0.28%
TOTAL                    -2      -2  +0.02%
```


Results for commit: e2e4b7d5e88757ec33f51547641ec00f3dfb484d


_Minimum allowed coverage is `70%`_


:recycle: This comment has been updated with latest results


<!-- Sticky Pull Request Commentartifacts/unit-test-results-python3.12-windows-latest/coverage.xml -->
github-actions[bot]: ## Test Results
    4 files  ± 0      4 suites  ±0   6m 4s :stopwatch: +3s
  510 tests + 3    507 :white_check_mark: + 3   3 :zzz: ±0  0 :x: ±0 
2 040 runs  +12  1 954 :white_check_mark: +12  86 :zzz: ±0  0 :x: ±0 

Results for commit 8eb6aa07. ± Comparison against base commit 80b4fd00.

[test-results]:data:application/gzip;base64,H4sIAOOa/WUC/02MzQ6DIBAGX8Vw7mFRfrQv0ywICalKg3AyffdCW9DLJjNfdg5i3WJ2cu/YrSN7crHBnAJG57eMgygiT7GMnEKlx560LgrkqZ7uVX6asOiWLM4nE4IPfxPSVpo9sEq1SSfOmvo1R1HFpfnla1L7dXUxAxmNEoggB80t9nLSoBmIfKmZOYIAQGqVkuT9AdrRlVIIAQAA


Patch:
diff --git a/eodag/api/core.py b/eodag/api/core.py
index 7c35b41a..97c55643 100644
--- a/eodag/api/core.py
+++ b/eodag/api/core.py
@@ -537,36 +537,34 @@ class EODataAccessGateway:
             self.fetch_product_types_list(provider=provider)
 
         product_types: List[Dict[str, Any]] = []
-        if provider is not None:
-            if provider in self.providers_config:
-                provider_supported_products = self.providers_config[provider].products
-                for product_type_id in provider_supported_products:
-                    if product_type_id == GENERIC_PRODUCT_TYPE:
-                        continue
 
-                    config = self.product_types_config[product_type_id]
-                    config["_id"] = product_type_id
-                    if "alias" in config:
-                        product_type_id = config["alias"]
-                    product_type = dict(ID=product_type_id, **config)
-                    if product_type_id not in product_types:
-                        product_types.append(product_type)
-                return sorted(product_types, key=itemgetter("ID"))
+        providers_configs = (
+            list(self.providers_config.values())
+            if not provider
+            else [
+                p
+                for p in self.providers_config.values()
+                if provider in [p.name, getattr(p, "group", None)]
+            ]
+        )
+
+        if provider and not providers_configs:
             raise UnsupportedProvider(
-                f"invalid requested provider: {provider} is not (yet) supported"
-            )
-        # Only get the product types supported by the available providers
-        for provider in self.available_providers():
-            current_product_type_ids = [pt["ID"] for pt in product_types]
-            product_types.extend(
-                [
-                    pt
-                    for pt in self.list_product_types(
-                        provider=provider, fetch_providers=False
-                    )
-                    if pt["ID"] not in current_product_type_ids
-                ]
+                f"The requested provider is not (yet) supported: {provider}"
             )
+
+        for p in providers_configs:
+            for product_type_id in p.products:  # type: ignore
+                if product_type_id == GENERIC_PRODUCT_TYPE:
+                    continue
+                config = self.product_types_config[product_type_id]
+                config["_id"] = product_type_id
+                if "alias" in config:
+                    product_type_id = config["alias"]
+                product_type = {"ID": product_type_id, **config}
+                if product_type not in product_types:
+                    product_types.append(product_type)
+
         # Return the product_types sorted in lexicographic order of their ID
         return sorted(product_types, key=itemgetter("ID"))
 
@@ -856,23 +854,49 @@ class EODataAccessGateway:
         # rebuild index after product types list update
         self.build_index()
 
-    def available_providers(self, product_type: Optional[str] = None) -> List[str]:
-        """Gives the sorted list of the available providers
+    def available_providers(
+        self, product_type: Optional[str] = None, by_group: bool = False
+    ) -> List[str]:
+        """Gives the sorted list of the available providers or groups
+
+        The providers or groups are sorted first by their priority level in descending order,
+        and then alphabetically in ascending order for providers or groups with the same
+        priority level.
 
         :param product_type: (optional) Only list providers configured for this product_type
-        :type product_type: str
-        :returns: the sorted list of the available providers
-        :rtype: list
+        :type product_type: Optional[str]
+        :param by_group: (optional) If set to True, list groups when available instead
+                         of providers, mixed with other providers
+        :type by_group: bool
+        :returns: the sorted list of the available providers or groups
+        :rtype: List[str]
         """
 
         if product_type:
-            return sorted(
-                k
+            providers = [
+                (v.group if by_group and hasattr(v, "group") else k, v.priority)
                 for k, v in self.providers_config.items()
                 if product_type in getattr(v, "products", {}).keys()
-            )
+            ]
         else:
-            return sorted(tuple(self.providers_config.keys()))
+            providers = [
+                (v.group if by_group and hasattr(v, "group") else k, v.priority)
+                for k, v in self.providers_config.items()
+            ]
+
+        # If by_group is True, keep only the highest priority for each group
+        if by_group:
+            group_priority: Dict[str, int] = {}
+            for name, priority in providers:
+                if name not in group_priority or priority > group_priority[name]:
+                    group_priority[name] = priority
+            providers = list(group_priority.items())
+
+        # Sort by priority (descending) and then by name (ascending)
+        providers.sort(key=lambda x: (-x[1], x[0]))
+
+        # Return only the names of the providers or groups
+        return [name for name, _ in providers]
 
     def get_product_type_from_alias(self, alias_or_id: str) -> str:
         """Return the ID of a product type by either its ID or alias
diff --git a/eodag/cli.py b/eodag/cli.py
index a1ce549a..99734a31 100755
--- a/eodag/cli.py
+++ b/eodag/cli.py
@@ -445,8 +445,6 @@ def list_pt(ctx: Context, **kwargs: Any) -> None:
                     provider=provider, fetch_providers=fetch_providers
                 )
                 if pt["ID"] in guessed_product_types
-                or "alias" in pt
-                and pt["alias"] in guessed_product_types
             ]
         else:
             product_types = dag.list_product_types(
diff --git a/eodag/config.py b/eodag/config.py
index b9953645..1b7e4aa4 100644
--- a/eodag/config.py
+++ b/eodag/config.py
@@ -129,7 +129,11 @@ class ProviderConfig(yaml.YAMLObject):
     """
 
     name: str
+    group: str
     priority: int = 0  # Set default priority to 0
+    roles: List[str]
+    description: str
+    url: str
     api: PluginConfig
     search: PluginConfig
     products: Dict[str, Any]
@@ -142,7 +146,7 @@ class ProviderConfig(yaml.YAMLObject):
     yaml_tag = "!provider"
 
     @classmethod
-    def from_yaml(cls, loader: yaml.Loader, node: Any) -> ProviderConfig:
+    def from_yaml(cls, loader: yaml.Loader, node: Any) -> Iterator[ProviderConfig]:
         """Build a :class:`~eodag.config.ProviderConfig` from Yaml"""
         cls.validate(tuple(node_key.value for node_key, _ in node.value))
         for node_key, node_value in node.value:
@@ -437,7 +441,7 @@ def load_config(config_path: str) -> Dict[str, ProviderConfig]:
     :returns: The default provider's configuration
     :rtype: dict
     """
-    logger.debug(f"Loading configuration from {config_path}")
+    logger.debug("Loading configuration from %s", config_path)
     config: Dict[str, ProviderConfig] = {}
     try:
         # Providers configs are stored in this file as separated yaml documents
diff --git a/eodag/plugins/manager.py b/eodag/plugins/manager.py
index 3e94a8f3..1350cb6d 100644
--- a/eodag/plugins/manager.py
+++ b/eodag/plugins/manager.py
@@ -173,11 +173,13 @@ class PluginManager:
         :param product_type: (optional) The product type that the constructed plugins
                              must support
         :type product_type: str
-        :param provider: (optional) The provider on which to get the search plugin
+        :param provider: (optional) The provider or the provider group on which to get
+            the search plugins
         :type provider: str
         :returns: All the plugins supporting the product type, one by one (a generator
                   object)
-        :rtype: types.GeneratorType(:class:`~eodag.plugins.search.Search` or :class:`~eodag.plugins.download.Api`)
+        :rtype: types.GeneratorType(:class:`~eodag.plugins.search.Search`
+            or :class:`~eodag.plugins.download.Api`)
         :raises: :class:`~eodag.utils.exceptions.UnsupportedProvider`
         """
 
@@ -209,7 +211,9 @@ class PluginManager:
             configs = list(self.providers_config.values())
 
         if provider:
-            configs = [c for c in configs if provider == c.name]
+            configs = [
+                c for c in configs if provider in [getattr(c, "group", None), c.name]
+            ]
 
         if not configs:
             raise UnsupportedProvider(f"{provider} is not (yet) supported")
@@ -233,7 +237,6 @@ class PluginManager:
                 Download,
                 self._build_plugin(product.provider, download, Download),
             )
-            return plugin
         elif api := getattr(plugin_conf, "api", None):
             plugin_conf.api.priority = plugin_conf.priority
             plugin = cast(Api, self._build_plugin(product.provider, api, Api))
diff --git a/eodag/resources/stac.yml b/eodag/resources/stac.yml
index 7e631caa..d0472f87 100644
--- a/eodag/resources/stac.yml
+++ b/eodag/resources/stac.yml
@@ -136,14 +136,6 @@ collection:
       - '$.product_type.instrument'
     processing:level: '$.product_type.processingLevel'
 
-provider:
-  name: '$.provider.name'
-  description: '$.provider.description'
-  # one or more of "producer" "licensor" "processor" "host"
-  roles: '$.provider.roles'
-  url: '$.provider.url'
-  priority: '$.provider.priority'
-
 # Data ------------------------------------------------------------------------
 
 # https://stacspec.org/STAC-api.html#operation/getFeatures
diff --git a/eodag/rest/stac.py b/eodag/rest/stac.py
index 5cd0beae..a1e514f6 100644
--- a/eodag/rest/stac.py
+++ b/eodag/rest/stac.py
@@ -21,7 +21,6 @@ import logging
 import os
 from collections import defaultdict
 from datetime import datetime, timezone
-from operator import itemgetter
 from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, cast
 from urllib.parse import parse_qs, urlencode, urlparse
 
@@ -170,6 +169,21 @@ class StacCommon:
 
     __geo_interface__ = property(as_dict)
 
+    def get_provider_dict(self, provider: str) -> Dict[str, Any]:
+        """Generate STAC provider dict"""
+        provider_config = [
+            p
+            for p in self.eodag_api.providers_config.values()
+            if provider in [p.name, getattr(p, "group", None)]
+        ][0]
+        return {
+            "name": getattr(provider_config, "group", provider_config.name),
+            "description": getattr(provider_config, "description", None),
+            "roles": getattr(provider_config, "roles", None),
+            "url": getattr(provider_config, "url", None),
+            "priority": getattr(provider_config, "priority", None),
+        }
+
 
 class StacItem(StacCommon):
     """Stac item object
@@ -220,7 +234,6 @@ class StacItem(StacCommon):
         item_model = self.__filter_item_model_properties(
             self.stac_config["item"], str(search_results[0].product_type)
         )
-        provider_model = deepcopy(self.stac_config["provider"])
 
         # check if some items need to be converted
         need_conversion: Dict[str, Any] = {}
@@ -238,23 +251,13 @@ class StacItem(StacCommon):
 
         item_list: List[Dict[str, Any]] = []
         for product in search_results:
-            # parse jsonpath
-            provider_dict = jsonpath_parse_dict_items(
-                provider_model,
-                {
-                    "provider": self.eodag_api.providers_config[
-                        product.provider
-                    ].__dict__
-                },
-            )
-
             product_dict = deepcopy(product.__dict__)
 
             product_item = jsonpath_parse_dict_items(
                 item_model,
                 {
                     "product": product_dict,
-                    "providers": [provider_dict],
+                    "providers": [self.get_provider_dict(product.provider)],
                 },
             )
             # parse download link
@@ -556,12 +559,6 @@ class StacItem(StacCommon):
         item_model = self.__filter_item_model_properties(
             self.stac_config["item"], product_type
         )
-        provider_model = deepcopy(self.stac_config["provider"])
-
-        provider_dict = jsonpath_parse_dict_items(
-            provider_model,
-            {"provider": self.eodag_api.providers_config[product.provider].__dict__},
-        )
 
         catalog = StacCatalog(
             url=self.url.split("/items")[0],
@@ -580,7 +577,7 @@ class StacItem(StacCommon):
             item_model,
             {
                 "product": product_dict,
-                "providers": provider_dict,
+                "providers": self.get_provider_dict(product.provider),
             },
         )
         # parse f-strings
@@ -714,39 +711,31 @@ class StacCollection(StacCommon):
         :rtype: list
         """
         collection_model = deepcopy(self.stac_config["collection"])
-        provider_model = deepcopy(self.stac_config["provider"])
 
         product_types = self.__get_product_types(filters)
 
         collection_list: List[Dict[str, Any]] = []
         for product_type in product_types:
-            if self.provider:
-                providers = [self.provider]
-            else:
-                providers = sorted(
-                    [
-                        self.eodag_api.providers_config[p].__dict__
-                        for p in self.eodag_api.available_providers(
-                            product_type=product_type["_id"]
+            # get available providers for each product_type
+            providers = (
+                [self.provider]
+                if self.provider
+                else [
+                    plugin.provider
+                    for plugin in self.eodag_api._plugins_manager.get_search_plugins(
+                        product_type=(
+                            product_type.get("_id", None) or product_type["ID"]
                         )
-                    ],
-                    key=itemgetter("priority"),
-                    reverse=True,
-                )
-            providers_models: List[Dict[str, Any]] = []
-            for provider in providers:
-                provider_m = jsonpath_parse_dict_items(
-                    provider_model,
-                    {"provider": provider},
-                )
-                providers_models.append(provider_m)
+                    )
+                ]
+            )
 
             # parse jsonpath
             product_type_collection = jsonpath_parse_dict_items(
                 collection_model,
                 {
                     "product_type": product_type,
-                    "providers": providers_models,
+                    "providers": [self.get_provider_dict(p) for p in providers],
                 },
             )
             # override EODAG's collection with the external collection
@@ -764,10 +753,10 @@ class StacCollection(StacCommon):
                 product_type_collection["keywords"] = merged_keywords
             # parse f-strings
             format_args = deepcopy(self.stac_config)
-            format_args["collection"] = dict(
-                product_type_collection,
+            format_args["collection"] = {
+                **product_type_collection,
                 **{"url": f"{self.url}/{product_type['ID']}", "root": self.root},
-            )
+            }
             product_type_collection = format_dict_items(
                 product_type_collection, **format_args
             )


Test patch:
diff --git a/tests/integration/test_core_search.py b/tests/integration/test_core_search.py
index 573726bc..b83efb16 100644
--- a/tests/integration/test_core_search.py
+++ b/tests/integration/test_core_search.py
@@ -236,7 +236,7 @@ class TestCoreSearch(unittest.TestCase):
         available_providers = self.dag.available_providers(product_type)
         self.assertListEqual(
             available_providers,
-            ["cop_dataspace", "creodias", "onda", "peps", "sara", "wekeo"],
+            ["peps", "cop_dataspace", "creodias", "onda", "sara", "wekeo"],
         )
 
         products, count = self.dag.search(productType="S1_SAR_SLC")
@@ -264,7 +264,7 @@ class TestCoreSearch(unittest.TestCase):
         available_providers = self.dag.available_providers(product_type)
         self.assertListEqual(
             available_providers,
-            ["cop_dataspace", "creodias", "onda", "peps", "sara", "wekeo"],
+            ["peps", "cop_dataspace", "creodias", "onda", "sara", "wekeo"],
         )
 
         self.assertRaises(
@@ -291,7 +291,7 @@ class TestCoreSearch(unittest.TestCase):
         available_providers = self.dag.available_providers(product_type)
         self.assertListEqual(
             available_providers,
-            ["cop_dataspace", "creodias", "onda", "peps", "sara", "wekeo"],
+            ["peps", "cop_dataspace", "creodias", "onda", "sara", "wekeo"],
         )
 
         # peps comes 1st by priority
@@ -338,7 +338,7 @@ class TestCoreSearch(unittest.TestCase):
         available_providers = self.dag.available_providers(product_type)
         self.assertListEqual(
             available_providers,
-            ["cop_dataspace", "creodias", "onda", "peps", "sara", "wekeo"],
+            ["peps", "cop_dataspace", "creodias", "onda", "sara", "wekeo"],
         )
 
         # onda comes 3rd by priority
@@ -373,7 +373,7 @@ class TestCoreSearch(unittest.TestCase):
         available_providers = self.dag.available_providers(product_type)
         self.assertListEqual(
             available_providers,
-            ["cop_dataspace", "creodias", "onda", "peps", "sara", "wekeo"],
+            ["peps", "cop_dataspace", "creodias", "onda", "sara", "wekeo"],
         )
 
         mock_query.side_effect = [
@@ -400,7 +400,7 @@ class TestCoreSearch(unittest.TestCase):
         available_providers = self.dag.available_providers(product_type)
         self.assertListEqual(
             available_providers,
-            ["cop_dataspace", "creodias", "onda", "peps", "sara", "wekeo"],
+            ["peps", "cop_dataspace", "creodias", "onda", "sara", "wekeo"],
         )
 
         mock_query.return_value = ([], 0)
diff --git a/tests/test_cli.py b/tests/test_cli.py
index f54e24bb..975913b1 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -615,9 +615,7 @@ class TestEodagCli(unittest.TestCase):
         self.assertEqual(result.exit_code, 1)
         self.assertIn("Unsupported provider. You may have a typo", result.output)
         self.assertIn(
-            "Available providers: {}".format(
-                ", ".join(sorted(test_core.TestCore.SUPPORTED_PROVIDERS))
-            ),
+            f"Available providers: {', '.join(test_core.TestCore.SUPPORTED_PROVIDERS)}",
             result.output,
         )
 
diff --git a/tests/units/test_core.py b/tests/units/test_core.py
index 2e863669..8b4ba1ea 100644
--- a/tests/units/test_core.py
+++ b/tests/units/test_core.py
@@ -521,30 +521,30 @@ class TestCore(TestCoreBase):
     }
     SUPPORTED_PROVIDERS = [
         "peps",
-        "usgs",
-        "theia",
-        "creodias",
-        "onda",
-        "aws_eos",
         "astraea_eod",
-        "usgs_satapi_aws",
+        "aws_eos",
+        "cop_ads",
+        "cop_cds",
+        "cop_dataspace",
+        "creodias",
+        "creodias_s3",
+        "dedl",
+        "dedt_lumi",
         "earth_search",
         "earth_search_cog",
         "earth_search_gcs",
         "ecmwf",
-        "cop_ads",
-        "cop_cds",
-        "sara",
+        "eumetsat_ds",
+        "hydroweb_next",
         "meteoblue",
-        "cop_dataspace",
+        "onda",
         "planetary_computer",
-        "hydroweb_next",
+        "sara",
+        "theia",
+        "usgs",
+        "usgs_satapi_aws",
         "wekeo",
         "wekeo_cmems",
-        "creodias_s3",
-        "dedt_lumi",
-        "dedl",
-        "eumetsat_ds",
     ]
 
     def setUp(self):
@@ -1636,7 +1636,8 @@ class TestCoreInvolvingConfDir(unittest.TestCase):
     def execution_involving_conf_dir(self, inspect=None, conf_dir=None):
         """Check that the path(s) inspected (str, list) are created after the instantation
         of EODataAccessGateway. If they were already there, rename them (.old), instantiate,
-        check, delete the new files, and restore the existing files to there previous name."""
+        check, delete the new files, and restore the existing files to there previous name.
+        """
         if inspect is not None:
             if conf_dir is None:
                 conf_dir = os.path.join(os.path.expanduser("~"), ".config", "eodag")
@@ -2350,6 +2351,7 @@ class TestCoreSearch(TestCoreBase):
 
     def test__do_search_does_not_raise_by_default(self):
         """_do_search must not raise any error by default"""
+
         # provider attribute required internally by __do_search for logging purposes.
         class DummyConfig:
             pagination = {}
@@ -2680,7 +2682,8 @@ class TestCoreSearch(TestCoreBase):
 
     def test_search_sort_by_raise_errors(self):
         """search used with "sortBy" argument must raise errors if the argument is incorrect or if the provider does
-        not support a maximum number of sorting parameter, one sorting parameter or the sorting feature"""
+        not support a maximum number of sorting parameter, one sorting parameter or the sorting feature
+        """
         dag = EODataAccessGateway()
         dummy_provider_config = """
         dummy_provider:
@@ -2975,3 +2978,73 @@ class TestCoreProductAlias(TestCoreBase):
         # not existing product type
         with self.assertRaises(NoMatchingProductType):
             self.dag.get_product_type_from_alias("JUST_A_TYPE")
+
+
+class TestCoreProviderGroup(TestCoreBase):
+    group = ("peps", "creodias")
+    group_name = "testgroup"
+
+    @classmethod
+    def setUpClass(cls) -> None:
+        super().setUpClass()
+        cls.dag = EODataAccessGateway()
+        providers_configs = cls.dag.providers_config
+
+        setattr(providers_configs[cls.group[0]], "group", cls.group_name)
+        setattr(providers_configs[cls.group[1]], "group", cls.group_name)
+
+    def test_available_providers_by_group(self) -> None:
+        """
+        The method available_providers returns only one entry for both grouped providers
+        """
+        # drop the grouped names from expected providers, as they are grouped under "testgroup" name
+        providers = self.dag.available_providers()
+        providers.remove(self.group[0])
+        providers.remove(self.group[1])
+        providers.append(self.group_name)
+
+        self.assertCountEqual(self.dag.available_providers(by_group=True), providers)
+
+    def test_list_product_types(self) -> None:
+        """
+        List the product types for the provider group.
+        EODAG return the merged list of product types from both providers of the group.
+        """
+
+        earth_search_products = self.dag.list_product_types(
+            self.group[0], fetch_providers=False
+        )
+        earth_search_cog_products = self.dag.list_product_types(
+            self.group[1], fetch_providers=False
+        )
+
+        merged_list = list(
+            {
+                d["ID"]: d for d in earth_search_products + earth_search_cog_products
+            }.values()
+        )
+
+        self.assertCountEqual(
+            self.dag.list_product_types(self.group_name, fetch_providers=False),
+            merged_list,
+        )
+
+    def test_get_search_plugins(
+        self,
+    ) -> None:
+        """
+        The method _plugins_manager.get_search_plugins is called with provider group
+        It returns a list containing the 2 grouped plugins
+        """
+        plugin1 = list(
+            self.dag._plugins_manager.get_search_plugins(provider=self.group[0])
+        )
+        plugin2 = list(
+            self.dag._plugins_manager.get_search_plugins(provider=self.group[1])
+        )
+
+        group_plugins = list(
+            self.dag._plugins_manager.get_search_plugins(provider=self.group_name)
+        )
+
+        self.assertCountEqual(group_plugins, [*plugin1, *plugin2])