Repository: CS-SI/eodag
Pull request #1559 (2025-03-17 10:12:53)

feat(core): add EODAG_PRODUCT_TYPES_CFG_FILE env var
add `EODAG_PRODUCT_TYPES_CFG_FILE` environment variable to configure the path for the `product_types.yml` file

github-actions[bot]: ## Test Results
    4 files  ±0      4 suites  ±0   6m 41s ⏱️ +10s
  609 tests ±0    606 ✅ ±0   3 💤 ±0  0 ❌ ±0 
2 436 runs  ±0  2 338 ✅ ±0  98 💤 ±0  0 ❌ ±0 

Results for commit dd5469fd. ± Comparison against base commit 4d6f726a.

[test-results]:data:application/gzip;base64,H4sIAG7212cC/03MSw6DIBSF4a0Yxh0AFxC6mcbySEhVGh4j070XrFKH/3eSsyHnZ5vQfWC3AaXicw9T4pR9WFtiUqFOuY0Cq7MeqWi9k/jTy78rQQc3+bkC7mBjDPGQWNb2SRmIo85PCiA7/T5Vh8vn3tdLHZbF5xrIGM6EcoZIPUpLYcRaGvkkgLnAkjHuQFkgHH2+YSWtBggBAAA=

github-actions[bot]: ![badge](https://raw.githubusercontent.com/CS-SI/eodag/_xml_coverage_reports/data/env-var-product-types-config/./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%   80-82, 91, 99, 103-105, 176, 187, 692-694, 808-811, 854-855, 864-865, 970, 1029-1034, 1036
crunch.py                                        5       5  0.00%    20-24
api/__init__.py                                  0       0  100.00%
api/core.py                                    783      74  90.55%   316-317, 367, 657, 701-704, 742, 786, 820, 865-870, 896, 987, 1055, 1193, 1278-1290, 1326, 1328, 1356, 1360-1371, 1384-1390, 1473-1476, 1509-1529, 1581, 1598-1602, 1614-1617, 1953, 1977-1983, 2234, 2238-2242, 2251-2253, 2297-2298, 2327-2328
api/search_result.py                            58       4  93.10%   82, 91, 98, 112
api/product/__init__.py                          6       0  100.00%
api/product/_assets.py                          48       5  89.58%   90, 176, 184, 187-191
api/product/_product.py                        196      19  90.31%   70-72, 236-237, 312, 341, 398, 412-415, 428, 452-455, 515-518
api/product/metadata_mapping.py                699      65  90.70%   119-121, 218, 250-251, 297-298, 308-320, 322, 333, 358, 398-399, 436, 457-460, 483, 491-492, 585-586, 610-611, 617-620, 635-636, 785, 831, 984, 993-997, 1014-1019, 1146, 1160-1180, 1200, 1205, 1334, 1348, 1373, 1419, 1471, 1494-1495, 1515-1519, 1535, 1543
api/product/drivers/__init__.py                 20       2  90.00%   33-36
api/product/drivers/base.py                     29       1  96.55%   98
api/product/drivers/generic.py                   7       0  100.00%
api/product/drivers/sentinel1.py                15       0  100.00%
api/product/drivers/sentinel2.py                15       0  100.00%
plugins/__init__.py                              0       0  100.00%
plugins/base.py                                 21       2  90.48%   48, 55
plugins/manager.py                             172      14  91.86%   106-111, 161, 199, 221, 225, 249, 389-392, 404-405
plugins/apis/__init__.py                         0       0  100.00%
plugins/apis/base.py                             4       0  100.00%
plugins/apis/ecmwf.py                           97      10  89.69%   167-169, 216-217, 243-245, 298-299
plugins/apis/usgs.py                           180      26  85.56%   156, 258, 292, 327-329, 334, 360-361, 366, 396-403, 414-419, 441-447, 478
plugins/authentication/__init__.py               6       1  83.33%   31
plugins/authentication/aws_auth.py              20       0  100.00%
plugins/authentication/base.py                  17       2  88.24%   45, 58
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%   43-45, 49-52
plugins/authentication/openid_connect.py       208      28  86.54%   80-81, 93-111, 149, 155-183, 191, 323-326, 352, 386
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     152  69.04%   270, 283, 350-353, 367-371, 413-415, 419, 451-452, 458-462, 492, 524, 528, 535, 565-573, 577, 609-617, 628-630, 661-735, 753-811, 822-827, 880, 895-897, 900, 910-918, 926-939, 949-971, 978-990, 1028, 1054, 1099-1101, 1321
plugins/download/base.py                       253      42  83.40%   127, 155, 235-238, 291-292, 340-344, 350-351, 393, 396-410, 422, 426, 490-494, 524-525, 550-558, 560-564, 607, 629, 651, 659
plugins/download/creodias_s3.py                 25       9  64.00%   55-69
plugins/download/http.py                       529      95  82.04%   227, 323-326, 329, 336-341, 359-374, 391, 403, 451, 458-464, 482, 496, 510, 518-520, 536-541, 552, 570, 606-609, 638, 642, 662, 739, 758-769, 777-782, 792-809, 827, 857-866, 902, 927-928, 947-952, 958, 961, 978, 981, 996-997, 1032, 1094, 1109, 1168-1169, 1175, 1185, 1221, 1257, 1277, 1312-1314
plugins/download/s3rest.py                     116      24  79.31%   119, 153, 160, 195, 222-229, 232-234, 238, 249-255, 263-264, 267-271, 294, 315-318
plugins/search/__init__.py                      22       0  100.00%
plugins/search/base.py                         145      11  92.41%   101, 105, 129-135, 276, 296, 429
plugins/search/build_search_result.py          352      37  89.49%   241-242, 278, 281, 412-423, 447, 559, 561, 622, 629, 653, 688, 739, 788-803, 848, 873, 876, 880, 1000-1001, 1007, 1018, 1097
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                   25       1  96.00%   51
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%   189-192, 208, 219, 223-224, 235, 240, 245, 252, 265-268, 322-323, 327, 337-343, 348, 374-377, 385-396, 413, 415, 422-425, 427-428, 446-450, 483, 493, 504, 517, 523-538, 543
plugins/search/qssearch.py                     701      88  87.45%   448, 506, 520, 524-530, 558-562, 675-687, 732-735, 806-807, 855, 874, 881, 893, 950, 971, 974-975, 984-985, 994-995, 1004-1005, 1032, 1114-1119, 1123-1132, 1166, 1188, 1261-1262, 1268, 1358, 1455-1459, 1519, 1522, 1528-1529, 1550, 1578-1590, 1597, 1629-1631, 1641-1647, 1677, 1700, 1705-1706, 1721, 1737, 1822-1825, 1830-1833, 1842, 1874-1878, 1884
plugins/search/stac_list_assets.py              25      10  60.00%   44-51, 75-85
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      67  74.23%   258, 266, 284-301, 316-352, 446, 483-522, 599, 705, 712-765
rest/errors.py                                  69       5  92.75%   106, 116, 127, 143-144
rest/server.py                                 192      24  87.50%   86, 109-111, 285-290, 318, 514-516, 533-538, 567, 569, 573-574, 578-579
rest/stac.py                                   319      63  80.25%   309, 331, 383-386, 413-440, 471-473, 496, 528-529, 611-651, 673-689, 781-785, 792, 846-847, 908, 998-1000
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                        64      12  81.25%   61, 71-79, 105-110, 174
rest/types/stac_search.py                      125       7  94.40%   128, 174, 189-191, 199, 203
rest/utils/__init__.py                          94      12  87.23%   101-102, 121-123, 175, 185-199
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                              148      40  72.97%   67, 80-84, 95-107, 135-137, 144-149, 239, 249-265, 270, 272, 294, 299, 307, 317
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                                 81      16  80.25%   129-132, 136-143, 155-161, 174-176
utils/__init__.py                              538      38  92.94%   81, 198-199, 208-235, 238, 253, 333-337, 410-414, 433-435, 449, 541, 556, 596-597, 626, 1001-1004, 1051-1052, 1099-1100, 1234
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/s3.py                                     65       3  95.38%   139-140, 203
utils/stac_reader.py                           111      45  59.46%   56-57, 63-85, 95-97, 101, 137, 153-156, 203-212, 222-252
TOTAL                                        10132    1610  84.11%
```

</details>


### Diff against develop

```
Filename      Stmts    Miss  Cover
----------  -------  ------  --------
TOTAL             0       0  +100.00%
```


Results for commit: dd5469fd18c78e2370c8d8b1305608445f39e315


_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/env-var-product-types-config/./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%   80-82, 91, 99, 103-105, 176, 187, 692-694, 808-811, 854-855, 864-865, 970, 999, 1029-1034, 1036
crunch.py                                        5       5  0.00%    20-24
api/__init__.py                                  0       0  100.00%
api/core.py                                    783      74  90.55%   316-317, 367, 657, 701-704, 742, 786, 820, 865-870, 896, 987, 1055, 1193, 1278-1290, 1326, 1328, 1356, 1360-1371, 1384-1390, 1473-1476, 1509-1529, 1581, 1598-1602, 1614-1617, 1953, 1977-1983, 2234, 2238-2242, 2251-2253, 2297-2298, 2327-2328
api/search_result.py                            58       4  93.10%   82, 91, 98, 112
api/product/__init__.py                          6       0  100.00%
api/product/_assets.py                          48       5  89.58%   90, 176, 184, 187-191
api/product/_product.py                        196      19  90.31%   70-72, 236-237, 312, 341, 398, 412-415, 428, 452-455, 515-518
api/product/metadata_mapping.py                699      65  90.70%   119-121, 218, 250-251, 297-298, 308-320, 322, 333, 358, 398-399, 436, 457-460, 483, 491-492, 585-586, 610-611, 617-620, 635-636, 785, 831, 984, 993-997, 1014-1019, 1146, 1160-1180, 1200, 1205, 1334, 1348, 1373, 1419, 1471, 1494-1495, 1515-1519, 1535, 1543
api/product/drivers/__init__.py                 20       2  90.00%   33-36
api/product/drivers/base.py                     29       1  96.55%   98
api/product/drivers/generic.py                   7       0  100.00%
api/product/drivers/sentinel1.py                15       0  100.00%
api/product/drivers/sentinel2.py                15       0  100.00%
plugins/__init__.py                              0       0  100.00%
plugins/base.py                                 21       3  85.71%   48, 55, 68
plugins/manager.py                             172      14  91.86%   106-111, 161, 199, 221, 225, 249, 389-392, 404-405
plugins/apis/__init__.py                         0       0  100.00%
plugins/apis/base.py                             4       0  100.00%
plugins/apis/ecmwf.py                           97      10  89.69%   167-169, 216-217, 243-245, 298-299
plugins/apis/usgs.py                           180      26  85.56%   156, 258, 292, 327-329, 334, 360-361, 366, 396-403, 414-419, 441-447, 478
plugins/authentication/__init__.py               6       1  83.33%   31
plugins/authentication/aws_auth.py              20       0  100.00%
plugins/authentication/base.py                  17       2  88.24%   45, 58
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%   43-45, 49-52
plugins/authentication/openid_connect.py       208      28  86.54%   80-81, 93-111, 149, 155-183, 191, 323-326, 352, 386
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     152  69.04%   270, 283, 350-353, 367-371, 413-415, 419, 451-452, 458-462, 492, 524, 528, 535, 565-573, 577, 609-617, 628-630, 661-735, 753-811, 822-827, 880, 895-897, 900, 910-918, 926-939, 949-971, 978-990, 1028, 1054, 1099-1101, 1321
plugins/download/base.py                       253      44  82.61%   127, 155, 222-224, 235-238, 291-292, 340-344, 350-351, 393, 396-410, 422, 426, 490-494, 524-525, 550-558, 560-564, 607, 629, 651, 659
plugins/download/creodias_s3.py                 25       9  64.00%   55-69
plugins/download/http.py                       529      95  82.04%   227, 323-326, 329, 336-341, 359-374, 391, 403, 451, 458-464, 482, 496, 510, 518-520, 536-541, 552, 570, 606-609, 638, 642, 662, 739, 758-769, 777-782, 792-809, 827, 857-866, 902, 927-928, 947-952, 958, 961, 978, 981, 996-997, 1032, 1094, 1109, 1168-1169, 1175, 1185, 1221, 1257, 1277, 1312-1314
plugins/download/s3rest.py                     116      24  79.31%   119, 153, 160, 195, 222-229, 232-234, 238, 249-255, 263-264, 267-271, 294, 315-318
plugins/search/__init__.py                      22       0  100.00%
plugins/search/base.py                         145      11  92.41%   101, 105, 129-135, 276, 296, 429
plugins/search/build_search_result.py          352      38  89.20%   241-242, 278, 281, 412-423, 447, 559, 561, 622, 629, 653, 688, 739, 754, 788-803, 848, 873, 876, 880, 1000-1001, 1007, 1018, 1097
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                   25       1  96.00%   51
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%   189-192, 208, 219, 223-224, 235, 240, 245, 252, 265-268, 322-323, 327, 337-343, 348, 374-377, 385-396, 413, 415, 422-425, 427-428, 446-450, 483, 493, 504, 517, 523-538, 543
plugins/search/qssearch.py                     701      98  86.02%   448, 506, 520, 524-530, 558-562, 675-687, 732-735, 806-807, 855, 874, 881, 893, 950, 971, 974-975, 984-985, 994-995, 1004-1005, 1032, 1114-1119, 1123-1132, 1166, 1188, 1261-1262, 1268, 1358, 1455-1459, 1519, 1522, 1528-1529, 1550, 1578-1590, 1597, 1629-1631, 1641-1647, 1677, 1700, 1705-1706, 1721, 1737, 1809-1810, 1822-1825, 1830-1833, 1842, 1863-1865, 1874-1878, 1883-1893, 1915
plugins/search/stac_list_assets.py              25      10  60.00%   44-51, 75-85
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-592
rest/stac.py                                   319      68  78.68%   243, 309, 331, 383-386, 413-440, 471-473, 496, 528-529, 611-651, 673-689, 716, 781-785, 792, 846-847, 853, 908, 946, 979, 998-1000
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                        64      20  68.75%   54-55, 60-63, 68-79, 105-110, 119-120, 174
rest/types/stac_search.py                      125      11  91.20%   126-128, 174, 189-191, 199, 203, 251, 254
rest/utils/__init__.py                          94      30  68.09%   72-78, 98, 101-102, 121-123, 136, 143, 168-176, 183-204
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                              148      42  71.62%   67, 71, 80-84, 95-107, 135-137, 144-149, 204, 239, 249-265, 270, 272, 294, 299, 307, 317
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                                 81      16  80.25%   129-132, 136-143, 155-161, 174-176
utils/__init__.py                              538      38  92.94%   81, 198-199, 208-235, 238, 253, 333-337, 410-414, 433-435, 449, 541, 556, 596-597, 626, 1001-1004, 1051-1052, 1099-1100, 1234
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/s3.py                                     65       3  95.38%   139-140, 203
utils/stac_reader.py                           111      45  59.46%   56-57, 63-85, 95-97, 101, 137, 153-156, 203-212, 222-252
TOTAL                                        10132    2017  80.09%
```

</details>


### Diff against develop

```
Filename      Stmts    Miss  Cover
----------  -------  ------  --------
TOTAL             0       0  +100.00%
```


Results for commit: dd5469fd18c78e2370c8d8b1305608445f39e315


_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/docs/getting_started_guide/configure.rst b/docs/getting_started_guide/configure.rst
index 9c90d4dd..728d53e5 100644
--- a/docs/getting_started_guide/configure.rst
+++ b/docs/getting_started_guide/configure.rst
@@ -149,6 +149,7 @@ Some EODAG core settings can be overriden using environment variables:
   `locations <https://eodag.readthedocs.io/en/stable/notebooks/api_user_guide/4_search.html#Locations-search>`_
   configuration file
 * ``EODAG_PROVIDERS_CFG_FILE`` for defining the desired path to the providers configuration file
+* ``EODAG_PRODUCT_TYPES_CFG_FILE`` for defining the desired path to the product types configuration file
 * ``EODAG_EXT_PRODUCT_TYPES_CFG_FILE`` for defining the desired path to the `external product types configuration file\
   <https://eodag.readthedocs.io/en/stable/notebooks/api_user_guide/2_providers_products_available.html#Product-types-discovery>`_
 
diff --git a/eodag/api/core.py b/eodag/api/core.py
index 02ca7713..0f9fdfb6 100644
--- a/eodag/api/core.py
+++ b/eodag/api/core.py
@@ -119,7 +119,7 @@ class EODataAccessGateway:
         user_conf_file_path: Optional[str] = None,
         locations_conf_path: Optional[str] = None,
     ) -> None:
-        product_types_config_path = str(
+        product_types_config_path = os.getenv("EODAG_PRODUCT_TYPES_CFG_FILE") or str(
             res_files("eodag") / "resources" / "product_types.yml"
         )
         self.product_types_config = SimpleYamlProxyConfig(product_types_config_path)
diff --git a/eodag/plugins/download/aws.py b/eodag/plugins/download/aws.py
index 1d76df67..a31ae25d 100644
--- a/eodag/plugins/download/aws.py
+++ b/eodag/plugins/download/aws.py
@@ -60,6 +60,7 @@ from eodag.utils.exceptions import (
     NotAvailableError,
     TimeOutError,
 )
+from eodag.utils.s3 import open_s3_zipped_object
 
 if TYPE_CHECKING:
     from boto3.resources.collection import ResourceCollection
@@ -233,6 +234,7 @@ class AwsDownload(Download):
         super(AwsDownload, self).__init__(provider, config)
         self.requester_pays = getattr(self.config, "requester_pays", False)
         self.s3_session: Optional[boto3.session.Session] = None
+        self.s3_resource: Optional[boto3.resources.base.ServiceResource] = None
 
     def download(
         self,
@@ -326,19 +328,32 @@ class AwsDownload(Download):
             bucket_names_and_prefixes, auth
         )
 
+        # files in zip
+        updated_bucket_names_and_prefixes = self._download_file_in_zip(
+            product, bucket_names_and_prefixes, product_local_path, progress_callback
+        )
+        # prevent nothing-to-download errors if download was performed in zip
+        raise_error = (
+            False
+            if len(updated_bucket_names_and_prefixes) != len(bucket_names_and_prefixes)
+            else True
+        )
+
         # downloadable files
         unique_product_chunks = self._get_unique_products(
-            bucket_names_and_prefixes,
+            updated_bucket_names_and_prefixes,
             authenticated_objects,
             asset_filter,
             ignore_assets,
             product,
+            raise_error=raise_error,
         )
 
         total_size = sum([p.size for p in unique_product_chunks]) or None
 
         # download
-        progress_callback.reset(total=total_size)
+        if len(unique_product_chunks) > 0:
+            progress_callback.reset(total=total_size)
         try:
             for product_chunk in unique_product_chunks:
                 try:
@@ -390,6 +405,52 @@ class AwsDownload(Download):
 
         return product_local_path
 
+    def _download_file_in_zip(
+        self, product, bucket_names_and_prefixes, product_local_path, progress_callback
+    ):
+        """
+        Download file in zip from a prefix like `foo/bar.zip!file.txt`
+        """
+        if self.s3_resource is None:
+            logger.debug("Cannot check files in s3 zip without s3 resource")
+            return bucket_names_and_prefixes
+
+        s3_client = self.s3_resource.meta.client
+
+        downloaded = []
+        for i, pack in enumerate(bucket_names_and_prefixes):
+            bucket_name, prefix = pack
+            if ".zip!" in prefix:
+                splitted_path = prefix.split(".zip!")
+                zip_prefix = f"{splitted_path[0]}.zip"
+                rel_path = splitted_path[-1]
+                dest_file = os.path.join(product_local_path, rel_path)
+                dest_abs_path_dir = os.path.dirname(dest_file)
+                if not os.path.isdir(dest_abs_path_dir):
+                    os.makedirs(dest_abs_path_dir)
+
+                with open_s3_zipped_object(
+                    bucket_name, zip_prefix, s3_client, partial=False
+                ) as zip_file:
+                    # file size
+                    file_info = zip_file.getinfo(rel_path)
+                    progress_callback.reset(total=file_info.file_size)
+                    with zip_file.open(rel_path) as extracted, open(
+                        dest_file, "wb"
+                    ) as output_file:
+                        # Read in 1MB chunks
+                        for zchunk in iter(lambda: extracted.read(1024 * 1024), b""):
+                            output_file.write(zchunk)
+                            progress_callback(len(zchunk))
+
+                downloaded.append(i)
+
+        return [
+            pack
+            for i, pack in enumerate(bucket_names_and_prefixes)
+            if i not in downloaded
+        ]
+
     def _download_preparation(
         self,
         product: EOProduct,
@@ -397,10 +458,12 @@ class AwsDownload(Download):
         **kwargs: Unpack[DownloadConf],
     ) -> tuple[Optional[str], Optional[str]]:
         """
-        preparation for the download:
+        Preparation for the download:
+
         - check if file was already downloaded
         - get file path
         - create directories
+
         :param product: product to be downloaded
         :param progress_callback: progress callback to be used
         :param kwargs: additional arguments
@@ -424,7 +487,8 @@ class AwsDownload(Download):
 
     def _configure_safe_build(self, build_safe: bool, product: EOProduct):
         """
-        updates the product properties with fetch metadata if safe build is enabled
+        Updates the product properties with fetch metadata if safe build is enabled
+
         :param build_safe: if safe build is enabled
         :param product: product to be updated
         """
@@ -514,10 +578,11 @@ class AwsDownload(Download):
         auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
     ) -> tuple[dict[str, Any], ResourceCollection]:
         """
-        authenticates with s3 and retrieves the available objects
-        raises an error when authentication is not possible
+        Authenticates with s3 and retrieves the available objects
+
         :param bucket_names_and_prefixes: list of bucket names and corresponding path prefixes
         :param auth: authentication information
+        :raises AuthenticationError: authentication is not possible
         :return: authenticated objects per bucket, list of available objects
         """
         if not isinstance(auth, (dict, type(None))):
@@ -584,14 +649,17 @@ class AwsDownload(Download):
         asset_filter: Optional[str],
         ignore_assets: bool,
         product: EOProduct,
+        raise_error: bool = True,
     ) -> set[Any]:
         """
-        retrieve unique product chunks based on authenticated objects and asset filters
+        Retrieve unique product chunks based on authenticated objects and asset filters
+
         :param bucket_names_and_prefixes: list of bucket names and corresponding path prefixes
         :param authenticated_objects: available objects per bucket
         :param asset_filter: text for which assets should be filtered
         :param ignore_assets: if product instead of individual assets should be used
         :param product: product that shall be downloaded
+        :param raise_error: raise error if there is nothing to download
         :return: set of product chunks that can be downloaded
         """
         product_chunks: list[Any] = []
@@ -613,12 +681,12 @@ class AwsDownload(Download):
                     unique_product_chunks,
                 )
             )
-            if not unique_product_chunks:
+            if not unique_product_chunks and raise_error:
                 raise NotAvailableError(
                     rf"No file basename matching re.fullmatch(r'{asset_filter}') was found in {product.remote_location}"
                 )
 
-        if not unique_product_chunks:
+        if not unique_product_chunks and raise_error:
             raise NoMatchingProductType("No product found to download.")
 
         return unique_product_chunks
@@ -702,6 +770,13 @@ class AwsDownload(Download):
             bucket_names_and_prefixes, auth
         )
 
+        # stream not implemented for prefixes like `foo/bar.zip!file.txt`
+        for _, prefix in bucket_names_and_prefixes:
+            if prefix and ".zip!" in prefix:
+                raise NotImplementedError(
+                    "Download streaming is not implemented for files in zip on S3"
+                )
+
         # downloadable files
         unique_product_chunks = self._get_unique_products(
             bucket_names_and_prefixes,
@@ -936,6 +1011,7 @@ class AwsDownload(Download):
                 objects = s3_resource.Bucket(bucket_name).objects
             list(objects.filter(Prefix=prefix).limit(1))
             self.s3_session = s3_session
+            self.s3_resource = s3_resource
             return objects
         else:
             return None
@@ -966,6 +1042,7 @@ class AwsDownload(Download):
                 objects = s3_resource.Bucket(bucket_name).objects
             list(objects.filter(Prefix=prefix).limit(1))
             self.s3_session = s3_session
+            self.s3_resource = s3_resource
             return objects
         else:
             return None
@@ -987,6 +1064,7 @@ class AwsDownload(Download):
             objects = s3_resource.Bucket(bucket_name).objects
         list(objects.filter(Prefix=prefix).limit(1))
         self.s3_session = s3_session
+        self.s3_resource = s3_resource
         return objects
 
     def get_product_bucket_name_and_prefix(
diff --git a/eodag/resources/providers.yml b/eodag/resources/providers.yml
index 4a2c6cdd..bfe16877 100644
--- a/eodag/resources/providers.yml
+++ b/eodag/resources/providers.yml
@@ -6109,6 +6109,9 @@
     ssl_verify: false
     ignore_assets: true
     archive_depth: 2
+    auth_error_code:
+      - 401
+      - 403
   auth: !plugin
     type: HTTPHeaderAuth
     matching_url: https://geodes-portal.cnes.fr
diff --git a/eodag/utils/s3.py b/eodag/utils/s3.py
index 12ed4f25..d57fe4b3 100644
--- a/eodag/utils/s3.py
+++ b/eodag/utils/s3.py
@@ -36,7 +36,7 @@ from eodag.utils.exceptions import (
 )
 
 if TYPE_CHECKING:
-    from zipfile import ZipInfo
+    from zipfile import ZipFile, ZipInfo
 
     from mypy_boto3_s3.client import S3Client
 
@@ -78,11 +78,11 @@ def parse_int(bytes: bytes) -> int:
     return val
 
 
-def list_files_in_s3_zipped_object(
-    bucket_name: str, key_name: str, client_s3: S3Client
-) -> List[ZipInfo]:
+def open_s3_zipped_object(
+    bucket_name: str, key_name: str, client_s3: S3Client, partial: bool = True
+) -> ZipFile:
     """
-    List files in s3 zipped object, without downloading it.
+    Open s3 zipped object, without downloading it.
 
     See https://stackoverflow.com/questions/41789176/how-to-count-files-inside-zip-in-aws-s3-without-downloading-it;
     Based on https://stackoverflow.com/questions/51351000/read-zip-files-from-s3-without-downloading-the-entire-file
@@ -90,6 +90,7 @@ def list_files_in_s3_zipped_object(
     :param bucket_name: Bucket name of the object to fetch
     :param key_name: Key name of the object to fetch
     :param client_s3: s3 client used to fetch the object
+    :param partial: fetch partial data if only content info is needed
     :returns: List of files in zip
     """
     response = client_s3.head_object(Bucket=bucket_name, Key=key_name)
@@ -104,11 +105,33 @@ def list_files_in_s3_zipped_object(
 
     # fetch central directory, append EOCD, and open as zipfile
     cd = fetch(bucket_name, key_name, cd_start, cd_size, client_s3)
-    zip = zipfile.ZipFile(io.BytesIO(cd + eocd))
 
-    logger.debug("Found %s files in %s" % (len(zip.filelist), key_name))
+    zip_data = (
+        cd + eocd if partial else fetch(bucket_name, key_name, 0, size, client_s3)
+    )
+
+    zip = zipfile.ZipFile(io.BytesIO(zip_data))
+
+    return zip
+
+
+def list_files_in_s3_zipped_object(
+    bucket_name: str, key_name: str, client_s3: S3Client
+) -> List[ZipInfo]:
+    """
+    List files in s3 zipped object, without downloading it.
+
+    See https://stackoverflow.com/questions/41789176/how-to-count-files-inside-zip-in-aws-s3-without-downloading-it;
+    Based on https://stackoverflow.com/questions/51351000/read-zip-files-from-s3-without-downloading-the-entire-file
 
-    return zip.filelist
+    :param bucket_name: Bucket name of the object to fetch
+    :param key_name: Key name of the object to fetch
+    :param client_s3: s3 client used to fetch the object
+    :returns: List of files in zip
+    """
+    with open_s3_zipped_object(bucket_name, key_name, client_s3) as zip_file:
+        logger.debug("Found %s files in %s" % (len(zip_file.filelist), key_name))
+        return zip_file.filelist
 
 
 def update_assets_from_s3(


Test patch:
diff --git a/tests/context.py b/tests/context.py
index e4c2215b..7385e295 100644
--- a/tests/context.py
+++ b/tests/context.py
@@ -98,7 +98,11 @@ from eodag.utils import (
     get_ssl_context,
 )
 from eodag.utils.requests import fetch_json
-from eodag.utils.s3 import list_files_in_s3_zipped_object, update_assets_from_s3
+from eodag.utils.s3 import (
+    list_files_in_s3_zipped_object,
+    update_assets_from_s3,
+    open_s3_zipped_object,
+)
 from eodag.utils.exceptions import (
     AddressNotFound,
     AuthenticationError,
diff --git a/tests/resources/file_product_types_override.yml b/tests/resources/file_product_types_override.yml
new file mode 100644
index 00000000..9d5a0cbe
--- /dev/null
+++ b/tests/resources/file_product_types_override.yml
@@ -0,0 +1,25 @@
+TEST_PRODUCT_1:
+  abstract: |
+    This is the first test product.
+  instrument: I1
+  platform: Platform1
+  platformSerialIdentifier: P1
+  processingLevel: L1
+  keywords: TEST,PRODUCT1
+  sensorType: test
+  license: other
+  missionStartDate: "2014-12-07T00:00:00Z"
+  title: Test Product 1
+
+TEST_PRODUCT_2:
+  abstract: |
+    This is the second test product.
+  instrument: I2
+  platform: Platform2
+  platformSerialIdentifier: P2
+  processingLevel: L2
+  keywords: TEST,PRODUCT2
+  sensorType: test
+  license: other
+  missionStartDate: "2014-12-07T00:00:00Z"
+  title: Test Product 2
diff --git a/tests/units/test_core.py b/tests/units/test_core.py
index df3f6c07..1ece0ffb 100644
--- a/tests/units/test_core.py
+++ b/tests/units/test_core.py
@@ -29,6 +29,7 @@ from importlib.resources import files as res_files
 from pathlib import Path
 from tempfile import TemporaryDirectory
 
+import yaml
 from lxml import html
 from pkg_resources import DistributionNotFound
 from pydantic import ValidationError
@@ -37,7 +38,7 @@ from shapely.geometry import LineString, MultiPolygon, Polygon
 
 from eodag import __version__ as eodag_version
 from eodag.types.queryables import QueryablesDict
-from eodag.utils import GENERIC_PRODUCT_TYPE
+from eodag.utils import GENERIC_PRODUCT_TYPE, cached_yaml_load_all
 from tests import TEST_RESOURCES_PATH
 from tests.context import (
     DEFAULT_MAX_ITEMS_PER_PAGE,
@@ -1932,19 +1933,12 @@ class TestCore(TestCoreBase):
 
 
 class TestCoreConfWithEnvVar(TestCoreBase):
-    @classmethod
-    def setUpClass(cls):
-        super(TestCoreConfWithEnvVar, cls).setUpClass()
-        cls.dag = EODataAccessGateway()
-        # mock os.environ to empty env
-        cls.mock_os_environ = mock.patch.dict(os.environ, {}, clear=True)
-        cls.mock_os_environ.start()
-
-    @classmethod
-    def tearDownClass(cls):
-        super(TestCoreConfWithEnvVar, cls).tearDownClass()
-        # stop os.environ
-        cls.mock_os_environ.stop()
+    def tearDown(self):
+        """Teardown run after every test"""
+        if dag := getattr(self, "dag", None):
+            index_dir = os.path.join(dag.conf_dir, ".index")
+            shutil.rmtree(index_dir)
+            del dag
 
     def test_core_object_prioritize_locations_file_in_envvar(self):
         """The core object must use the locations file pointed by the EODAG_LOCS_CFG_FILE env var"""
@@ -1952,9 +1946,9 @@ class TestCoreConfWithEnvVar(TestCoreBase):
             os.environ["EODAG_LOCS_CFG_FILE"] = os.path.join(
                 TEST_RESOURCES_PATH, "file_locations_override.yml"
             )
-            dag = EODataAccessGateway()
+            self.dag = EODataAccessGateway()
             self.assertEqual(
-                dag.locations_config,
+                self.dag.locations_config,
                 [dict(attr="dummyattr", name="dummyname", path="dummypath.shp")],
             )
         finally:
@@ -1966,11 +1960,13 @@ class TestCoreConfWithEnvVar(TestCoreBase):
             os.environ["EODAG_CFG_FILE"] = os.path.join(
                 TEST_RESOURCES_PATH, "file_config_override.yml"
             )
-            dag = EODataAccessGateway()
+            self.dag = EODataAccessGateway()
             # usgs priority is set to 5 in the test config overrides
-            self.assertEqual(dag.get_preferred_provider(), ("usgs", 5))
+            self.assertEqual(self.dag.get_preferred_provider(), ("usgs", 5))
             # peps outputs prefix is set to /data
-            self.assertEqual(dag.providers_config["peps"].download.output_dir, "/data")
+            self.assertEqual(
+                self.dag.providers_config["peps"].download.output_dir, "/data"
+            )
         finally:
             os.environ.pop("EODAG_CFG_FILE", None)
 
@@ -1980,16 +1976,46 @@ class TestCoreConfWithEnvVar(TestCoreBase):
             os.environ["EODAG_PROVIDERS_CFG_FILE"] = os.path.join(
                 TEST_RESOURCES_PATH, "file_providers_override.yml"
             )
-            dag = EODataAccessGateway()
+            self.dag = EODataAccessGateway()
             # only foo_provider in conf
-            self.assertEqual(dag.available_providers(), ["foo_provider"])
+            self.assertEqual(self.dag.available_providers(), ["foo_provider"])
             self.assertEqual(
-                dag.providers_config["foo_provider"].search.api_endpoint,
+                self.dag.providers_config["foo_provider"].search.api_endpoint,
                 "https://foo.bar/search",
             )
         finally:
             os.environ.pop("EODAG_PROVIDERS_CFG_FILE", None)
 
+    def test_core_product_types_config_envvar(self):
+        """product types should be loaded from file defined in env var"""
+        # setup providers config
+        config_path = os.path.join(TEST_RESOURCES_PATH, "file_providers_override.yml")
+        providers_config: list[ProviderConfig] = cached_yaml_load_all(config_path)
+        providers_config[0].products["TEST_PRODUCT_1"] = {"productType": "TP1"}
+        providers_config[0].products["TEST_PRODUCT_2"] = {"productType": "TP2"}
+        with open(
+            os.path.join(self.tmp_home_dir.name, "file_providers_override2.yml"), "w"
+        ) as f:
+            f.write(yaml.dump(providers_config[0]))
+        # set env variables
+        os.environ["EODAG_PROVIDERS_CFG_FILE"] = os.path.join(
+            self.tmp_home_dir.name, "file_providers_override2.yml"
+        )
+        os.environ["EODAG_PRODUCT_TYPES_CFG_FILE"] = os.path.join(
+            TEST_RESOURCES_PATH, "file_product_types_override.yml"
+        )
+        # check product types
+        try:
+            self.dag = EODataAccessGateway()
+            pt = self.dag.list_product_types(fetch_providers=False)
+            self.assertEqual(2, len(pt))
+            self.assertEqual("TEST_PRODUCT_1", pt[0]["ID"])
+            self.assertEqual("TEST_PRODUCT_2", pt[1]["ID"])
+        finally:
+            # remove env variables
+            os.environ.pop("EODAG_PROVIDERS_CFG_FILE", None)
+            os.environ.pop("EODAG_PRODUCT_TYPES_CFG_FILE", None)
+
 
 class TestCoreInvolvingConfDir(unittest.TestCase):
     @classmethod
diff --git a/tests/units/test_download_plugins.py b/tests/units/test_download_plugins.py
index fc74ac0f..662b1b3a 100644
--- a/tests/units/test_download_plugins.py
+++ b/tests/units/test_download_plugins.py
@@ -1729,6 +1729,66 @@ class TestDownloadPluginAws(BaseDownloadPluginTest):
             os.path.join(self.output_dir, self.product.properties["title"]),
         )
 
+    @mock.patch(
+        "eodag.plugins.download.aws.open_s3_zipped_object",
+        autospec=True,
+    )
+    @mock.patch(
+        "eodag.plugins.download.aws.AwsDownload.get_authenticated_objects",
+        autospec=True,
+    )
+    def test_plugins_download_aws_in_zip(
+        self,
+        mock_get_authenticated_objects: mock.Mock,
+        mock_open_s3_zipped_object: mock.Mock,
+    ):
+        """AwsDownload.download() must handle files in zip"""
+
+        def _open_zip(*args, **kwargs):
+            return zipfile.ZipFile(
+                os.path.join(
+                    TEST_RESOURCES_PATH,
+                    "products",
+                    "as_archive",
+                    "S2A_MSIL1C_20180101T105441_N0206_R051_T31TDH_20180101T124911.zip",
+                )
+            )
+
+        mock_open_s3_zipped_object.side_effect = _open_zip
+
+        plugin = self.get_download_plugin(self.product)
+        plugin.s3_resource = mock.Mock()
+        self.product.assets.clear()
+        self.product.assets.update(
+            {
+                "file1": {
+                    "href": (
+                        "http://example.com/path/to/foo.zip!"
+                        "GRANULE/L1C_T31TDH_A013204_20180101T105435/IMG_DATA/T31TDH_20180101T105441_B01.jp2"
+                    )
+                },
+                "file2": {
+                    "href": "http://example.com/path/to/foo.zip!GRANULE/L1C_T31TDH_A013204_20180101T105435/MTD_TL.xml"
+                },
+            }
+        )
+        # no SAFE build and flatten_top_dirs
+        plugin.config.products[self.product.product_type]["build_safe"] = False
+        plugin.config.flatten_top_dirs = True
+
+        path = plugin.download(self.product, output_dir=self.output_dir)
+
+        self.assertEqual(mock_open_s3_zipped_object.call_count, 2)
+        mock_open_s3_zipped_object.assert_called_with(
+            "example", "path/to/foo.zip", plugin.s3_resource.meta.client, partial=False
+        )
+        self.assertTrue(
+            os.path.isfile(
+                os.path.join(path, "IMG_DATA/T31TDH_20180101T105441_B01.jp2")
+            )
+        )
+        self.assertTrue(os.path.isfile(os.path.join(path, "MTD_TL.xml")))
+
     @mock.patch("eodag.plugins.download.aws.flatten_top_directories", autospec=True)
     @mock.patch(
         "eodag.plugins.download.aws.AwsDownload.check_manifest_file_list", autospec=True
diff --git a/tests/units/test_utils_s3.py b/tests/units/test_utils_s3.py
index 3e176f0a..89e90d67 100644
--- a/tests/units/test_utils_s3.py
+++ b/tests/units/test_utils_s3.py
@@ -1,4 +1,5 @@
 import os
+import zipfile
 from unittest import TestCase
 
 import boto3
@@ -11,6 +12,7 @@ from tests.context import (
     MisconfiguredError,
     PluginConfig,
     list_files_in_s3_zipped_object,
+    open_s3_zipped_object,
     update_assets_from_s3,
 )
 
@@ -73,6 +75,14 @@ class TestUtilsS3(TestCase):
             ],
         )
 
+    def test_utils_s3_open_s3_zipped_object(self):
+        """list_files_in_s3_zipped_object must list the files in a zipped object stored in S3"""
+        zip_file = open_s3_zipped_object(
+            "mybucket", "path/to/product.zip", self.s3_client
+        )
+        self.assertIsInstance(zip_file, zipfile.ZipFile)
+        self.assertEqual(len(zip_file.filelist), 6)
+
     def test_utils_s3_update_assets_from_s3_zip(self):
         """update_assets_from_s3 must update the assets of a product from a zipped object stored in S3"""
         prod = EOProduct("dummy", dict(geometry="POINT (0 0)", id="foo"))