Repository: huggingface/huggingface_hub
Pull request #1075 (2022-09-22 15:54:14)

Add user agent to all requests with `huggingface_hub` version (and other)
Fix https://github.com/huggingface/huggingface_hub/issues/1018.

For the user agent, I took the same implementation as before (custom library name/version, python, huggingface_hub, tensorflow, torch, fastai, fastcore). This is only a first version, we can iterate afterwards if we want to update this rule.

I also moved all imports to a `_runtime.py` utility trying to keep backward compatibility everywhere for non private methods/attributes.

(cc @Kakulukian @Pierrci who where interested in having this)

HuggingFaceDocBuilderDev: The docs for this PR live [here](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_1075). All of your documentation changes will be reflected on that endpoint.
codecov[bot]: # [Codecov](https://codecov.io/gh/huggingface/huggingface_hub/pull/1075?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) Report
Base: **84.36**% // Head: **84.39**% // Increases project coverage by **`+0.02%`** :tada:
> Coverage data is based on head [(`9030922`)](https://codecov.io/gh/huggingface/huggingface_hub/pull/1075?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) compared to base [(`e8801bd`)](https://codecov.io/gh/huggingface/huggingface_hub/commit/e8801bd4629ae3ca2473583a0932667678b813f4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface).
> Patch coverage: 95.95% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1075      +/-   ##
==========================================
+ Coverage   84.36%   84.39%   +0.02%     
==========================================
  Files          40       41       +1     
  Lines        4100     4095       -5     
==========================================
- Hits         3459     3456       -3     
+ Misses        641      639       -2     
```


| [Impacted Files](https://codecov.io/gh/huggingface/huggingface_hub/pull/1075?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface) | Coverage Δ | |
|---|---|---|
| [src/huggingface\_hub/file\_download.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/1075/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi9maWxlX2Rvd25sb2FkLnB5) | `87.09% <94.44%> (-1.40%)` | :arrow_down: |
| [src/huggingface\_hub/utils/\_runtime.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/1075/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi91dGlscy9fcnVudGltZS5weQ==) | `94.44% <94.44%> (ø)` | |
| [src/huggingface\_hub/fastai\_utils.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/1075/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi9mYXN0YWlfdXRpbHMucHk=) | `81.18% <100.00%> (ø)` | |
| [src/huggingface\_hub/keras\_mixin.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/1075/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi9rZXJhc19taXhpbi5weQ==) | `90.47% <100.00%> (ø)` | |
| [src/huggingface\_hub/repocard.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/1075/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi9yZXBvY2FyZC5weQ==) | `94.47% <100.00%> (+0.03%)` | :arrow_up: |
| [src/huggingface\_hub/utils/\_\_init\_\_.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/1075/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi91dGlscy9fX2luaXRfXy5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/huggingface\_hub/utils/\_headers.py](https://codecov.io/gh/huggingface/huggingface_hub/pull/1075/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface#diff-c3JjL2h1Z2dpbmdmYWNlX2h1Yi91dGlscy9faGVhZGVycy5weQ==) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/huggingface/huggingface_hub/pull/1075?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=huggingface).


Patch:
diff --git a/src/huggingface_hub/fastai_utils.py b/src/huggingface_hub/fastai_utils.py
index 0fd6ff68..d22c587e 100644
--- a/src/huggingface_hub/fastai_utils.py
+++ b/src/huggingface_hub/fastai_utils.py
@@ -9,15 +9,16 @@ from packaging import version
 
 from huggingface_hub import snapshot_download
 from huggingface_hub.constants import CONFIG_NAME
-from huggingface_hub.file_download import (
-    _PY_VERSION,
+from huggingface_hub.hf_api import HfApi
+from huggingface_hub.utils import (
     get_fastai_version,
     get_fastcore_version,
+    get_python_version,
 )
-from huggingface_hub.hf_api import HfApi
 
 from .utils import logging, validate_hf_hub_args
 from .utils._deprecation import _deprecate_arguments, _deprecate_positional_args
+from .utils._runtime import _PY_VERSION  # noqa: F401 # for backward compatibility...
 
 
 logger = logging.get_logger(__name__)
@@ -214,7 +215,7 @@ More information needed
 """
 
 PYPROJECT_TEMPLATE = f"""[build-system]
-requires = ["setuptools>=40.8.0", "wheel", "python={_PY_VERSION}", "fastai={get_fastai_version()}", "fastcore={get_fastcore_version()}"]
+requires = ["setuptools>=40.8.0", "wheel", "python={get_python_version()}", "fastai={get_fastai_version()}", "fastcore={get_fastcore_version()}"]
 build-backend = "setuptools.build_meta:__legacy__"
 """
 
diff --git a/src/huggingface_hub/file_download.py b/src/huggingface_hub/file_download.py
index 261a7038..9ad5a707 100644
--- a/src/huggingface_hub/file_download.py
+++ b/src/huggingface_hub/file_download.py
@@ -5,7 +5,6 @@ import json
 import os
 import re
 import shutil
-import sys
 import tempfile
 import warnings
 from contextlib import contextmanager
@@ -16,14 +15,12 @@ from pathlib import Path
 from typing import BinaryIO, Dict, Optional, Tuple, Union
 from urllib.parse import quote, urlparse
 
-import packaging.version
-
 import requests
 from filelock import FileLock
 from huggingface_hub import constants
 from requests.exceptions import ConnectTimeout, ProxyError
 
-from . import __version__
+from . import __version__  # noqa: F401 # for backward compatibility
 from .constants import (
     DEFAULT_REVISION,
     HUGGINGFACE_CO_URL_TEMPLATE,
@@ -34,6 +31,20 @@ from .constants import (
     REPO_TYPES,
     REPO_TYPES_URL_PREFIXES,
 )
+from .utils import get_fastai_version  # noqa: F401 # for backward compatibility
+from .utils import get_fastcore_version  # noqa: F401 # for backward compatibility
+from .utils import get_graphviz_version  # noqa: F401 # for backward compatibility
+from .utils import get_jinja_version  # noqa: F401 # for backward compatibility
+from .utils import get_pydot_version  # noqa: F401 # for backward compatibility
+from .utils import get_tf_version  # noqa: F401 # for backward compatibility
+from .utils import get_torch_version  # noqa: F401 # for backward compatibility
+from .utils import is_fastai_available  # noqa: F401 # for backward compatibility
+from .utils import is_fastcore_available  # noqa: F401 # for backward compatibility
+from .utils import is_graphviz_available  # noqa: F401 # for backward compatibility
+from .utils import is_jinja_available  # noqa: F401 # for backward compatibility
+from .utils import is_pydot_available  # noqa: F401 # for backward compatibility
+from .utils import is_tf_available  # noqa: F401 # for backward compatibility
+from .utils import is_torch_available  # noqa: F401 # for backward compatibility
 from .utils import (
     EntryNotFoundError,
     LocalEntryNotFoundError,
@@ -44,133 +55,12 @@ from .utils import (
     tqdm,
     validate_hf_hub_args,
 )
+from .utils._headers import _http_user_agent
+from .utils._runtime import _PY_VERSION  # noqa: F401 # for backward compatibility
 
 
 logger = logging.get_logger(__name__)
 
-_PY_VERSION: str = sys.version.split()[0].rstrip("+")
-
-if packaging.version.Version(_PY_VERSION) < packaging.version.Version("3.8.0"):
-    import importlib_metadata
-else:
-    import importlib.metadata as importlib_metadata
-
-_torch_version = "N/A"
-_torch_available = False
-try:
-    _torch_version = importlib_metadata.version("torch")
-    _torch_available = True
-except importlib_metadata.PackageNotFoundError:
-    pass
-
-_pydot_available = False
-
-try:
-    _pydot_version = importlib_metadata.version("pydot")
-    _pydot_available = True
-except importlib_metadata.PackageNotFoundError:
-    pass
-
-
-def is_pydot_available():
-    return _pydot_available
-
-
-_graphviz_available = False
-
-try:
-    _graphviz_version = importlib_metadata.version("graphviz")
-    _graphviz_available = True
-except importlib_metadata.PackageNotFoundError:
-    pass
-
-
-def is_graphviz_available():
-    return _graphviz_available
-
-
-_tf_version = "N/A"
-_tf_available = False
-_tf_candidates = (
-    "tensorflow",
-    "tensorflow-cpu",
-    "tensorflow-gpu",
-    "tf-nightly",
-    "tf-nightly-cpu",
-    "tf-nightly-gpu",
-    "intel-tensorflow",
-    "intel-tensorflow-avx512",
-    "tensorflow-rocm",
-    "tensorflow-macos",
-)
-for package_name in _tf_candidates:
-    try:
-        _tf_version = importlib_metadata.version(package_name)
-        _tf_available = True
-        break
-    except importlib_metadata.PackageNotFoundError:
-        pass
-
-_fastai_version = "N/A"
-_fastai_available = False
-try:
-    _fastai_version: str = importlib_metadata.version("fastai")
-    _fastai_available = True
-except importlib_metadata.PackageNotFoundError:
-    pass
-
-_fastcore_version = "N/A"
-_fastcore_available = False
-try:
-    _fastcore_version: str = importlib_metadata.version("fastcore")
-    _fastcore_available = True
-except importlib_metadata.PackageNotFoundError:
-    pass
-
-_jinja_version = "N/A"
-_jinja_available = False
-try:
-    _jinja_version: str = importlib_metadata.version("Jinja2")
-    _jinja_available = True
-except importlib_metadata.PackageNotFoundError:
-    pass
-
-
-def is_torch_available():
-    return _torch_available
-
-
-def is_tf_available():
-    return _tf_available
-
-
-def get_tf_version():
-    return _tf_version
-
-
-def is_fastai_available():
-    return _fastai_available
-
-
-def get_fastai_version():
-    return _fastai_version
-
-
-def is_fastcore_available():
-    return _fastcore_available
-
-
-def get_fastcore_version():
-    return _fastcore_version
-
-
-def is_jinja_available():
-    return _jinja_available
-
-
-def get_jinja_version():
-    return _jinja_version
-
 
 _are_symlinks_supported_in_dir: Dict[str, bool] = {}
 
@@ -432,38 +322,12 @@ def http_user_agent(
     library_version: Optional[str] = None,
     user_agent: Union[Dict, str, None] = None,
 ) -> str:
-    """Formats a user-agent string with basic info about a request.
-
-    Args:
-        library_name (`str`, *optional*):
-            The name of the library to which the object corresponds.
-        library_version (`str`, *optional*):
-            The version of the library.
-        user_agent (`str`, `dict`, *optional*):
-            The user agent info in the form of a dictionary or a single string.
-
-    Returns:
-        The formatted user-agent string.
-    """
-    if library_name is not None:
-        ua = f"{library_name}/{library_version}"
-    else:
-        ua = "unknown/None"
-    ua += f"; hf_hub/{__version__}"
-    ua += f"; python/{_PY_VERSION}"
-    if is_torch_available():
-        ua += f"; torch/{_torch_version}"
-    if is_tf_available():
-        ua += f"; tensorflow/{_tf_version}"
-    if is_fastai_available():
-        ua += f"; fastai/{_fastai_version}"
-    if is_fastcore_available():
-        ua += f"; fastcore/{_fastcore_version}"
-    if isinstance(user_agent, dict):
-        ua += "; " + "; ".join(f"{k}/{v}" for k, v in user_agent.items())
-    elif isinstance(user_agent, str):
-        ua += "; " + user_agent
-    return ua
+    """Deprecated in favor of [`build_hf_headers`]."""
+    return _http_user_agent(
+        library_name=library_name,
+        library_version=library_version,
+        user_agent=user_agent,
+    )
 
 
 class OfflineModeIsEnabled(ConnectionError):
@@ -733,8 +597,8 @@ def cached_download(
 
     os.makedirs(cache_dir, exist_ok=True)
 
-    headers = build_hf_headers(use_auth_token=use_auth_token)
-    headers["user-agent"] = http_user_agent(
+    headers = build_hf_headers(
+        use_auth_token=use_auth_token,
         library_name=library_name,
         library_version=library_version,
         user_agent=user_agent,
@@ -1172,8 +1036,8 @@ def hf_hub_download(
 
     url = hf_hub_url(repo_id, filename, repo_type=repo_type, revision=revision)
 
-    headers = build_hf_headers(use_auth_token=use_auth_token)
-    headers["user-agent"] = http_user_agent(
+    headers = build_hf_headers(
+        use_auth_token=use_auth_token,
         library_name=library_name,
         library_version=library_version,
         user_agent=user_agent,
diff --git a/src/huggingface_hub/keras_mixin.py b/src/huggingface_hub/keras_mixin.py
index f293301d..43ee885c 100644
--- a/src/huggingface_hub/keras_mixin.py
+++ b/src/huggingface_hub/keras_mixin.py
@@ -9,13 +9,13 @@ from typing import Any, Dict, List, Optional, Union
 from urllib.parse import quote
 
 from huggingface_hub import CommitOperationDelete, ModelHubMixin, snapshot_download
-from huggingface_hub.file_download import (
+from huggingface_hub.utils import (
     get_tf_version,
     is_graphviz_available,
     is_pydot_available,
     is_tf_available,
+    yaml_dump,
 )
-from huggingface_hub.utils import yaml_dump
 
 from .constants import CONFIG_NAME, DEFAULT_REVISION
 from .hf_api import HfApi, _parse_revision_from_pr_url, _prepare_upload_folder_commit
diff --git a/src/huggingface_hub/repocard.py b/src/huggingface_hub/repocard.py
index 22175873..a301ab1a 100644
--- a/src/huggingface_hub/repocard.py
+++ b/src/huggingface_hub/repocard.py
@@ -13,7 +13,7 @@ else:
 
 import requests
 import yaml
-from huggingface_hub.file_download import hf_hub_download, is_jinja_available
+from huggingface_hub.file_download import hf_hub_download
 from huggingface_hub.hf_api import upload_file
 from huggingface_hub.repocard_data import (
     CardData,
@@ -23,7 +23,7 @@ from huggingface_hub.repocard_data import (
     eval_results_to_model_index,
     model_index_to_eval_results,
 )
-from huggingface_hub.utils import yaml_dump
+from huggingface_hub.utils import is_jinja_available, yaml_dump
 
 from .constants import REPOCARD_NAME
 from .utils import EntryNotFoundError, validate_hf_hub_args
diff --git a/src/huggingface_hub/utils/__init__.py b/src/huggingface_hub/utils/__init__.py
index 20162259..a4dc42d4 100644
--- a/src/huggingface_hub/utils/__init__.py
+++ b/src/huggingface_hub/utils/__init__.py
@@ -40,6 +40,24 @@ from ._headers import build_hf_headers
 from ._hf_folder import HfFolder
 from ._http import http_backoff
 from ._paths import filter_repo_objects
+from ._runtime import (
+    get_fastai_version,
+    get_fastcore_version,
+    get_graphviz_version,
+    get_hf_hub_version,
+    get_jinja_version,
+    get_pydot_version,
+    get_python_version,
+    get_tf_version,
+    get_torch_version,
+    is_fastai_available,
+    is_fastcore_available,
+    is_graphviz_available,
+    is_jinja_available,
+    is_pydot_available,
+    is_tf_available,
+    is_torch_available,
+)
 from ._subprocess import run_subprocess
 from ._validators import HFValidationError, validate_hf_hub_args, validate_repo_id
 from .tqdm import (
diff --git a/src/huggingface_hub/utils/_headers.py b/src/huggingface_hub/utils/_headers.py
index d5103518..aeeef7bc 100644
--- a/src/huggingface_hub/utils/_headers.py
+++ b/src/huggingface_hub/utils/_headers.py
@@ -17,10 +17,27 @@ import os
 from typing import Dict, Optional, Union
 
 from ._hf_folder import HfFolder
+from ._runtime import (
+    get_fastai_version,
+    get_fastcore_version,
+    get_hf_hub_version,
+    get_python_version,
+    get_tf_version,
+    get_torch_version,
+    is_fastai_available,
+    is_fastcore_available,
+    is_tf_available,
+    is_torch_available,
+)
 
 
 def build_hf_headers(
-    *, use_auth_token: Optional[Union[bool, str]] = None, is_write_action: bool = False
+    *,
+    use_auth_token: Optional[Union[bool, str]] = None,
+    is_write_action: bool = False,
+    library_name: Optional[str] = None,
+    library_version: Optional[str] = None,
+    user_agent: Union[Dict, str, None] = None,
 ) -> Dict[str, str]:
     """
     Build headers dictionary to send in a HF Hub call.
@@ -33,6 +50,10 @@ def build_hf_headers(
     In case of an API call that requires write access, an error is thrown if token is
     `None` or token is an organization token (starting with `"api_org***"`).
 
+    In addition to the auth header, a user-agent is added to provide information about
+    the installed packages (versions of python, huggingface_hub, torch, tensorflow,
+    fastai and fastcore).
+
     Args:
         use_auth_token (`str`, `bool`, *optional*):
             The token to be sent in authorization header for the Hub call:
@@ -44,6 +65,15 @@ def build_hf_headers(
         is_write_action (`bool`, default to `False`):
             Set to True if the API call requires a write access. If `True`, the token
             will be validated (cannot be `None`, cannot start by `"api_org***"`).
+        library_name (`str`, *optional*):
+            The name of the library that is making the HTTP request. Will be added to
+            the user-agent header.
+        library_version (`str`, *optional*):
+            The version of the library that is making the HTTP request. Will be added
+            to the user-agent header.
+        user_agent (`str`, `dict`, *optional*):
+            The user agent info in the form of a dictionary or a single string. It will
+            be completed with information about the installed packages.
 
     Returns:
         A `Dict` of headers to pass in your API call.
@@ -51,23 +81,26 @@ def build_hf_headers(
     Example:
     ```py
         >>> build_hf_headers(use_auth_token="hf_***") # explicit token
-        {"authorization": "Bearer hf_***"}
+        {"authorization": "Bearer hf_***", "user-agent": ""}
 
         >>> build_hf_headers(use_auth_token=True) # explicitly use cached token
-        {"authorization": "Bearer hf_***"}
+        {"authorization": "Bearer hf_***",...}
 
         >>> build_hf_headers(use_auth_token=False) # explicitly don't use cached token
-        {}
+        {"user-agent": ...}
 
         >>> build_hf_headers() # implicit use of the cached token
-        {"authorization": "Bearer hf_***"}
+        {"authorization": "Bearer hf_***",...}
 
         # HF_HUB_DISABLE_IMPLICIT_TOKEN=True # to set as env variable
         >>> build_hf_headers() # token is not sent
-        {}
+        {"user-agent": ...}
 
         >>> build_hf_headers(use_auth_token="api_org_***", is_write_action=True)
         ValueError: You must use your personal account token for write-access methods.
+
+        >>> build_hf_headers(library_name="transformers", library_version="1.2.3")
+        {"authorization": ..., "user-agent": "transformers/1.2.3; hf_hub/0.10.2; python/3.10.4; tensorflow/1.55"}
     ```
 
     Raises:
@@ -83,10 +116,15 @@ def build_hf_headers(
     _validate_token_to_send(token_to_send, is_write_action=is_write_action)
 
     # Combine headers
-    headers = {}
+    headers = {
+        "user-agent": _http_user_agent(
+            library_name=library_name,
+            library_version=library_version,
+            user_agent=user_agent,
+        )
+    }
     if token_to_send is not None:
         headers["authorization"] = f"Bearer {token_to_send}"
-    # TODO: add user agent in headers
     return headers
 
 
@@ -137,3 +175,43 @@ def _validate_token_to_send(token: Optional[str], is_write_action: bool) -> None
                 " generate a write-access token, go to"
                 " https://huggingface.co/settings/tokens"
             )
+
+
+def _http_user_agent(
+    *,
+    library_name: Optional[str] = None,
+    library_version: Optional[str] = None,
+    user_agent: Union[Dict, str, None] = None,
+) -> str:
+    """Format a user-agent string containing information about the installed packages.
+
+    Args:
+        library_name (`str`, *optional*):
+            The name of the library that is making the HTTP request.
+        library_version (`str`, *optional*):
+            The version of the library that is making the HTTP request.
+        user_agent (`str`, `dict`, *optional*):
+            The user agent info in the form of a dictionary or a single string.
+
+    Returns:
+        The formatted user-agent string.
+    """
+    if library_name is not None:
+        ua = f"{library_name}/{library_version}"
+    else:
+        ua = "unknown/None"
+    ua += f"; hf_hub/{get_hf_hub_version()}"
+    ua += f"; python/{get_python_version()}"
+    if is_torch_available():
+        ua += f"; torch/{get_torch_version()}"
+    if is_tf_available():
+        ua += f"; tensorflow/{get_tf_version()}"
+    if is_fastai_available():
+        ua += f"; fastai/{get_fastai_version()}"
+    if is_fastcore_available():
+        ua += f"; fastcore/{get_fastcore_version()}"
+    if isinstance(user_agent, dict):
+        ua += "; " + "; ".join(f"{k}/{v}" for k, v in user_agent.items())
+    elif isinstance(user_agent, str):
+        ua += "; " + user_agent
+    return ua
diff --git a/src/huggingface_hub/utils/_runtime.py b/src/huggingface_hub/utils/_runtime.py
new file mode 100644
index 00000000..f06b2546
--- /dev/null
+++ b/src/huggingface_hub/utils/_runtime.py
@@ -0,0 +1,144 @@
+# coding=utf-8
+# Copyright 2022-present, the HuggingFace Inc. team.
+#
+# 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.
+"""Check presence of installed packages at runtime."""
+import sys
+from typing import Optional
+
+import packaging.version
+
+from .. import __version__
+
+
+_PY_VERSION: str = sys.version.split()[0].rstrip("+")
+
+if packaging.version.Version(_PY_VERSION) < packaging.version.Version("3.8.0"):
+    import importlib_metadata
+else:
+    import importlib.metadata as importlib_metadata
+
+
+_package_versions = {}
+
+_CANDIDATES = {
+    "torch": {"torch"},
+    "pydot": {"pydot"},
+    "graphviz": {"graphviz"},
+    "tensorflow": (
+        "tensorflow",
+        "tensorflow-cpu",
+        "tensorflow-gpu",
+        "tf-nightly",
+        "tf-nightly-cpu",
+        "tf-nightly-gpu",
+        "intel-tensorflow",
+        "intel-tensorflow-avx512",
+        "tensorflow-rocm",
+        "tensorflow-macos",
+    ),
+    "fastai": {"fastai"},
+    "fastcore": {"fastcore"},
+    "jinja": {"Jinja2"},
+}
+
+# Check once at runtime
+for candidate_name, package_names in _CANDIDATES.items():
+    _package_versions[candidate_name] = "N/A"
+    for name in package_names:
+        try:
+            _package_versions[candidate_name] = importlib_metadata.version(name)
+            break
+        except importlib_metadata.PackageNotFoundError:
+            pass
+
+
+def _get_version(package_name: str) -> Optional[str]:
+    return _package_versions.get(package_name, "N/A")
+
+
+def _is_available(package_name: str) -> bool:
+    return _get_version(package_name) != "N/A"
+
+
+# Python
+def get_python_version() -> str:
+    return _PY_VERSION
+
+
+# Huggingface Hub
+def get_hf_hub_version() -> str:
+    return __version__
+
+
+# FastAI
+def is_fastai_available() -> bool:
+    return _is_available("fastai")
+
+
+def get_fastai_version() -> str:
+    return _get_version("fastai")
+
+
+# Fastcore
+def is_fastcore_available() -> bool:
+    return _is_available("fastcore")
+
+
+def get_fastcore_version() -> str:
+    return _get_version("fastcore")
+
+
+# Graphviz
+def is_graphviz_available() -> bool:
+    return _is_available("graphviz")
+
+
+def get_graphviz_version() -> str:
+    return _get_version("graphviz")
+
+
+# Jinja
+def is_jinja_available() -> bool:
+    return _is_available("jinja")
+
+
+def get_jinja_version() -> str:
+    return _get_version("jinja")
+
+
+# Pydot
+def is_pydot_available() -> bool:
+    return _is_available("pydot")
+
+
+def get_pydot_version() -> str:
+    return _get_version("pydot")
+
+
+# Tensorflow
+def is_tf_available() -> bool:
+    return _is_available("tensorflow")
+
+
+def get_tf_version() -> str:
+    return _get_version("tensorflow")
+
+
+# Torch
+def is_torch_available() -> bool:
+    return _is_available("torch")
+
+
+def get_torch_version() -> str:
+    return _get_version("torch")


Test patch:
diff --git a/tests/test_fastai_integration.py b/tests/test_fastai_integration.py
index a9568b46..bf3a3be1 100644
--- a/tests/test_fastai_integration.py
+++ b/tests/test_fastai_integration.py
@@ -8,7 +8,7 @@ from huggingface_hub.fastai_utils import (
     from_pretrained_fastai,
     push_to_hub_fastai,
 )
-from huggingface_hub.file_download import (
+from huggingface_hub.utils import (
     is_fastai_available,
     is_fastcore_available,
     is_torch_available,
diff --git a/tests/test_hubmixin.py b/tests/test_hubmixin.py
index d78f6e2e..f672e938 100644
--- a/tests/test_hubmixin.py
+++ b/tests/test_hubmixin.py
@@ -6,10 +6,9 @@ import unittest
 from unittest.mock import Mock
 
 from huggingface_hub import HfApi, hf_hub_download
-from huggingface_hub.file_download import is_torch_available
 from huggingface_hub.hub_mixin import PyTorchModelHubMixin
 from huggingface_hub.repository import Repository
-from huggingface_hub.utils import logging
+from huggingface_hub.utils import is_torch_available, logging
 
 from .testing_constants import ENDPOINT_STAGING, TOKEN, USER
 from .testing_utils import expect_deprecation, repo_name, set_write_permission_and_retry
diff --git a/tests/test_keras_integration.py b/tests/test_keras_integration.py
index becd6065..d19d528e 100644
--- a/tests/test_keras_integration.py
+++ b/tests/test_keras_integration.py
@@ -8,11 +8,6 @@ import unittest
 import pytest
 
 from huggingface_hub import HfApi, hf_hub_download
-from huggingface_hub.file_download import (
-    is_graphviz_available,
-    is_pydot_available,
-    is_tf_available,
-)
 from huggingface_hub.keras_mixin import (
     KerasModelHubMixin,
     from_pretrained_keras,
@@ -20,7 +15,12 @@ from huggingface_hub.keras_mixin import (
     save_pretrained_keras,
 )
 from huggingface_hub.repository import Repository
-from huggingface_hub.utils import logging
+from huggingface_hub.utils import (
+    is_graphviz_available,
+    is_pydot_available,
+    is_tf_available,
+    logging,
+)
 
 from .testing_constants import ENDPOINT_STAGING, TOKEN, USER
 from .testing_utils import (
diff --git a/tests/test_repocard.py b/tests/test_repocard.py
index 9cec19fe..3f75ba6d 100644
--- a/tests/test_repocard.py
+++ b/tests/test_repocard.py
@@ -36,12 +36,12 @@ from huggingface_hub import (
     metadata_update,
 )
 from huggingface_hub.constants import REPOCARD_NAME
-from huggingface_hub.file_download import hf_hub_download, is_jinja_available
+from huggingface_hub.file_download import hf_hub_download
 from huggingface_hub.hf_api import HfApi
 from huggingface_hub.repocard import RepoCard
 from huggingface_hub.repocard_data import CardData
 from huggingface_hub.repository import Repository
-from huggingface_hub.utils import logging
+from huggingface_hub.utils import is_jinja_available, logging
 
 from .testing_constants import (
     ENDPOINT_STAGING,
diff --git a/tests/test_tf_import.py b/tests/test_tf_import.py
index 96aac7b8..d794e009 100644
--- a/tests/test_tf_import.py
+++ b/tests/test_tf_import.py
@@ -1,7 +1,7 @@
 import sys
 import unittest
 
-from huggingface_hub.file_download import is_tf_available
+from huggingface_hub.utils import is_tf_available
 
 
 def require_tf(test_case):
diff --git a/tests/test_utils_headers.py b/tests/test_utils_headers.py
index b8cb1457..0c0f446e 100644
--- a/tests/test_utils_headers.py
+++ b/tests/test_utils_headers.py
@@ -1,14 +1,22 @@
 import unittest
 from unittest.mock import Mock, patch
 
-from huggingface_hub.utils._headers import build_hf_headers
+from huggingface_hub.utils import get_hf_hub_version, get_python_version
+from huggingface_hub.utils._headers import _http_user_agent, build_hf_headers
 
-from .testing_utils import handle_injection
+from .testing_utils import handle_injection, handle_injection_in_test
 
 
+# Only for tests that are not related to user agent
+DEFAULT_USER_AGENT = _http_user_agent()
+
 FAKE_TOKEN = "123456789"
 FAKE_TOKEN_ORG = "api_org_123456789"
-FAKE_TOKEN_HEADER = {"authorization": f"Bearer {FAKE_TOKEN}"}
+FAKE_TOKEN_HEADER = {
+    "authorization": f"Bearer {FAKE_TOKEN}",
+    "user-agent": DEFAULT_USER_AGENT,
+}
+NO_AUTH_HEADER = {"user-agent": DEFAULT_USER_AGENT}
 
 
 @patch("huggingface_hub.utils._headers.HfFolder")
@@ -28,11 +36,11 @@ class TestAuthHeadersUtil(unittest.TestCase):
 
     def test_use_auth_token_false(self, mock_HfFolder: Mock) -> None:
         mock_HfFolder().get_token.return_value = FAKE_TOKEN
-        self.assertEqual(build_hf_headers(use_auth_token=False), {})
+        self.assertEqual(build_hf_headers(use_auth_token=False), NO_AUTH_HEADER)
 
     def test_use_auth_token_none_no_cached_token(self, mock_HfFolder: Mock) -> None:
         mock_HfFolder().get_token.return_value = None
-        self.assertEqual(build_hf_headers(), {})
+        self.assertEqual(build_hf_headers(), NO_AUTH_HEADER)
 
     def test_use_auth_token_none_has_cached_token(self, mock_HfFolder: Mock) -> None:
         mock_HfFolder().get_token.return_value = FAKE_TOKEN
@@ -54,7 +62,7 @@ class TestAuthHeadersUtil(unittest.TestCase):
     @patch.dict("os.environ", {"HF_HUB_DISABLE_IMPLICIT_TOKEN": "1"})
     def test_implicit_use_disabled(self, mock_HfFolder: Mock) -> None:
         mock_HfFolder().get_token.return_value = FAKE_TOKEN
-        self.assertEqual(build_hf_headers(), {})  # token is not sent
+        self.assertEqual(build_hf_headers(), NO_AUTH_HEADER)  # token is not sent
 
     @patch.dict("os.environ", {"HF_HUB_DISABLE_IMPLICIT_TOKEN": "1"})
     def test_implicit_use_disabled_but_explicit_use(self, mock_HfFolder: Mock) -> None:
@@ -62,3 +70,80 @@ class TestAuthHeadersUtil(unittest.TestCase):
 
         # This is not an implicit use so we still send it
         self.assertEqual(build_hf_headers(use_auth_token=True), FAKE_TOKEN_HEADER)
+
+
+class TestUserAgentHeadersUtil(unittest.TestCase):
+    def _get_user_agent(self, **kwargs) -> str:
+        return build_hf_headers(**kwargs)["user-agent"]
+
+    @patch("huggingface_hub.utils._headers.get_fastai_version")
+    @patch("huggingface_hub.utils._headers.get_fastcore_version")
+    @patch("huggingface_hub.utils._headers.get_tf_version")
+    @patch("huggingface_hub.utils._headers.get_torch_version")
+    @patch("huggingface_hub.utils._headers.is_fastai_available")
+    @patch("huggingface_hub.utils._headers.is_fastcore_available")
+    @patch("huggingface_hub.utils._headers.is_tf_available")
+    @patch("huggingface_hub.utils._headers.is_torch_available")
+    @handle_injection_in_test
+    def test_default_user_agent(
+        self,
+        mock_get_fastai_version: Mock,
+        mock_get_fastcore_version: Mock,
+        mock_get_tf_version: Mock,
+        mock_get_torch_version: Mock,
+        mock_is_fastai_available: Mock,
+        mock_is_fastcore_available: Mock,
+        mock_is_tf_available: Mock,
+        mock_is_torch_available: Mock,
+    ) -> None:
+        mock_get_fastai_version.return_value = "fastai_version"
+        mock_get_fastcore_version.return_value = "fastcore_version"
+        mock_get_tf_version.return_value = "tf_version"
+        mock_get_torch_version.return_value = "torch_version"
+        mock_is_fastai_available.return_value = True
+        mock_is_fastcore_available.return_value = True
+        mock_is_tf_available.return_value = True
+        mock_is_torch_available.return_value = True
+        self.assertEqual(
+            self._get_user_agent(),
+            f"unknown/None; hf_hub/{get_hf_hub_version()};"
+            f" python/{get_python_version()}; torch/torch_version;"
+            " tensorflow/tf_version; fastai/fastai_version;"
+            " fastcore/fastcore_version",
+        )
+
+    @patch("huggingface_hub.utils._headers.is_torch_available")
+    @patch("huggingface_hub.utils._headers.is_tf_available")
+    @handle_injection_in_test
+    def test_user_agent_with_library_name_multiple_missing(
+        self, mock_is_torch_available: Mock, mock_is_tf_available: Mock
+    ) -> None:
+        mock_is_torch_available.return_value = False
+        mock_is_tf_available.return_value = False
+        self.assertNotIn("torch", self._get_user_agent())
+        self.assertNotIn("tensorflow", self._get_user_agent())
+
+    def test_user_agent_with_library_name_and_version(self) -> None:
+        self.assertTrue(
+            self._get_user_agent(
+                library_name="foo",
+                library_version="bar",
+            ).startswith("foo/bar;")
+        )
+
+    def test_user_agent_with_library_name_no_version(self) -> None:
+        self.assertTrue(
+            self._get_user_agent(library_name="foo").startswith("foo/None;")
+        )
+
+    def test_user_agent_with_custom_agent_string(self) -> None:
+        self.assertTrue(
+            self._get_user_agent(user_agent="this is a custom agent").endswith(
+                "this is a custom agent"
+            )
+        )
+
+    def test_user_agent_with_custom_agent_dict(self) -> None:
+        self.assertTrue(
+            self._get_user_agent(user_agent={"a": "b", "c": "d"}).endswith("a/b; c/d")
+        )