Repository: huggingface/huggingface_hub
Pull request #878 (2022-05-18 20:56:25)

Refine 404 errors
This PR adds finer error management to the Hugging Face Hub error messages.

Here's the difference between current `main` and this PR on the following errors:

<details>

<summary>An example for <code>hf_api</code>: <code>model_info</code></summary

<br>

Using the `model_info` method:
```py
In [1]: from huggingface_hub import model_info

In [2]: model_info('random_model')
```

Before:
```
---------------------------------------------------------------------------
HTTPError                                 Traceback (most recent call last)
Input In [2], in <cell line: 1>()
----> 1 model_info('random_model')

[...]

File ~/Workspaces/Python/huggingface_hub/.env/lib/python3.10/site-packages/requests/models.py:960, in Response.raise_for_status(self)
    957     http_error_msg = u'%s Server Error: %s for url: %s' % (self.status_code, reason, self.url)
    959 if http_error_msg:
--> 960     raise HTTPError(http_error_msg, response=self)

HTTPError: 404 Client Error: Not Found for url: https://huggingface.co/api/models/random_model
```

After:
```
---------------------------------------------------------------------------
RepositoryNotFoundError                   Traceback (most recent call last)
Input In [2], in <cell line: 1>()
----> 1 model_info('random_model')

[...]

File ~/Workspaces/Python/huggingface_hub/src/huggingface_hub/utils/_errors.py:29, in _raise_for_status(request)
     27 error_code = request.headers["X-Error-Code"]
     28 if error_code == "RepoNotFound":
---> 29     raise RepositoryNotFoundError(
     30         f"404 Client Error: Repository Not Found for url: {request.url}"
     31     )
     32 elif error_code == "EntryNotFound":
     33     raise EntryNotFoundError(
     34         f"404 Client Error: Entry Not Found for url: {request.url}"
     35     )

RepositoryNotFoundError: 404 Client Error: Repository Not Found for url: https://huggingface.co/api/models/random_model
```

</details>

<details>

<summary>An example for <code>cached_download</code></summary>

<br>

`cached_download` is an interesting test-case as it can raise the three different errors. Right now, on `main`, using either of the three following errors results in the same error message:

```py
In [1]: cached_download(hf_hub_url('<bad_repo_id>', 'config.json'))
In [1]: cached_download(hf_hub_url('lysandre/sharded-repo, '<bad_file>'))
In [1]: cached_download(hf_hub_url('lysandre/sharded-repo', 'config.json', revision='<bad_revision>'))
```

```
In [1]: cached_download('https://huggingface.co/random_org/random_repo', 'file.json')

[...]

File ~/Workspaces/Python/huggingface_hub/.env/lib/python3.10/site-packages/requests/models.py:960, in Response.raise_for_status(self)
    957     http_error_msg = u'%s Server Error: %s for url: %s' % (self.status_code, reason, self.url)
    959 if http_error_msg:
--> 960     raise HTTPError(http_error_msg, response=self)

HTTPError: 404 Client Error: Not Found for url: <URL>
```

This PR will now provide tailored error messages:

*Wrong repo ID*

```py
In [1]: cached_download(hf_hub_url('<bad_repo_id>', 'config.json'))
```
```

[...]

File ~/Workspaces/Python/huggingface_hub/src/huggingface_hub/utils/_errors.py:29, in _raise_for_status(request)
     27 error_code = request.headers["X-Error-Code"]
     28 if error_code == "RepoNotFound":
---> 29     raise RepositoryNotFoundError(
     30         f"404 Client Error: Repository Not Found for url: {request.url}"
     31     )
     32 elif error_code == "EntryNotFound":
     33     raise EntryNotFoundError(
     34         f"404 Client Error: Entry Not Found for url: {request.url}"
     35     )

RepositoryNotFoundError: 404 Client Error: Repository Not Found for url: https://huggingface.co/%3Cbad_repo%3E/resolve/main/config.json
```

*Wrong file*

```py
In [1]: cached_download(hf_hub_url('lysandre/sharded-repo, '<bad_file>'))
```

```

[...]

File ~/Workspaces/Python/huggingface_hub/src/huggingface_hub/utils/_errors.py:33, in _raise_for_status(request)
     29     raise RepositoryNotFoundError(
     30         f"404 Client Error: Repository Not Found for url: {request.url}"
     31     )
     32 elif error_code == "EntryNotFound":
---> 33     raise EntryNotFoundError(
     34         f"404 Client Error: Entry Not Found for url: {request.url}"
     35     )
     36 elif error_code == "RevisionNotFound":
     37     raise RevisionNotFoundError(
     38         f"404 Client Error: Revision Not Found for url: {request.url}"
     39     )

EntryNotFoundError: 404 Client Error: Entry Not Found for url: https://huggingface.co/lysandre/sharded-repo/resolve/main/%3Cbad%3E
```

*Wrong revision*
```py
In [1]: cached_download(hf_hub_url('lysandre/sharded-repo', 'config.json', revision='<bad_revision>'))
```

```
In [2]: cached_download(hf_hub_url('lysandre/sharded-repo', 'config.json', revision='bad'))

[...]

File ~/Workspaces/Python/huggingface_hub/src/huggingface_hub/utils/_errors.py:37, in _raise_for_status(request)
     33         raise EntryNotFoundError(
     34             f"404 Client Error: Entry Not Found for url: {request.url}"
     35         )
     36     elif error_code == "RevisionNotFound":
---> 37         raise RevisionNotFoundError(
     38             f"404 Client Error: Revision Not Found for url: {request.url}"
     39         )
     41 request.raise_for_status()

RevisionNotFoundError: 404 Client Error: Revision Not Found for url: https://huggingface.co/lysandre/sharded-repo/resolve/bad/config.json
```

</details>

HuggingFaceDocBuilderDev: The docs for this PR live [here](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_878). All of your documentation changes will be reflected on that endpoint.
LysandreJik: This PR can be independant of the `cache-file-layout` branch, so I'll switch the target to `main` and rebase `cache-file-layout` on this branch to benefit from the fix.

Patch:
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 00000000..69cb7601
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,1 @@
+comment: false
diff --git a/docs/source/_toctree.yml b/docs/source/_toctree.yml
index a712d2b0..7f2df6af 100644
--- a/docs/source/_toctree.yml
+++ b/docs/source/_toctree.yml
@@ -27,6 +27,6 @@
       title: Mixins & serialization methods
     - local: package_reference/inference_api
       title: Inference API
-    - local: package_reference/logging
-      title: Logging
+    - local: package_reference/utilities
+      title: Utilities
   title: "Reference"
\ No newline at end of file
diff --git a/docs/source/package_reference/logging.mdx b/docs/source/package_reference/utilities.mdx
similarity index 82%
rename from docs/source/package_reference/logging.mdx
rename to docs/source/package_reference/utilities.mdx
index c2460a6d..96415fac 100644
--- a/docs/source/package_reference/logging.mdx
+++ b/docs/source/package_reference/utilities.mdx
@@ -1,4 +1,6 @@
-# Controlling the logging of `huggingface_hub`
+# Utilities
+
+## Controlling the logging of `huggingface_hub`
 
 The `huggingface_hub` package exposes a `logging` utility to control the logging level of the package itself.
 You can import it as such:
@@ -40,9 +42,19 @@ under the hood.
 [[autodoc]] logging.disable_propagation
 [[autodoc]] logging.enable_propagation
 
-# Repo-specific helper methods
+### Repo-specific helper methods
 
 The methods exposed below are relevant when modifying modules from the `huggingface_hub` library itself.
 Using these shouldn't be necessary if you use `huggingface_hub` and you don't modify them.
 
 [[autodoc]] logging.get_logger
+
+## Custom errors
+
+See below for all custom errors thrown by different methods across the package.
+
+[[autodoc]] huggingface_hub.utils.RepositoryNotFoundError
+
+[[autodoc]] huggingface_hub.utils.RevisionNotFoundError
+
+[[autodoc]] huggingface_hub.utils.EntryNotFoundError
diff --git a/setup.py b/setup.py
index 0ba23a6b..9a2eae17 100644
--- a/setup.py
+++ b/setup.py
@@ -37,6 +37,7 @@ extras["tensorflow"] = ["tensorflow", "pydot", "graphviz"]
 
 extras["testing"] = [
     "pytest",
+    "pytest-cov",
     "datasets",
     "soundfile",
 ]
diff --git a/src/huggingface_hub/commands/user.py b/src/huggingface_hub/commands/user.py
index 41cecb77..ac69abf1 100644
--- a/src/huggingface_hub/commands/user.py
+++ b/src/huggingface_hub/commands/user.py
@@ -319,7 +319,7 @@ def notebook_login():
     except ImportError:
         raise ImportError(
             "The `notebook_login` function can only be used in a notebook (Jupyter or"
-            " Colab) and you need the `ipywdidgets` module: `pip install ipywidgets`."
+            " Colab) and you need the `ipywidgets` module: `pip install ipywidgets`."
         )
 
     box_layout = widgets.Layout(
diff --git a/src/huggingface_hub/file_download.py b/src/huggingface_hub/file_download.py
index c3e602f8..79bc592d 100644
--- a/src/huggingface_hub/file_download.py
+++ b/src/huggingface_hub/file_download.py
@@ -35,6 +35,7 @@ from .constants import (
 from .hf_api import HfFolder
 from .utils import logging
 from .utils._deprecation import _deprecate_positional_args
+from .utils._errors import _raise_for_status
 
 
 logger = logging.get_logger(__name__)
@@ -436,7 +437,7 @@ def http_get(
     max_retries=0,
 ):
     """
-    Donwload remote file. Do not gobble up errors.
+    Donwload a remote file. Do not gobble up errors, and will return errors tailored to the Hugging Face Hub.
     """
     headers = copy.deepcopy(headers)
     if resume_size > 0:
@@ -450,7 +451,7 @@ def http_get(
         timeout=timeout,
         max_retries=max_retries,
     )
-    r.raise_for_status()
+    _raise_for_status(r)
     content_length = r.headers.get("Content-Length")
     total = resume_size + int(content_length) if content_length is not None else None
     progress = tqdm(
@@ -493,6 +494,8 @@ def cached_download(
     cache. If it's not there, download it. Then return the path to the cached
     file.
 
+    Will raise errors tailored to the Hugging Face Hub.
+
     Args:
         url (`str`):
             The path to the file to be downloaded.
@@ -545,6 +548,13 @@ def cached_download(
           if ETag cannot be determined.
         - [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)
           if some parameter value is invalid
+        - [`~huggingface_hub.utils.RepositoryNotFoundError`]
+          If the repository to download from cannot be found. This may be because it doesn't exist,
+          or because it is set to `private` and you do not have access.
+        - [`~huggingface_hub.utils.RevisionNotFoundError`]
+          If the revision to download from cannot be found.
+        - [`~huggingface_hub.utils.EntryNotFoundError`]
+          If the file to download cannot be found.
 
     </Tip>
     """
@@ -592,7 +602,7 @@ def cached_download(
                 proxies=proxies,
                 timeout=etag_timeout,
             )
-            r.raise_for_status()
+            _raise_for_status(r)
             etag = r.headers.get("X-Linked-Etag") or r.headers.get("ETag")
             # We favor a custom header indicating the etag of the linked resource, and
             # we fallback to the regular etag header.
@@ -898,6 +908,13 @@ def hf_hub_download(
           if ETag cannot be determined.
         - [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)
           if some parameter value is invalid
+        - [`~huggingface_hub.utils.RepositoryNotFoundError`]
+          If the repository to download from cannot be found. This may be because it doesn't exist,
+          or because it is set to `private` and you do not have access.
+        - [`~huggingface_hub.utils.RevisionNotFoundError`]
+          If the revision to download from cannot be found.
+        - [`~huggingface_hub.utils.EntryNotFoundError`]
+          If the file to download cannot be found.
 
     </Tip>
     """
@@ -1003,7 +1020,7 @@ def hf_hub_download(
                 proxies=proxies,
                 timeout=etag_timeout,
             )
-            r.raise_for_status()
+            _raise_for_status(r)
             commit_hash = r.headers[HUGGINGFACE_HEADER_X_REPO_COMMIT]
             if commit_hash is None:
                 raise OSError(
@@ -1051,8 +1068,9 @@ def hf_hub_download(
                 "We have no connection or you passed local_files_only, so"
                 " force_download is not an accepted option."
             )
-        commit_hash = revision
-        if not REGEX_COMMIT_HASH.match(revision):
+        if REGEX_COMMIT_HASH.match(revision):
+            commit_hash = revision
+        else:
             ref_path = os.path.join(storage_folder, "refs", revision)
             with open(ref_path) as f:
                 commit_hash = f.read()
diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py
index 626a23cc..3e034f46 100644
--- a/src/huggingface_hub/hf_api.py
+++ b/src/huggingface_hub/hf_api.py
@@ -33,6 +33,7 @@ from .constants import (
 )
 from .utils import logging
 from .utils._deprecation import _deprecate_positional_args
+from .utils._errors import _raise_for_status
 from .utils._fixes import JSONDecodeError
 from .utils.endpoint_helpers import (
     AttributeDictionary,
@@ -1180,6 +1181,18 @@ class HfApi:
 
         Returns:
             [`huggingface_hub.hf_api.ModelInfo`]: The model repository information.
+
+        <Tip>
+
+        Raises the following errors:
+
+            - [`~huggingface_hub.utils.RepositoryNotFoundError`]
+              If the repository to download from cannot be found. This may be because it doesn't exist,
+              or because it is set to `private` and you do not have access.
+            - [`~huggingface_hub.utils.RevisionNotFoundError`]
+              If the revision to download from cannot be found.
+
+        </Tip>
         """
         if token is None:
             token = HfFolder.get_token()
@@ -1194,7 +1207,7 @@ class HfApi:
         r = requests.get(
             path, headers=headers, timeout=timeout, params=status_query_param
         )
-        r.raise_for_status()
+        _raise_for_status(r)
         d = r.json()
         return ModelInfo(**d)
 
@@ -1226,6 +1239,18 @@ class HfApi:
 
         Returns:
             [`DatasetInfo`]: The dataset repository information.
+
+        <Tip>
+
+        Raises the following errors:
+
+            - [`~huggingface_hub.utils.RepositoryNotFoundError`]
+              If the repository to download from cannot be found. This may be because it doesn't exist,
+              or because it is set to `private` and you do not have access.
+            - [`~huggingface_hub.utils.RevisionNotFoundError`]
+              If the revision to download from cannot be found.
+
+        </Tip>
         """
         if token is None:
             token = HfFolder.get_token()
@@ -1237,7 +1262,7 @@ class HfApi:
         )
         headers = {"authorization": f"Bearer {token}"} if token is not None else None
         r = requests.get(path, headers=headers, timeout=timeout)
-        r.raise_for_status()
+        _raise_for_status(r)
         d = r.json()
         return DatasetInfo(**d)
 
@@ -1269,6 +1294,18 @@ class HfApi:
 
         Returns:
             [`SpaceInfo`]: The space repository information.
+
+        <Tip>
+
+        Raises the following errors:
+
+            - [`~huggingface_hub.utils.RepositoryNotFoundError`]
+              If the repository to download from cannot be found. This may be because it doesn't exist,
+              or because it is set to `private` and you do not have access.
+            - [`~huggingface_hub.utils.RevisionNotFoundError`]
+              If the revision to download from cannot be found.
+
+        </Tip>
         """
         if token is None:
             token = HfFolder.get_token()
@@ -1280,7 +1317,7 @@ class HfApi:
         )
         headers = {"authorization": f"Bearer {token}"} if token is not None else None
         r = requests.get(path, headers=headers, timeout=timeout)
-        r.raise_for_status()
+        _raise_for_status(r)
         d = r.json()
         return SpaceInfo(**d)
 
@@ -1312,6 +1349,18 @@ class HfApi:
         Returns:
             `Union[SpaceInfo, DatasetInfo, ModelInfo]`: The repository
             information.
+
+        <Tip>
+
+        Raises the following errors:
+
+            - [`~huggingface_hub.utils.RepositoryNotFoundError`]
+              If the repository to download from cannot be found. This may be because it doesn't exist,
+              or because it is set to `private` and you do not have access.
+            - [`~huggingface_hub.utils.RevisionNotFoundError`]
+              If the revision to download from cannot be found.
+
+        </Tip>
         """
         if repo_type is None or repo_type == "model":
             return self.model_info(
@@ -1528,6 +1577,16 @@ class HfApi:
             repo_type (`str`, *optional*):
                 Set to `"dataset"` or `"space"` if uploading to a dataset or
                 space, `None` or `"model"` if uploading to a model.
+
+        <Tip>
+
+        Raises the following errors:
+
+            - [`~huggingface_hub.utils.RepositoryNotFoundError`]
+              If the repository to download from cannot be found. This may be because it doesn't exist,
+              or because it is set to `private` and you do not have access.
+
+        </Tip>
         """
         name, organization = _validate_repo_id_deprecation(repo_id, name, organization)
 
@@ -1582,12 +1641,14 @@ class HfApi:
             json=json,
         )
         try:
-            r.raise_for_status()
+            _raise_for_status(r)
         except requests.exceptions.RequestException as e:
             try:
                 message = e.response.json()["error"]
             except JSONDecodeError:
                 message = e.response.text
+            except AttributeError:
+                message = e.args[0]
             raise type(e)(message) from e
 
     @_deprecate_positional_args
@@ -1625,6 +1686,16 @@ class HfApi:
 
         Returns:
             The HTTP response in json.
+
+        <Tip>
+
+        Raises the following errors:
+
+            - [`~huggingface_hub.utils.RepositoryNotFoundError`]
+              If the repository to download from cannot be found. This may be because it doesn't exist,
+              or because it is set to `private` and you do not have access.
+
+        </Tip>
         """
         if repo_type not in REPO_TYPES:
             raise ValueError("Invalid repo type")
@@ -1653,7 +1724,7 @@ class HfApi:
             headers={"authorization": f"Bearer {token}"},
             json=json,
         )
-        r.raise_for_status()
+        _raise_for_status(r)
         return r.json()
 
     @_deprecate_positional_args
@@ -1686,6 +1757,15 @@ class HfApi:
             token (`str`, *optional*):
                 An authentication token (See https://huggingface.co/settings/token)
 
+        <Tip>
+
+        Raises the following errors:
+
+            - [`~huggingface_hub.utils.RepositoryNotFoundError`]
+              If the repository to download from cannot be found. This may be because it doesn't exist,
+              or because it is set to `private` and you do not have access.
+
+        </Tip>
         """
 
         token, name = self._validate_or_retrieve_token(token)
@@ -1711,7 +1791,7 @@ class HfApi:
             json=json,
         )
         try:
-            r.raise_for_status()
+            _raise_for_status(r)
         except HTTPError as e:
             if r.text:
                 raise HTTPError(
@@ -1780,6 +1860,11 @@ class HfApi:
               if the HuggingFace API returned an error
             - [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)
               if some parameter value is invalid
+            - [`~huggingface_hub.utils.RepositoryNotFoundError`]
+              If the repository to download from cannot be found. This may be because it doesn't exist,
+              or because it is set to `private` and you do not have access.
+            - [`~huggingface_hub.utils.RevisionNotFoundError`]
+              If the revision to download from cannot be found.
 
         </Tip>
 
@@ -1856,7 +1941,7 @@ class HfApi:
             r = requests.post(path, headers=headers, data=path_or_fileobj)
 
         try:
-            r.raise_for_status()
+            _raise_for_status(r)
         except HTTPError as err:
             if identical_ok and err.response.status_code == 409:
                 from .file_download import hf_hub_url
@@ -1908,6 +1993,13 @@ class HfApi:
               if the HuggingFace API returned an error
             - [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)
               if some parameter value is invalid
+            - [`~huggingface_hub.utils.RepositoryNotFoundError`]
+              If the repository to download from cannot be found. This may be because it doesn't exist,
+              or because it is set to `private` and you do not have access.
+            - [`~huggingface_hub.utils.RevisionNotFoundError`]
+              If the revision to download from cannot be found.
+            - [`~huggingface_hub.utils.EntryNotFoundError`]
+              If the file to download cannot be found.
 
         </Tip>
 
@@ -1927,7 +2019,7 @@ class HfApi:
         headers = {"authorization": f"Bearer {token}"}
         r = requests.delete(path, headers=headers)
 
-        r.raise_for_status()
+        _raise_for_status(r)
 
     @_deprecate_positional_args
     def get_full_repo_name(
diff --git a/src/huggingface_hub/keras_mixin.py b/src/huggingface_hub/keras_mixin.py
index c6cd5722..9d54135e 100644
--- a/src/huggingface_hub/keras_mixin.py
+++ b/src/huggingface_hub/keras_mixin.py
@@ -50,35 +50,6 @@ def _create_hyperparameter_table(model):
     return table
 
 
-def _parse_model_history(model, save_directory):
-    lines = None
-    if model.history is not None:
-        if model.history.history != {}:
-            path = os.path.join(save_directory, "history.json")
-            with open(path, "w", encoding="utf-8") as f:
-                json.dump(model.history.history, f, indent=2, sort_keys=True)
-            lines = []
-            logs = model.history.history
-            num_epochs = len(logs["loss"])
-
-            for value in range(num_epochs):
-                epoch_dict = {
-                    log_key: log_value_list[value]
-                    for log_key, log_value_list in logs.items()
-                }
-                values = dict()
-                for k, v in epoch_dict.items():
-                    if k.startswith("val_"):
-                        k = "validation_" + k[4:]
-                    elif k != "epoch":
-                        k = "train_" + k
-                    splits = k.split("_")
-                    name = " ".join([part.capitalize() for part in splits])
-                    values[name] = v
-                lines.append(values)
-    return lines
-
-
 def _plot_network(model, save_directory):
     tf.keras.utils.plot_model(
         model,
@@ -93,26 +64,6 @@ def _plot_network(model, save_directory):
     )
 
 
-def _write_metrics(model, model_card, save_directory):
-    lines = _parse_model_history(model, save_directory)
-    if lines is not None:
-        model_card += "\n| Epochs |"
-
-        for i in lines[0].keys():
-            model_card += f" {i} |"
-        model_card += "\n |"
-        for i in range(len(lines[0].keys()) + 1):
-            model_card += "--- |"  # add header of table
-        for line in lines:
-            model_card += f"\n| {lines.index(line) + 1}|"  # add values
-            for key in line:
-                value = round(line[key], 3)
-                model_card += f" {value}| "
-    else:
-        model_card += "Model history needed"
-    return model_card
-
-
 def _create_model_card(
     model,
     repo_dir: Path,
@@ -139,8 +90,6 @@ def _create_model_card(
         model_card += "\nThe following hyperparameters were used during training:\n\n"
         model_card += hyperparameters
         model_card += "\n"
-    model_card += "\n ## Training Metrics\n"
-    model_card = _write_metrics(model, model_card, repo_dir)
     if plot_model and os.path.exists(f"{repo_dir}/model.png"):
         model_card += "\n ## Model Plot\n"
         model_card += "\n<details>"
@@ -232,6 +181,18 @@ def save_pretrained_keras(
         else:
             metadata["tags"] = [task_name]
 
+    if model.history is not None:
+        if model.history.history != {}:
+            path = os.path.join(save_directory, "history.json")
+            if os.path.exists(path):
+                warnings.warn(
+                    "`history.json` file already exists, it will be overwritten by the"
+                    " history of this version.",
+                    UserWarning,
+                )
+            with open(path, "w", encoding="utf-8") as f:
+                json.dump(model.history.history, f, indent=2, sort_keys=True)
+
     _create_model_card(model, save_directory, plot_model, metadata)
     tf.keras.models.save_model(
         model, save_directory, include_optimizer=include_optimizer, **model_save_kwargs
diff --git a/src/huggingface_hub/repocard.py b/src/huggingface_hub/repocard.py
index b62018a1..9ddc1767 100644
--- a/src/huggingface_hub/repocard.py
+++ b/src/huggingface_hub/repocard.py
@@ -1,6 +1,8 @@
 import dataclasses
 import os
 import re
+import shutil
+import tempfile
 from pathlib import Path
 from typing import Any, Dict, Optional, Union
 
@@ -149,50 +151,53 @@ def metadata_update(
         `str`: URL of the commit which updated the card metadata.
     """
 
-    filepath = hf_hub_download(
+    upstream_filepath = hf_hub_download(
         repo_id,
         filename=REPOCARD_NAME,
         repo_type=repo_type,
         use_auth_token=token,
-        force_download=True,
     )
-    existing_metadata = metadata_load(filepath)
+    # work on a copy of the upstream file, to not mess up the cache
+    with tempfile.TemporaryDirectory() as tmpdirname:
+        filepath = shutil.copy(upstream_filepath, tmpdirname)
 
-    for key in metadata:
-        # update model index containing the evaluation results
-        if key == "model-index":
-            if "model-index" not in existing_metadata:
-                existing_metadata["model-index"] = metadata["model-index"]
-            else:
-                # the model-index contains a list of results as used by PwC but only has one element thus we take the first one
-                existing_metadata["model-index"][0][
-                    "results"
-                ] = _update_metadata_model_index(
-                    existing_metadata["model-index"][0]["results"],
-                    metadata["model-index"][0]["results"],
-                    overwrite=overwrite,
-                )
-        # update all fields except model index
-        else:
-            if key in existing_metadata and not overwrite:
-                if existing_metadata[key] != metadata[key]:
-                    raise ValueError(
-                        f"""You passed a new value for the existing meta data field '{key}'. Set `overwrite=True` to overwrite existing metadata."""
+        existing_metadata = metadata_load(filepath)
+
+        for key in metadata:
+            # update model index containing the evaluation results
+            if key == "model-index":
+                if "model-index" not in existing_metadata:
+                    existing_metadata["model-index"] = metadata["model-index"]
+                else:
+                    # the model-index contains a list of results as used by PwC but only has one element thus we take the first one
+                    existing_metadata["model-index"][0][
+                        "results"
+                    ] = _update_metadata_model_index(
+                        existing_metadata["model-index"][0]["results"],
+                        metadata["model-index"][0]["results"],
+                        overwrite=overwrite,
                     )
+            # update all fields except model index
             else:
-                existing_metadata[key] = metadata[key]
+                if key in existing_metadata and not overwrite:
+                    if existing_metadata[key] != metadata[key]:
+                        raise ValueError(
+                            f"""You passed a new value for the existing meta data field '{key}'. Set `overwrite=True` to overwrite existing metadata."""
+                        )
+                else:
+                    existing_metadata[key] = metadata[key]
 
-    # save and push to hub
-    metadata_save(filepath, existing_metadata)
+        # save and push to hub
+        metadata_save(filepath, existing_metadata)
 
-    return HfApi().upload_file(
-        path_or_fileobj=filepath,
-        path_in_repo=REPOCARD_NAME,
-        repo_id=repo_id,
-        repo_type=repo_type,
-        identical_ok=False,
-        token=token,
-    )
+        return HfApi().upload_file(
+            path_or_fileobj=filepath,
+            path_in_repo=REPOCARD_NAME,
+            repo_id=repo_id,
+            repo_type=repo_type,
+            identical_ok=False,
+            token=token,
+        )
 
 
 def _update_metadata_model_index(existing_results, new_results, overwrite=False):
diff --git a/src/huggingface_hub/utils/__init__.py b/src/huggingface_hub/utils/__init__.py
index 108414a3..a09fbcf8 100644
--- a/src/huggingface_hub/utils/__init__.py
+++ b/src/huggingface_hub/utils/__init__.py
@@ -15,4 +15,5 @@
 # See the License for the specific language governing permissions and
 # limitations under the License
 
+from ._errors import EntryNotFoundError, RepositoryNotFoundError, RevisionNotFoundError
 from ._subprocess import run_subprocess
diff --git a/src/huggingface_hub/utils/_errors.py b/src/huggingface_hub/utils/_errors.py
new file mode 100644
index 00000000..fb1aef01
--- /dev/null
+++ b/src/huggingface_hub/utils/_errors.py
@@ -0,0 +1,78 @@
+from requests import HTTPError
+
+
+class RepositoryNotFoundError(HTTPError):
+    """
+    Raised when trying to access a hf.co URL with an invalid repository name, or
+    with a private repo name the user does not have access to.
+
+    Example:
+
+    ```py
+    >>> from huggingface_hub import model_info
+    >>> model_info("<non_existant_repository>")
+    huggingface_hub.utils._errors.RepositoryNotFoundError: 404 Client Error: Repository Not Found for url: <url>
+    ```
+    """
+
+
+class RevisionNotFoundError(HTTPError):
+    """
+    Raised when trying to access a hf.co URL with a valid repository but an invalid
+    revision.
+
+    Example:
+
+    ```py
+    >>> from huggingface_hub import hf_hub_download
+    >>> hf_hub_download('bert-base-cased', 'config.json', revision='<non-existant-revision>')
+    huggingface_hub.utils._errors.RevisionNotFoundError: 404 Client Error: Revision Not Found for url: <url>
+    ```
+
+    """
+
+
+class EntryNotFoundError(HTTPError):
+    """
+    Raised when trying to access a hf.co URL with a valid repository and revision
+    but an invalid filename.
+
+    Example:
+
+    ```py
+    >>> from huggingface_hub import hf_hub_download
+    >>> hf_hub_download('bert-base-cased', '<non-existant-file>')
+    huggingface_hub.utils._errors.EntryNotFoundError: 404 Client Error: Entry Not Found for url: <url>
+    ```
+    """
+
+
+def _raise_for_status(request):
+    """
+    Internal version of `request.raise_for_status()` that will refine a
+    potential HTTPError.
+    """
+    if "X-Error-Code" in request.headers:
+        error_code = request.headers["X-Error-Code"]
+        if error_code == "RepoNotFound":
+            raise RepositoryNotFoundError(
+                f"404 Client Error: Repository Not Found for url: {request.url}. "
+                "If the repo is private, make sure you are authenticated."
+            )
+        elif error_code == "RevisionNotFound":
+            raise RevisionNotFoundError(
+                f"404 Client Error: Revision Not Found for url: {request.url}"
+            )
+        elif error_code == "EntryNotFound":
+            raise EntryNotFoundError(
+                f"404 Client Error: Entry Not Found for url: {request.url}"
+            )
+
+    if request.status_code == 401:
+        # The repo was not found and the user is not Authenticated
+        raise RepositoryNotFoundError(
+            f"401 Client Error: Repository Not Found for url: {request.url}. "
+            "If the repo is private, make sure you are authenticated."
+        )
+
+    request.raise_for_status()


Test patch:
diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml
index d286f14b..f398918d 100644
--- a/.github/workflows/python-tests.yml
+++ b/.github/workflows/python-tests.yml
@@ -42,12 +42,15 @@ jobs:
       - run: |
           if [[ "${{ matrix.test_repository }}" == "Repository only" ]]
           then
-            pytest -Werror::FutureWarning --log-cli-level=INFO -sv ./tests/test_repository.py -k RepositoryTest
-          
+            pytest --cov -Werror::FutureWarning --log-cli-level=INFO -sv ./tests/test_repository.py -k RepositoryTest
+
           else
-            pytest -Werror::FutureWarning --log-cli-level=INFO -sv ./tests/ -k 'not RepositoryTest'
+            pytest --cov -Werror::FutureWarning --log-cli-level=INFO -sv ./tests/ -k 'not RepositoryTest'
           fi
 
+      - name: Upload coverage reports to Codecov with GitHub Action
+        uses: codecov/codecov-action@v3
+
   build_pytorch:
     runs-on: ubuntu-latest
     strategy:
@@ -67,7 +70,10 @@ jobs:
         pip install --upgrade pip
         pip install .[testing,torch]
 
-    - run: pytest -Werror::FutureWarning -sv ./tests/test_hubmixin*
+    - run: pytest --cov -Werror::FutureWarning -sv ./tests/test_hubmixin*
+
+    - name: Upload coverage reports to Codecov with GitHub Action
+      uses: codecov/codecov-action@v3
 
   build_tensorflow:
     runs-on: ubuntu-latest
@@ -89,8 +95,11 @@ jobs:
         pip install --upgrade pip
         pip install .[testing,tensorflow]
 
-    - run: pytest -Werror::FutureWarning -sv ./tests/test_keras*
-  
+    - run: pytest --cov -Werror::FutureWarning -sv ./tests/test_keras*
+
+    - name: Upload coverage reports to Codecov with GitHub Action
+      uses: codecov/codecov-action@v3
+
   build_fastai:
     runs-on: ubuntu-latest
     strategy:
@@ -110,7 +119,10 @@ jobs:
         pip install --upgrade pip
         pip install .[testing,fastai]
 
-    - run: pytest -Werror::FutureWarning -sv ./tests/test_fastai*
+    - run: pytest --cov -Werror::FutureWarning -sv ./tests/test_fastai*
+
+    - name: Upload coverage reports to Codecov with GitHub Action
+      uses: codecov/codecov-action@v3
 
   tests_lfs:
     runs-on: ubuntu-latest
@@ -132,4 +144,7 @@ jobs:
         pip install --upgrade pip
         pip install .[testing]
 
-    - run: RUN_GIT_LFS_TESTS=1 pytest -Werror::FutureWarning -sv ./tests/ -k "HfLargefilesTest"
+    - run: RUN_GIT_LFS_TESTS=1 pytest --cov -Werror::FutureWarning -sv ./tests/ -k "HfLargefilesTest"
+
+    - name: Upload coverage reports to Codecov with GitHub Action
+      uses: codecov/codecov-action@v3
diff --git a/tests/test_file_download.py b/tests/test_file_download.py
index 071478c0..0681ddc3 100644
--- a/tests/test_file_download.py
+++ b/tests/test_file_download.py
@@ -14,7 +14,6 @@
 
 import unittest
 
-import requests
 from huggingface_hub.constants import (
     CONFIG_NAME,
     PYTORCH_WEIGHTS_NAME,
@@ -26,6 +25,11 @@ from huggingface_hub.file_download import (
     hf_hub_download,
     hf_hub_url,
 )
+from huggingface_hub.utils import (
+    EntryNotFoundError,
+    RepositoryNotFoundError,
+    RevisionNotFoundError,
+)
 
 from .testing_utils import (
     DUMMY_MODEL_ID,
@@ -86,7 +90,9 @@ class CachedDownloadTests(unittest.TestCase):
     def test_file_not_found(self):
         # Valid revision (None) but missing file.
         url = hf_hub_url(DUMMY_MODEL_ID, filename="missing.bin")
-        with self.assertRaisesRegex(requests.exceptions.HTTPError, "404 Client Error"):
+        with self.assertRaisesRegex(
+            EntryNotFoundError, "404 Client Error: Entry Not Found"
+        ):
             _ = cached_download(url, legacy_cache_layout=True)
 
     def test_revision_not_found(self):
@@ -96,7 +102,17 @@ class CachedDownloadTests(unittest.TestCase):
             filename=CONFIG_NAME,
             revision=DUMMY_MODEL_ID_REVISION_INVALID,
         )
-        with self.assertRaisesRegex(requests.exceptions.HTTPError, "404 Client Error"):
+        with self.assertRaisesRegex(
+            RevisionNotFoundError, "404 Client Error: Revision Not Found"
+        ):
+            _ = cached_download(url, legacy_cache_layout=True)
+
+    def test_repo_not_found(self):
+        # Invalid model file.
+        url = hf_hub_url("bert-base", filename="pytorch_model.bin")
+        with self.assertRaisesRegex(
+            RepositoryNotFoundError, "401 Client Error: Repository Not Found"
+        ):
             _ = cached_download(url, legacy_cache_layout=True)
 
     def test_standard_object(self):
diff --git a/tests/test_hf_api.py b/tests/test_hf_api.py
index c64cf6c4..a87d301a 100644
--- a/tests/test_hf_api.py
+++ b/tests/test_hf_api.py
@@ -304,10 +304,7 @@ class HfApiEndpointsTest(HfApiCommonTestWithLogin):
         # test for #751
         with pytest.raises(
             HTTPError,
-            match=(
-                "No model repo found matching __DUMMY_TRANSFORMERS_USER__/"
-                "repo-that-does-not-exist"
-            ),
+            match="404 Client Error: Repository Not Found",
         ):
             self._api.delete_repo("repo-that-does-not-exist", token=self._token)
 
@@ -1109,7 +1106,9 @@ class HfApiPrivateTest(HfApiCommonTestWithLogin):
     def test_model_info(self):
         shutil.rmtree(os.path.dirname(HfFolder.path_token))
         # Test we cannot access model info without a token
-        with self.assertRaisesRegex(requests.exceptions.HTTPError, "404 Client Error"):
+        with self.assertRaisesRegex(
+            requests.exceptions.HTTPError, "404 Client Error: Repository Not Found"
+        ):
             _ = self._api.model_info(repo_id=f"{USER}/{self.REPO_NAME}")
         # Test we can access model info with a token
         model_info = self._api.model_info(
diff --git a/tests/test_keras_integration.py b/tests/test_keras_integration.py
index 6e816fc7..8a8634fc 100644
--- a/tests/test_keras_integration.py
+++ b/tests/test_keras_integration.py
@@ -1,3 +1,4 @@
+import json
 import os
 import shutil
 import tempfile
@@ -226,8 +227,49 @@ class HubKerasSequentialTest(HubMixingTestKeras):
         self.assertIn("model.png", files)
         self.assertIn("README.md", files)
         self.assertIn("history.json", files)
+        with open(f"{WORKING_REPO_DIR}/{REPO_NAME}/history.json") as f:
+            history = json.load(f)
+
+        self.assertEqual(history, model.history.history)
         self.assertEqual(len(files), 7)
 
+    def test_save_model_card_history_removal(self):
+        REPO_NAME = repo_name("save")
+        model = self.model_init()
+        model = self.model_fit(model)
+        with tempfile.TemporaryDirectory() as tmpdirname:
+            os.makedirs(f"{tmpdirname}/{WORKING_REPO_DIR}/{REPO_NAME}")
+            with open(
+                f"{tmpdirname}/{WORKING_REPO_DIR}/{REPO_NAME}/history.json", "w+"
+            ) as fp:
+                fp.write("Keras FTW")
+
+            with pytest.warns(
+                UserWarning, match="`history.json` file already exists, *"
+            ):
+                save_pretrained_keras(
+                    model,
+                    f"{tmpdirname}/{WORKING_REPO_DIR}/{REPO_NAME}",
+                )
+                # assert that it's not the same as old history file and it's overridden
+                with open(
+                    f"{tmpdirname}/{WORKING_REPO_DIR}/{REPO_NAME}/history.json", "r"
+                ) as f:
+                    history_content = f.read()
+                    self.assertNotEqual("Keras FTW", history_content)
+
+            # Check the history is saved as a json in the repository.
+            files = os.listdir(f"{tmpdirname}/{WORKING_REPO_DIR}/{REPO_NAME}")
+            self.assertIn("history.json", files)
+
+            # Check that there is no "Training Metrics" section in the model card.
+            # This was done in an older version.
+            with open(
+                f"{tmpdirname}/{WORKING_REPO_DIR}/{REPO_NAME}/README.md", "r"
+            ) as file:
+                data = file.read()
+            self.assertNotIn(data, "Training Metrics")
+
     def test_save_pretrained_optimizer_state(self):
         REPO_NAME = repo_name("save")
         model = self.model_init()
diff --git a/tests/test_repocard.py b/tests/test_repocard.py
index 5a54c984..f857e8be 100644
--- a/tests/test_repocard.py
+++ b/tests/test_repocard.py
@@ -30,6 +30,7 @@ from huggingface_hub import (
     metadata_update,
 )
 from huggingface_hub.constants import REPOCARD_NAME
+from huggingface_hub.file_download import hf_hub_download
 from huggingface_hub.hf_api import HfApi
 from huggingface_hub.repository import Repository
 from huggingface_hub.utils import logging
@@ -240,6 +241,23 @@ class RepocardUpdateTest(unittest.TestCase):
         updated_metadata = metadata_load(self.repo_path / self.REPO_NAME / "README.md")
         self.assertDictEqual(updated_metadata, new_metadata)
 
+    def test_metadata_update_upstream(self):
+        new_metadata = copy.deepcopy(self.existing_metadata)
+        new_metadata["model-index"][0]["results"][0]["metrics"][0]["value"] = 0.1
+
+        path = hf_hub_download(
+            f"{USER}/{self.REPO_NAME}",
+            filename=REPOCARD_NAME,
+            use_auth_token=self._token,
+        )
+
+        metadata_update(
+            f"{USER}/{self.REPO_NAME}", new_metadata, token=self._token, overwrite=True
+        )
+
+        self.assertNotEqual(metadata_load(path), new_metadata)
+        self.assertEqual(metadata_load(path), self.existing_metadata)
+
     def test_update_existing_result_without_overwrite(self):
         new_metadata = copy.deepcopy(self.existing_metadata)
         new_metadata["model-index"][0]["results"][0]["metrics"][0][