Repository: huggingface/huggingface_hub (python)

Issue:
HTTP-based `push_to_hub` for fastai integration
Helper functions `push_to_hub` for keras integration and `ModelHubMixin` mixin now uses an HTTP approach instead of a git-based one. This work as been discussed in https://github.com/huggingface/huggingface_hub/pull/321 and done in https://github.com/huggingface/huggingface_hub/pull/847. The main advantage is that the all repo doesn't have to be cloned locally before uploading the model weights.

However, `fastai` integration is still git-based (see implementation [here](https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/fastai_utils.py#L352)). It would be good to update `push_to_hub_fastai` to use the new http-based approach.

Pull request description:
Add changes for push_to_hub_fastai to use the new http-based approach.
Fixes #996 

This PR updates the fastai integration from git-based to the new http-based approach.

Who can review:
@Wauplin 

Patch:
diff --git a/src/huggingface_hub/fastai_utils.py b/src/huggingface_hub/fastai_utils.py
index dd44224c..26d02cdb 100644
--- a/src/huggingface_hub/fastai_utils.py
+++ b/src/huggingface_hub/fastai_utils.py
@@ -1,22 +1,23 @@
 import json
 import os
+import tempfile
 from pathlib import Path
 from pickle import DEFAULT_PROTOCOL, PicklingError
-from typing import Any, Dict, Optional
+from typing import Any, Dict, List, Optional, Union
 
 from packaging import version
 
-from huggingface_hub import snapshot_download
+from huggingface_hub import hf_api, snapshot_download
 from huggingface_hub.constants import CONFIG_NAME
 from huggingface_hub.file_download import (
     _PY_VERSION,
     get_fastai_version,
     get_fastcore_version,
 )
-from huggingface_hub.hf_api import HfApi, HfFolder
-from huggingface_hub.repository import Repository
+from huggingface_hub.hf_api import HfApi
 
 from .utils import logging, validate_hf_hub_args
+from .utils._deprecation import _deprecate_arguments, _deprecate_positional_args
 
 
 logger = logging.get_logger(__name__)
@@ -351,19 +352,50 @@ def from_pretrained_fastai(
     return load_learner(os.path.join(storage_folder, "model.pkl"))
 
 
+@_deprecate_positional_args(version="0.12")
+@_deprecate_arguments(
+    version="0.12",
+    deprecated_args={
+        "git_user",
+        "git_email",
+    },
+)
 @validate_hf_hub_args
 def push_to_hub_fastai(
+    # NOTE: New arguments since 0.9
     learner,
+    *,
     repo_id: str,
     commit_message: Optional[str] = "Add model",
     private: bool = False,
     token: Optional[str] = None,
     config: Optional[dict] = None,
-    **kwargs,
+    branch: Optional[str] = None,
+    create_pr: Optional[bool] = None,
+    allow_patterns: Optional[Union[List[str], str]] = None,
+    ignore_patterns: Optional[Union[List[str], str]] = None,
+    api_endpoint: Optional[str] = None,
+    # NOTE: deprecated signature that will change in 0.12
+    git_user: Optional[str] = None,
+    git_email: Optional[str] = None,
+    # TODO (release 0.12): signature must be the following
+    # learner,
+    # repo_id: Optional[str] = None,  # optional only until 0.12
+    # commit_message: Optional[str] = "Add model",
+    # private: Optional[bool] = None,
+    # token: Optional[str] = None,
+    # config: Optional[dict] = None,
+    # branch: Optional[str] = None,
+    # create_pr: Optional[bool] = None,
+    # allow_patterns: Optional[Union[List[str], str]] = None,
+    # ignore_patterns: Optional[Union[List[str], str]] = None,
+    # api_endpoint: Optional[str] = None,
 ):
     """
-    Upload learner checkpoint files to the Hub while synchronizing a local clone of the repo in
-    :obj:`repo_id`.
+    Upload learner checkpoint files to the Hub.
+
+    Use `allow_patterns` and `ignore_patterns` to precisely filter which files should be
+    pushed to the hub. See [`upload_folder`] reference for more details.
 
     Args:
         learner (`Learner`):
@@ -378,15 +410,19 @@ def push_to_hub_fastai(
             The Hugging Face account token to use as HTTP bearer authorization for remote files. If :obj:`None`, the token will be asked by a prompt.
         config (`dict`, *optional*):
             Configuration object to be saved alongside the model weights.
-
-    Keyword Args:
+        branch (`str`, *optional*):
+            The git branch on which to push the model. This defaults to
+            the default branch as specified in your repository, which
+            defaults to `"main"`.
+        create_pr (`boolean`, *optional*):
+            Whether or not to create a Pull Request from `branch` with that commit.
+            Defaults to `False`.
         api_endpoint (`str`, *optional*):
             The API endpoint to use when pushing the model to the hub.
-        git_user (`str`, *optional*):
-            Will override the ``git config user.name`` for committing and pushing files to the hub.
-        git_email (`str`, *optional*):
-            Will override the ``git config user.email`` for committing and pushing files to the hub.
-
+        allow_patterns (`List[str]` or `str`, *optional*):
+            If provided, only files matching at least one pattern are pushed.
+        ignore_patterns (`List[str]` or `str`, *optional*):
+            If provided, files matching any of the patterns are not pushed.
     Returns:
         The url of the commit of your model in the given repository.
 
@@ -401,41 +437,28 @@ def push_to_hub_fastai(
     """
 
     _check_fastai_fastcore_versions()
-
-    api_endpoint: str = kwargs.get("api_endpoint", None)
-    git_user: str = kwargs.get("git_user", None)
-    git_email: str = kwargs.get("git_email", None)
-
-    if token is None:
-        token = HfFolder.get_token()
-
-    if token is None:
-        raise ValueError(
-            "You must login to the Hugging Face Hub. There are two options: "
-            "(1) Type `huggingface-cli login` in your terminal and enter your token. "
-            "(2) Enter your token in the `token` argument. "
-            "Your token is available in the Settings of your Hugging Face account. "
-        )
-
-    # Create repo using `HfApi()`.
-    repo_url = HfApi(endpoint=api_endpoint).create_repo(
-        repo_id,
+    token, _ = hf_api._validate_or_retrieve_token(token)
+    api = HfApi(endpoint=api_endpoint)
+    api.create_repo(
+        repo_id=repo_id,
+        repo_type="model",
         token=token,
         private=private,
-        repo_type=None,
         exist_ok=True,
     )
 
-    # If repository exists in the Hugging Face Hub then clone it locally in `repo_id`.
-    repo = Repository(
-        repo_id,
-        clone_from=repo_url,
-        use_auth_token=token,
-        git_user=git_user,
-        git_email=git_email,
-    )
-    repo.git_pull(rebase=True)
-
-    _save_pretrained_fastai(learner, repo_id, config=config)
-
-    return repo.push_to_hub(commit_message=commit_message)
+    # Push the files to the repo in a single commit
+    with tempfile.TemporaryDirectory() as tmp:
+        saved_path = Path(tmp) / repo_id
+        _save_pretrained_fastai(learner, saved_path, config=config)
+        return api.upload_folder(
+            repo_id=repo_id,
+            repo_type="model",
+            token=token,
+            folder_path=saved_path,
+            commit_message=commit_message,
+            revision=branch,
+            create_pr=create_pr,
+            allow_patterns=allow_patterns,
+            ignore_patterns=ignore_patterns,
+        )
diff --git a/src/huggingface_hub/file_download.py b/src/huggingface_hub/file_download.py
index 13ae7d23..f00d3c42 100644
--- a/src/huggingface_hub/file_download.py
+++ b/src/huggingface_hub/file_download.py
@@ -170,6 +170,8 @@ def get_jinja_version():
     return _jinja_version
 
 
+# Return value when trying to load a file from cache but the file does not exist in the distant repo.
+_CACHED_NO_EXIST = object()
 REGEX_COMMIT_HASH = re.compile(r"^[0-9a-f]{40}$")
 
 
@@ -1318,8 +1320,25 @@ def try_to_load_from_cache(
 
     This function will not raise any exception if the file in not cached.
 
+    Args:
+        cache_dir (`str` or `os.PathLike`):
+            The folder where the cached files lie.
+        repo_id (`str`):
+            The ID of the repo on huggingface.co.
+        filename (`str`):
+            The filename to look for inside `repo_id`.
+        revision (`str`, *optional*):
+            The specific model version to use. Will default to `"main"` if it's not provided and no `commit_hash` is
+            provided either.
+        repo_type (`str`, *optional*):
+            The type of the repository. Will default to `"model"`.
+
     Returns:
-        Local path (string) of file or `None` if no cached file is found.
+        `Optional[str]` or `_CACHED_NO_EXIST`:
+            Will return `None` if the file was not cached. Otherwise:
+            - The exact path to the cached file if it's found in the cache
+            - A special value `_CACHED_NO_EXIST` if the file does not exist at the given commit hash and this fact was
+              cached.
     """
     if revision is None:
         revision = "main"
@@ -1348,6 +1367,9 @@ def try_to_load_from_cache(
         with open(os.path.join(repo_cache, "refs", revision)) as f:
             revision = f.read()
 
+    if os.path.isfile(os.path.join(repo_cache, ".no_exist", revision, filename)):
+        return _CACHED_NO_EXIST
+
     cached_shas = os.listdir(os.path.join(repo_cache, "snapshots"))
     if revision not in cached_shas:
         # No cache for this revision and we won't try to return a random revision


Test patch:
diff --git a/tests/test_file_download.py b/tests/test_file_download.py
index 50ce54d5..13ae0e9d 100644
--- a/tests/test_file_download.py
+++ b/tests/test_file_download.py
@@ -25,6 +25,7 @@ from huggingface_hub.constants import (
     REPO_TYPE_DATASET,
 )
 from huggingface_hub.file_download import (
+    _CACHED_NO_EXIST,
     cached_download,
     filename_to_url,
     hf_hub_download,
@@ -320,3 +321,19 @@ class CachedDownloadTests(unittest.TestCase):
         )
         # Same for uncached models
         self.assertIsNone(try_to_load_from_cache("bert-base", filename=CONFIG_NAME))
+
+    def test_try_to_load_from_cache_no_exist(self):
+        # Make sure the file is cached
+        with self.assertRaises(EntryNotFoundError):
+            _ = hf_hub_download(DUMMY_MODEL_ID, filename="dummy")
+
+        new_file_path = try_to_load_from_cache(DUMMY_MODEL_ID, filename="dummy")
+        self.assertEqual(new_file_path, _CACHED_NO_EXIST)
+
+        new_file_path = try_to_load_from_cache(
+            DUMMY_MODEL_ID, filename="dummy", revision="main"
+        )
+        self.assertEqual(new_file_path, _CACHED_NO_EXIST)
+
+        # If file non-existence is not cached, returns None
+        self.assertIsNone(try_to_load_from_cache(DUMMY_MODEL_ID, filename="dummy2"))