Repository: CS-SI/eodag
Pull request #1243 (2024-06-28 15:51:56)

feat(core): SearchResult HTML representation
Implements HTML representation for `SearchResult`, `EOProduct`, `AssetsDict` and `Asset`, in order to enhance rendering in notebooks.

![image](https://github.com/CS-SI/eodag/assets/61419125/34f3dc0c-4be0-43f2-a6b6-f1e3bb6a48ca)


github-actions[bot]: ## Test Results
  2 files   -     2  2 suites   - 2   1m 18s :stopwatch: - 4m 53s
 61 tests  -   487  0 :white_check_mark:  -   545  0 :zzz:  -   3  0 :x: ±0   61 :fire: + 61 
106 runs   - 2 086  0 :white_check_mark:  - 2 090  0 :zzz:  - 102  0 :x: ±0  106 :fire: +106 

For more details on these errors, see [this check](https://github.com/CS-SI/eodag/runs/26817120038).

Results for commit 1a7d0b4c. ± Comparison against base commit e21c5d75.

<details>
  <summary>This pull request <b>removes</b> 525 and <b>adds</b> 38 tests. <i>Note that renamed tests count towards both.</i></summary>

```
eodag.types.__init__ ‑ eodag.types.json_field_definition_to_python
eodag.types.__init__ ‑ eodag.types.json_type_to_python
eodag.types.__init__ ‑ eodag.types.model_fields_to_annotated
eodag.types.__init__ ‑ eodag.types.python_field_definition_to_json
eodag.types.__init__ ‑ eodag.types.python_type_to_json
eodag.types.queryables ‑ eodag.types.queryables.CommonQueryables.get_queryable_from_alias
tests.integration.test_config_ext_plugin.TestExternalPluginConfig ‑ test_update_providers_from_ext_plugin
tests.integration.test_core_config.TestCoreProductTypesConfig ‑ test_core_discover_product_types_auth
tests.integration.test_core_config.TestCoreProvidersConfig ‑ test_core_providers_config_update
tests.integration.test_core_search.TestCoreSearch ‑ test_core_search_errors_buildpost
…
```

```
eodag.types.__init__
eodag.types.bbox
eodag.types.download_args
eodag.types.queryables
eodag.types.search_args
eodag.types.whoosh
eodag.utils.constraints
tests.__init__
tests.context
tests.integration.__init__
…
```
</details>

[test-results]:data:application/gzip;base64,H4sIAOTcfmYC/1XMTQ6DIBCG4asY1l0M2oLtZRoYhoRUpeFnZXr3opaqi1k83yTvzKwbKLJH014aFrNLf5gcVHJ+KpR9cfmk5Sd4xTNmxLLAPrzc+zRY5YbTQCH4UCshT0uRg/jhWNy8B1cfeqtrbkugH0eXChlX0oC+YjltADW/W9UiSegIid96ElJ30Bn2+QKaPhuhAgEAAA==

github-actions[bot]: ![badge](https://raw.githubusercontent.com/CS-SI/eodag/_xml_coverage_reports/data/search_results_repr/./badge.svg)


## Code Coverage (Ubuntu)

<details>


```
Filename                                     Stmts    Miss  Cover    Missing
-----------------------------------------  -------  ------  -------  -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
__init__.py                                     10       0  100.00%
cli.py                                         305      51  83.28%   60, 654-700, 802-853, 857
config.py                                      351      27  92.31%   81-83, 92, 100, 104-106, 183, 195, 460-462, 526-529, 576-577, 586-587, 666, 735-740, 742
crunch.py                                        5       5  0.00%    20-24
api/__init__.py                                  0       0  100.00%
api/core.py                                    757      83  89.04%   90-99, 372, 597, 641-644, 682, 719, 792, 796-801, 827, 923, 1006, 1143, 1233-1245, 1285, 1287, 1315, 1319-1330, 1343-1349, 1439-1442, 1475-1495, 1551, 1568-1571, 1583-1586, 1604-1611, 1939, 1972-1978, 2245, 2249-2252, 2266-2268, 2303
api/search_result.py                            54       6  88.89%   33-35, 75, 84, 91, 105
api/product/__init__.py                          5       0  100.00%
api/product/_assets.py                          52       8  84.62%   28-30, 80, 157, 167, 170-174
api/product/_product.py                        195      27  86.15%   57-64, 68-70, 240-241, 323, 352, 413, 427-430, 443, 467-470, 513-519
api/product/metadata_mapping.py                678      83  87.76%   67-69, 131-133, 234, 266-267, 313-314, 324-336, 338, 349, 355-367, 412-413, 450, 471-474, 497, 505-506, 582-583, 607-608, 614-617, 632-633, 782, 828, 1002-1007, 1138, 1152-1172, 1192, 1197, 1307, 1329, 1343, 1356-1375, 1414, 1466, 1504-1508, 1527
api/product/drivers/__init__.py                  5       0  100.00%
api/product/drivers/base.py                      8       2  75.00%   23, 41
plugins/__init__.py                              0       0  100.00%
plugins/base.py                                 23       3  86.96%   25, 48, 55
plugins/manager.py                             134      15  88.81%   49-51, 105-110, 156, 197, 219, 223, 249, 288-289
plugins/apis/__init__.py                         0       0  100.00%
plugins/apis/base.py                             4       0  100.00%
plugins/apis/ecmwf.py                           99      15  84.85%   46-54, 150-152, 199-200, 226-228
plugins/apis/usgs.py                           175      36  79.43%   60-65, 132, 205, 239, 274-276, 281, 307-308, 313, 343-350, 361-366, 388-394, 396-402, 425
plugins/authentication/__init__.py               6       1  83.33%   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                21       1  95.24%   28
plugins/authentication/keycloak.py              49       6  87.76%   33-35, 132, 156-161
plugins/authentication/oauth.py                 15       8  46.67%   25, 32-34, 38-41
plugins/authentication/openid_connect.py       185      19  89.73%   41-43, 119, 133-158, 166, 320-323, 347
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                 90      17  81.11%   35-37, 79, 107, 109, 131-143, 198-201
plugins/authentication/token_exchange.py        35      19  45.71%   74-80, 92-120
plugins/crunch/__init__.py                       0       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      19  72.06%   28-30, 33, 72-75, 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                     0       0  100.00%
plugins/download/aws.py                        492     165  66.46%   77-83, 272, 285, 352-355, 369-373, 419-421, 425, 459-460, 466-470, 503, 538, 542, 549, 579-587, 591, 629-637, 644-646, 687-761, 779-840, 851-856, 872-885, 914, 929-931, 934, 944-952, 960-973, 983-1002, 1009-1021, 1062, 1088, 1133-1135, 1355
plugins/download/base.py                       260      57  78.08%   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, 688, 710, 718
plugins/download/creodias_s3.py                 17       9  47.06%   44-58
plugins/download/http.py                       531     132  75.14%   85-91, 197-209, 211-212, 247-250, 314-317, 319-320, 327-332, 350-365, 382-384, 396, 444, 451-457, 475, 489, 503, 511-513, 529-534, 545, 563, 605-609, 631, 671, 716, 730-736, 772-836, 854, 887-896, 922-923, 950-955, 961, 964, 980, 997-998, 1025-1026, 1033, 1095-1101, 1156-1157, 1163, 1173, 1207, 1243, 1261-1277
plugins/download/s3rest.py                     118      27  77.12%   55-58, 121, 162, 197, 227-234, 237-239, 243, 256-262, 270-271, 274-278, 301, 322-325
plugins/search/__init__.py                      26       3  88.46%   27-31
plugins/search/base.py                         133      13  90.23%   48-53, 104, 108, 121, 271, 291, 350-351, 371, 380
plugins/search/build_search_result.py          183      24  86.89%   62, 97, 141-142, 148, 159, 295-298, 327, 384-401, 463, 466, 476, 493, 521, 523
plugins/search/cop_marine.py                   200      49  75.50%   43-44, 55, 63-65, 71-72, 88, 90, 93, 128-130, 142-143, 183-192, 196, 199, 203, 221, 251, 255, 270, 274, 278, 282, 286-290, 296-299, 302-316, 333, 356, 359, 365
plugins/search/creodias_s3.py                   55       3  94.55%   56, 74, 108
plugins/search/csw.py                          108      83  23.15%   44-46, 58-59, 63-64, 72-120, 126-139, 147-179, 197-238
plugins/search/data_request_search.py          198      65  67.17%   53, 90-93, 109, 121, 125-126, 137, 142, 147, 154, 167-170, 224-225, 229, 239-245, 250, 276-279, 287-298, 315, 317, 324-325, 327-328, 346-350, 383, 390, 401, 414, 420-432, 437
plugins/search/qssearch.py                     648      69  89.35%   102, 386, 390-396, 404-405, 511-517, 567, 583, 593, 620, 622, 665-668, 742-743, 789, 808, 823, 881, 902, 905-906, 915, 926, 935, 958, 1029-1034, 1038-1039, 1072, 1146, 1195, 1269-1273, 1339-1340, 1361, 1388-1400, 1407, 1439-1441, 1451-1457, 1501, 1516, 1536, 1639-1649
plugins/search/static_stac_search.py            73      12  83.56%   37-38, 95-122, 138, 151
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                                  26       0  100.00%
rest/constants.py                                7       0  100.00%
rest/core.py                                   238      30  87.39%   83-87, 281, 365, 475, 702, 709-757
rest/server.py                                 286      55  80.77%   81-82, 108, 131-133, 246-248, 304-305, 317-333, 425-430, 458, 626-633, 662, 706-707, 730, 803-805, 822-827, 856, 858, 862-863, 867-868
rest/stac.py                                   466     123  73.61%   65-67, 320, 342, 390-393, 417-443, 448-454, 477-479, 502, 537-538, 566, 579, 624-664, 692-708, 787-800, 807, 863-864, 931, 999-1001, 1220, 1230-1242, 1255-1277, 1291-1336, 1498-1499
rest/types/__init__.py                           0       0  100.00%
rest/types/collections_search.py                13      13  0.00%    18-44
rest/types/eodag_search.py                     184       9  95.11%   52-55, 235-239, 292, 295, 363
rest/types/queryables.py                        58       2  96.55%   38, 172
rest/types/stac_search.py                      131      11  91.60%   51-54, 133, 179, 194-196, 204, 208
rest/utils/__init__.py                          95      13  86.32%   51, 108-109, 128-130, 182, 192-206
rest/utils/cql_evaluate.py                      48       5  89.58%   69, 76, 90, 97, 105
rest/utils/rfc3339.py                           23       3  86.96%   49, 61, 63
types/__init__.py                              110      11  90.00%   53, 70, 129-132, 199, 214-218, 245, 258
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
types/whoosh.py                                 15       0  100.00%
utils/__init__.py                              501      39  92.22%   84, 89, 110-112, 193-194, 203-230, 233, 247, 329-333, 409-413, 434-436, 518, 533, 571-572, 968-971, 979-980, 1021-1022, 1204
utils/constraints.py                           119      37  68.91%   94-103, 144, 149, 153, 164, 190-192, 202, 216-232, 241-252
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/repr.py                                   30       8  73.33%   36, 38, 42, 76, 94-101
utils/requests.py                               55      11  80.00%   69, 96, 98, 100, 102, 104, 123, 128-130, 138
utils/rest.py                                   36       1  97.22%   57
utils/stac_reader.py                           111      45  59.46%   56-57, 63-85, 95-97, 101, 143, 159-162, 215-224, 234-264
TOTAL                                         9631    1719  82.15%
```

</details>


### Diff against develop

```
Filename                   Stmts    Miss  Cover
-----------------------  -------  ------  -------
api/search_result.py          +3       0  +0.65%
api/product/_assets.py        +8      +2  -1.74%
api/product/_product.py       +7       0  +0.51%
utils/repr.py                +30      +8  +73.33%
TOTAL                        +48     +10  -0.02%
```


Results for commit: 601ce9e292856c79e140e86924357f3d89e69711


_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/search_results_repr/./badge.svg)


## Code Coverage (Windows)

<details>


```
Filename                                     Stmts    Miss  Cover    Missing
-----------------------------------------  -------  ------  -------  ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
__init__.py                                     10       0  100.00%
cli.py                                         305      51  83.28%   60, 654-700, 802-853, 857
config.py                                      351      28  92.02%   81-83, 92, 100, 104-106, 183, 195, 460-462, 526-529, 576-577, 586-587, 666, 700, 735-740, 742
crunch.py                                        5       5  0.00%    20-24
api/__init__.py                                  0       0  100.00%
api/core.py                                    757      83  89.04%   90-99, 372, 597, 641-644, 682, 719, 792, 796-801, 827, 923, 1006, 1143, 1233-1245, 1285, 1287, 1315, 1319-1330, 1343-1349, 1439-1442, 1475-1495, 1551, 1568-1571, 1583-1586, 1604-1611, 1939, 1972-1978, 2245, 2249-2252, 2266-2268, 2303
api/search_result.py                            54       6  88.89%   33-35, 75, 84, 91, 105
api/product/__init__.py                          5       0  100.00%
api/product/_assets.py                          52       8  84.62%   28-30, 80, 157, 167, 170-174
api/product/_product.py                        195      27  86.15%   57-64, 68-70, 240-241, 323, 352, 413, 427-430, 443, 467-470, 513-519
api/product/metadata_mapping.py                678      83  87.76%   67-69, 131-133, 234, 266-267, 313-314, 324-336, 338, 349, 355-367, 412-413, 450, 471-474, 497, 505-506, 582-583, 607-608, 614-617, 632-633, 782, 828, 1002-1007, 1138, 1152-1172, 1192, 1197, 1307, 1329, 1343, 1356-1375, 1414, 1466, 1504-1508, 1527
api/product/drivers/__init__.py                  5       0  100.00%
api/product/drivers/base.py                      8       2  75.00%   23, 41
plugins/__init__.py                              0       0  100.00%
plugins/base.py                                 23       4  82.61%   25, 48, 55, 68
plugins/manager.py                             134      15  88.81%   49-51, 105-110, 156, 197, 219, 223, 249, 288-289
plugins/apis/__init__.py                         0       0  100.00%
plugins/apis/base.py                             4       0  100.00%
plugins/apis/ecmwf.py                           99      15  84.85%   46-54, 150-152, 199-200, 226-228
plugins/apis/usgs.py                           175      36  79.43%   60-65, 132, 205, 239, 274-276, 281, 307-308, 313, 343-350, 361-366, 388-394, 396-402, 425
plugins/authentication/__init__.py               6       1  83.33%   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                21       1  95.24%   28
plugins/authentication/keycloak.py              49       6  87.76%   33-35, 132, 156-161
plugins/authentication/oauth.py                 15       8  46.67%   25, 32-34, 38-41
plugins/authentication/openid_connect.py       185      19  89.73%   41-43, 119, 133-158, 166, 320-323, 347
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                 90      17  81.11%   35-37, 79, 107, 109, 131-143, 198-201
plugins/authentication/token_exchange.py        35      19  45.71%   74-80, 92-120
plugins/crunch/__init__.py                       0       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      19  72.06%   28-30, 33, 72-75, 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                     0       0  100.00%
plugins/download/aws.py                        492     165  66.46%   77-83, 272, 285, 352-355, 369-373, 419-421, 425, 459-460, 466-470, 503, 538, 542, 549, 579-587, 591, 629-637, 644-646, 687-761, 779-840, 851-856, 872-885, 914, 929-931, 934, 944-952, 960-973, 983-1002, 1009-1021, 1062, 1088, 1133-1135, 1355
plugins/download/base.py                       260      59  77.31%   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, 688, 710, 718
plugins/download/creodias_s3.py                 17       9  47.06%   44-58
plugins/download/http.py                       531     132  75.14%   85-91, 197-209, 211-212, 247-250, 314-317, 319-320, 327-332, 350-365, 382-384, 396, 444, 451-457, 475, 489, 503, 511-513, 529-534, 545, 563, 605-609, 631, 671, 716, 730-736, 772-836, 854, 887-896, 922-923, 950-955, 961, 964, 980, 997-998, 1025-1026, 1033, 1095-1101, 1156-1157, 1163, 1173, 1207, 1243, 1261-1277
plugins/download/s3rest.py                     118      27  77.12%   55-58, 121, 162, 197, 227-234, 237-239, 243, 256-262, 270-271, 274-278, 301, 322-325
plugins/search/__init__.py                      26       3  88.46%   27-31
plugins/search/base.py                         133      18  86.47%   48-53, 104, 108, 121, 271, 291, 350-351, 371, 374-382, 384
plugins/search/build_search_result.py          183      31  83.06%   62, 97, 141-142, 148, 159, 295-298, 327, 384-401, 463, 466, 476, 493, 513-528
plugins/search/cop_marine.py                   200      49  75.50%   43-44, 55, 63-65, 71-72, 88, 90, 93, 128-130, 142-143, 183-192, 196, 199, 203, 221, 251, 255, 270, 274, 278, 282, 286-290, 296-299, 302-316, 333, 356, 359, 365
plugins/search/creodias_s3.py                   55       3  94.55%   56, 74, 108
plugins/search/csw.py                          108      83  23.15%   44-46, 58-59, 63-64, 72-120, 126-139, 147-179, 197-238
plugins/search/data_request_search.py          198      65  67.17%   53, 90-93, 109, 121, 125-126, 137, 142, 147, 154, 167-170, 224-225, 229, 239-245, 250, 276-279, 287-298, 315, 317, 324-325, 327-328, 346-350, 383, 390, 401, 414, 420-432, 437
plugins/search/qssearch.py                     648      97  85.03%   102, 386, 390-396, 404-405, 511-517, 567, 570, 583, 593, 612-627, 665-668, 742-743, 789, 808, 823, 881, 902, 905-906, 915, 926, 935, 958, 1029-1034, 1038-1039, 1072, 1146, 1195, 1269-1273, 1339-1340, 1361, 1388-1400, 1407, 1439-1441, 1451-1457, 1501, 1516, 1536, 1598-1669
plugins/search/static_stac_search.py            73      12  83.56%   37-38, 95-122, 138, 151
rest/__init__.py                                 4       2  50.00%   21-22
rest/cache.py                                   33      22  33.33%   35-37, 44-70
rest/config.py                                  26       4  84.62%   34-36, 68
rest/constants.py                                7       0  100.00%
rest/core.py                                   238     153  35.71%   83-87, 167-248, 272-326, 339-375, 412-444, 464-480, 503-514, 523-559, 578, 624-663, 702, 709-757
rest/server.py                                 286     286  0.00%    18-879
rest/stac.py                                   466     394  15.45%   65-67, 128-134, 142-165, 196-201, 233, 253-376, 386-455, 473-512, 528-581, 591-614, 624-664, 692-708, 718, 734-737, 756-826, 844-880, 909-934, 942-958, 966-971, 981-1008, 1018-1020, 1028-1030, 1043-1045, 1059-1076, 1086-1107, 1117-1139, 1147-1164, 1187-1210, 1220, 1230-1242, 1255-1277, 1291-1336, 1344-1361, 1371-1529
rest/types/__init__.py                           0       0  100.00%
rest/types/collections_search.py                13      13  0.00%    18-44
rest/types/eodag_search.py                     184      19  89.67%   52-55, 125, 235-239, 272-274, 292, 295, 301, 305, 363, 375-378
rest/types/queryables.py                        58      14  75.86%   38, 51-52, 58-59, 65-66, 95-100, 109-110, 172
rest/types/stac_search.py                      131      22  83.21%   51-54, 131-133, 157-158, 163-164, 179, 194-196, 204, 208, 255-260
rest/utils/__init__.py                          95      31  67.37%   51, 79-85, 105, 108-109, 128-130, 143, 150, 175-183, 190-211
rest/utils/cql_evaluate.py                      48       5  89.58%   69, 76, 90, 97, 105
rest/utils/rfc3339.py                           23       6  73.91%   41, 44-45, 49, 61, 63
types/__init__.py                              110      36  67.27%   53, 66-70, 81-93, 120-122, 129-132, 172, 199, 209-221, 226, 245, 250, 258, 268
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
types/whoosh.py                                 15       0  100.00%
utils/__init__.py                              501      41  91.82%   84, 89, 110-112, 193-194, 203-230, 233, 247, 329-333, 409-413, 434-436, 518, 533, 571-572, 968-971, 979-980, 1021-1022, 1055, 1204, 1380
utils/constraints.py                           119      37  68.91%   94-103, 144, 149, 153, 164, 190-192, 202, 216-232, 241-252
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/repr.py                                   30       8  73.33%   36, 38, 42, 76, 94-101
utils/requests.py                               55      11  80.00%   69, 96, 98, 100, 102, 104, 123, 128-130, 138
utils/rest.py                                   36       1  97.22%   57
utils/stac_reader.py                           111      45  59.46%   56-57, 63-85, 95-97, 101, 143, 159-162, 215-224, 234-264
TOTAL                                         9631    2513  73.91%
```

</details>


### Diff against develop

```
Filename                   Stmts    Miss  Cover
-----------------------  -------  ------  -------
api/search_result.py          +3       0  +0.65%
api/product/_assets.py        +8      +2  -1.74%
api/product/_product.py       +7       0  +0.51%
utils/repr.py                +30      +8  +73.33%
TOTAL                        +48     +10  +0.03%
```


Results for commit: 601ce9e292856c79e140e86924357f3d89e69711


_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 -->

Patch:
diff --git a/eodag/api/product/_assets.py b/eodag/api/product/_assets.py
index cf03bea7..f47c7019 100644
--- a/eodag/api/product/_assets.py
+++ b/eodag/api/product/_assets.py
@@ -22,6 +22,7 @@ from collections import UserDict
 from typing import TYPE_CHECKING, Any, Dict, List, Optional
 
 from eodag.utils.exceptions import NotAvailableError
+from eodag.utils.repr import dict_to_html_table
 
 if TYPE_CHECKING:
     from eodag.api.product import EOProduct
@@ -84,6 +85,43 @@ class AssetsDict(UserDict):
         else:
             return [a for a in self.values() if "href" in a]
 
+    def _repr_html_(self, embeded=False):
+        thead = (
+            f"""<thead><tr><td style='text-align: left; color: grey;'>
+                {type(self).__name__}&ensp;({len(self)})
+                </td></tr></thead>
+            """
+            if not embeded
+            else ""
+        )
+        tr_style = "style='background-color: transparent;'" if embeded else ""
+        return (
+            f"<table>{thead}"
+            + "".join(
+                [
+                    f"""<tr {tr_style}><td style='text-align: left;'>
+                <details><summary style='color: grey;'>
+                    <span style='color: black'>'{k}'</span>:&ensp;
+                    {{
+                        {"'roles': '<span style='color: black'>"+str(v['roles'])+"</span>',&ensp;"
+                            if v.get("roles") else ""}
+                        {"'type': '"+str(v['type'])+"',&ensp;"
+                            if v.get("type") else ""}
+                        {"'title': '<span style='color: black'>"+str(v['title'])+"</span>',&ensp;"
+                            if v.get("title") else ""}
+                        ...
+                    }}
+                </summary>
+                    {dict_to_html_table(v, depth=1)}
+                </details>
+                </td></tr>
+                """
+                    for k, v in self.items()
+                ]
+            )
+            + "</table>"
+        )
+
 
 class Asset(UserDict):
     """A UserDict object containg one of the assets of a
@@ -127,3 +165,15 @@ class Asset(UserDict):
         :rtype: str
         """
         return self.product.download(asset=self.key, **kwargs)
+
+    def _repr_html_(self):
+        thead = f"""<thead><tr><td style='text-align: left; color: grey;'>
+            {type(self).__name__}&ensp;-&ensp;{self.key}
+            </td></tr></thead>
+        """
+        return f"""<table>{thead}
+                <tr><td style='text-align: left;'>
+                    {dict_to_html_table(self)}
+                </details>
+                </td></tr>
+            </table>"""
diff --git a/eodag/api/product/_product.py b/eodag/api/product/_product.py
index 0d8f1e18..9a3f644f 100644
--- a/eodag/api/product/_product.py
+++ b/eodag/api/product/_product.py
@@ -51,6 +51,7 @@ from eodag.utils import (
     get_geometry_from_various,
 )
 from eodag.utils.exceptions import DownloadError, MisconfiguredError
+from eodag.utils.repr import dict_to_html_table
 
 if TYPE_CHECKING:
     from shapely.geometry.base import BaseGeometry
@@ -517,3 +518,45 @@ class EOProduct:
             )
             pass
         return NoDriver()
+
+    def _repr_html_(self):
+        thumbnail = self.properties.get("thumbnail", None)
+        thumbnail_html = (
+            f"<img src='{thumbnail}' width=100 alt='thumbnail'/>"
+            if thumbnail and not thumbnail.startswith("s3")
+            else ""
+        )
+        geom_style = "style='color: grey; text-align: center; min-width:100px; vertical-align: top;'"
+        thumbnail_style = (
+            "style='padding-top: 1.5em; min-width:100px; vertical-align: top;'"
+        )
+
+        return f"""<table>
+                <thead><tr style='background-color: transparent;'><td style='text-align: left; color: grey;'>
+                {type(self).__name__}
+                </td></tr></thead>
+
+                <tr style='background-color: transparent;'>
+                    <td style='text-align: left; vertical-align: top;'>
+                        {dict_to_html_table({
+                            "provider": self.provider,
+                            "product_type": self.product_type,
+                            "properties[&quot;id&quot;]": self.properties.get('id', None),
+                            "properties[&quot;startTimeFromAscendingNode&quot;]": self.properties.get(
+                                'startTimeFromAscendingNode', None
+                            ),
+                            "properties[&quot;completionTimeFromAscendingNode&quot;]": self.properties.get(
+                                'completionTimeFromAscendingNode', None
+                            ),
+                        }, brackets=False)}
+                        <details><summary style='color: grey; margin-top: 10px;'>properties:&ensp;({
+                            len(self.properties)
+                        })</summary>{dict_to_html_table(self.properties, depth=1)}</details>
+                        <details><summary style='color: grey; margin-top: 10px;'>assets:&ensp;({
+                            len(self.assets)
+                        })</summary>{self.assets._repr_html_(embeded=True)}</details>
+                    </td>
+                    <td {geom_style} title='geometry'>geometry<br />{self.geometry._repr_svg_()}</td>
+                    <td {thumbnail_style} title='properties[&quot;thumbnail&quot;]'>{thumbnail_html}</td>
+                </tr>
+            </table>"""
diff --git a/eodag/api/search_result.py b/eodag/api/search_result.py
index 96199227..3ac0d7b5 100644
--- a/eodag/api/search_result.py
+++ b/eodag/api/search_result.py
@@ -174,6 +174,33 @@ class SearchResult(UserList):
         """
         return self.as_geojson_object()
 
+    def _repr_html_(self):
+        total_count = f"/{self.number_matched}" if self.number_matched else ""
+        return (
+            f"""<table>
+                <thead><tr><td style='text-align: left; color: grey;'>
+                 {type(self).__name__}&ensp;({len(self)}{total_count})
+                </td></tr></thead>
+            """
+            + "".join(
+                [
+                    f"""<tr><td style='text-align: left;'>
+                <details><summary style='color: grey; font-family: monospace;'>
+                    {i}&ensp;
+                    {type(p).__name__}(id=<span style='color: black;'>{
+                        p.properties['id']
+                    }</span>, provider={p.provider})
+                </summary>
+                {p._repr_html_()}
+                </details>
+                </td></tr>
+                """
+                    for i, p in enumerate(self)
+                ]
+            )
+            + "</table>"
+        )
+
 
 class RawSearchResult(UserList):
     """An object representing a collection of raw/unparsed search results obtained from a provider.
diff --git a/eodag/utils/repr.py b/eodag/utils/repr.py
new file mode 100644
index 00000000..c2913257
--- /dev/null
+++ b/eodag/utils/repr.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+# Copyright 2024, CS GROUP - France, https://www.csgroup.eu/
+#
+# This file is part of EODAG project
+#     https://www.github.com/CS-SI/EODAG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from __future__ import annotations
+
+import collections.abc
+from typing import Any, Optional
+from urllib.parse import urlparse
+
+
+def str_as_href(link: str) -> str:
+    """URL to html link"""
+    if urlparse(link).scheme in ("file", "http", "https", "s3"):
+        return f"<a href='{link}' target='_blank'>{link}</a>"
+    else:
+        return link
+
+
+def html_table(input: Any, depth: Optional[int] = None) -> str:
+    """Transform input to HTML table"""
+    if isinstance(input, collections.abc.Mapping):
+        return dict_to_html_table(input, depth=depth)
+    elif isinstance(input, collections.abc.Sequence) and not isinstance(input, str):
+        return list_to_html_table(input, depth=depth)
+    elif isinstance(input, str):
+        return f"'{str_as_href(input)}'"
+    else:
+        return str_as_href(str(input))
+
+
+def dict_to_html_table(
+    input_dict: collections.abc.Mapping,
+    depth: Optional[int] = None,
+    brackets: bool = True,
+) -> str:
+    """Transform input dict to HTML table"""
+    opening_bracket = "<span style='color: grey;'>{</span>" if brackets else ""
+    closing_bracket = "<span style='color: grey;'>}</span>" if brackets else ""
+    indent = "10px" if brackets else "0"
+
+    if depth is not None:
+        depth -= 1
+
+    if depth is None or depth >= 0:
+        return (
+            f"{opening_bracket}<table style='margin: 0;'>"
+            + "".join(
+                [
+                    f"""<tr style='background-color: transparent;'>
+                    <td style='padding: 5px 0 0 {indent}; text-align: left; color: grey; vertical-align:top;'>{k}:</td>
+                    <td style='padding: 5px 0 0 10px; text-align: left;'>{
+                        html_table(v, depth=depth)
+                    },</td>
+                </tr>
+                """
+                    for k, v in input_dict.items()
+                ]
+            )
+            + f"</table>{closing_bracket}"
+        )
+    else:
+        return (
+            f"{opening_bracket}"
+            + ", ".join(
+                [
+                    f"""<span style='text-align: left;'>
+                    '{k}': {html_table(v, depth=depth)}
+                </span>"""
+                    for k, v in input_dict.items()
+                ]
+            )
+            + f"{closing_bracket}"
+        )
+
+
+def list_to_html_table(
+    input_list: collections.abc.Sequence, depth: Optional[int] = None
+) -> str:
+    """Transform input list to HTML table"""
+    if depth is not None:
+        depth -= 1
+    separator = (
+        ",<br />"
+        if any(isinstance(v, collections.abc.Mapping) for v in input_list)
+        else ", "
+    )
+    return (
+        "<span style='color: grey;'>[</span>"
+        + separator.join(
+            [
+                f"""<span style='text-align: left;'>{
+                html_table(v, depth=depth)
+            }</span>
+            """
+                for v in input_list
+            ]
+        )
+        + "<span style='color: grey;'>]</span>"
+    )


Test patch:
diff --git a/tests/units/test_eoproduct.py b/tests/units/test_eoproduct.py
index 32af9619..b8ea3d61 100644
--- a/tests/units/test_eoproduct.py
+++ b/tests/units/test_eoproduct.py
@@ -26,6 +26,7 @@ import zipfile
 
 import geojson
 import requests
+from lxml import html
 from shapely import geometry
 
 from tests import EODagTestCase
@@ -539,3 +540,18 @@ class TestEOProduct(EODagTestCase):
             ]
             for needed_log in needed_logs:
                 self.assertIn(needed_log, str(mock_debug.call_args_list))
+
+    def test_eoproduct_repr_html(self):
+        """eoproduct html repr must be correctly formatted"""
+        product = self._dummy_product()
+        product_repr = html.fromstring(product._repr_html_())
+        self.assertIn("EOProduct", product_repr.xpath("//thead/tr/td")[0].text)
+
+        # assets dict
+        product.assets.update({"foo": {"href": "foo.href"}})
+        assets_dict_repr = html.fromstring(product.assets._repr_html_())
+        self.assertIn("AssetsDict", assets_dict_repr.xpath("//thead/tr/td")[0].text)
+
+        # asset
+        asset_repr = html.fromstring(product.assets._repr_html_())
+        self.assertIn("Asset", asset_repr.xpath("//thead/tr/td")[0].text)
diff --git a/tests/units/test_search_result.py b/tests/units/test_search_result.py
index e85e3233..6b563432 100644
--- a/tests/units/test_search_result.py
+++ b/tests/units/test_search_result.py
@@ -20,6 +20,7 @@ import unittest
 from collections import UserList
 
 import geojson
+from lxml import html
 from shapely.geometry.collection import GeometryCollection
 
 from tests.context import EOProduct, SearchResult
@@ -82,3 +83,8 @@ class TestSearchResult(unittest.TestCase):
             geojson.loads(geojson.dumps(self.search_result))
         )
         self.assertEqual(len(same_search_result), len(self.search_result))
+
+    def test_search_result_repr_html(self):
+        """SearchResult html repr must be correctly formatted"""
+        sr_repr = html.fromstring(self.search_result._repr_html_())
+        self.assertIn("SearchResult", sr_repr.xpath("//thead/tr/td")[0].text)