Repository: huggingface/huggingface_hub
Pull request #1779 (2023-10-25 14:59:52)

Implement API for Inference Endpoints
Implement https://github.com/huggingface/huggingface_hub/issues/1541 (+fix https://github.com/huggingface/huggingface_hub/issues/1605).

Ping @philschmid @jeffboudier Feedback is very welcomed if you see anything that can be improved product-wise :)

**EDIT / TL;DR:** [here is the guide](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_1779/en/guides/inference_endpoints) written in this PR.

---

This PR adds support for Inference Endpoints, following the [Swagger API docs](https://api.endpoints.huggingface.cloud/).

- **HfApi methods**:
  - [list_inference_endpoints](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_1779/en/package_reference/hf_api#huggingface_hub.HfApi.list_inference_endpoints)
  - [create_inference_endpoint](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_1779/en/package_reference/hf_api#huggingface_hub.HfApi.create_inference_endpoint)
  - [get_inference_endpoint](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_1779/en/package_reference/hf_api#huggingface_hub.HfApi.get_inference_endpoint)
  - [update_inference_endpoint](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_1779/en/package_reference/hf_api#huggingface_hub.HfApi.update_inference_endpoint)
  - [delete_inference_endpoint](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_1779/en/package_reference/hf_api#huggingface_hub.HfApi.delete_inference_endpoint)
  - [pause_inference_endpoint](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_1779/en/package_reference/hf_api#huggingface_hub.HfApi.pause_inference_endpoint)
  - [resume_inference_endpoint](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_1779/en/package_reference/hf_api#huggingface_hub.HfApi.resume_inference_endpoint)
  - [scale_to_zero_inference_endpoint](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_1779/en/package_reference/hf_api#huggingface_hub.HfApi.scale_to_zero_inference_endpoint)

- **InferenceEndpoint object + methods:**
  - `client` / `async_client` => return `InferenceClient` object
  - `wait()` => wait until fully deployed
  - `update()` => alias for `update_inference_endpoint`
  - `resume()` => alias for `resume_inference_endpoint`
  - `pause()` => alias for `pause_inference_endpoint`
  - `scale_to_zero()` => alias for `scale_to_zero_inference_endpoint`
  - `delete()` => alias for `delete_inference_endpoint`

I intentionally did not implement metrics and logs endpoints yet as I don't think that should be a priority.

Listing/getting inference endpoints information is quite straightforward, given the user namespace or endpoint name. Same goes for resume/pause/scale_to_zero feature which AFAIK would be the most useful ones in scripts. Creating and updating endpoints is more difficult as the user needs to know exactly the configuration they want to use.

The main object returned by most methods is an `InferenceEndpoint` dataclass with useful information like name, status, url, model, framework, task, created_at/updated_at,... It also has 2 properties `.client` and `.async_client` to run inference.

Regarding testing the API, I am not sure we can and we want to do that in the CI. Like for the Spaces API, it's quite hard (and costly) to do end-to-end tests on production and for little benefit IMO (as we can assume v2 endpoints will not be updated). I ran some tests locally to make sure everything runs as expected though.

---

Example:

```py
>>> from huggingface_hub import create_inference_endpoint

# Create endpoint
>>> endpoint = create_inference_endpoint(
...     "my-endpoint-name6",
...     repository="gpt2",
...     framework="pytorch",
...     task="text-generation",
...     accelerator="cpu",
...     vendor="aws",
...     region="us-east-1",
...     type="protected",
...     instance_size="medium",
...     instance_type="c6i"
... )

# pending creation => no url => cannot get client
>>> endpoint
InferenceEndpoint(name='my-endpoint-name6', namespace='Wauplin', repository='gpt2', status='pending', url=None)
>>> endpoint.client 
*** huggingface_hub._inference_endpoints.InferenceEndpointException: Cannot create a client for this endpoint as it is not yet deployed. Please wait for the endpoint to be deployed and try again.

# ... wait until it's initialized
>>> endpoint.wait()
InferenceEndpoint(name='my-endpoint-name6', namespace='Wauplin', repository='gpt2', status='running', url='https://kqehm5t0lfe628b2.us-east-1.aws.endpoints.huggingface.cloud')

# endpoint running => url => client available
>>> endpoint.client 
<InferenceClient(model='https://kqehm5t0lfe628b2.us-east-1.aws.endpoints.huggingface.cloud', timeout=None)>
>>> endpoint.client.text_generation("I am")
' not a fan of the idea of a "big-budget" movie. I think it\'s a'

# pause endpoint => no more url
>>> endpoint.pause()
InferenceEndpoint(name='my-endpoint-name6', namespace='Wauplin', repository='gpt2', status='paused', url=None)
```

---

Useful resources:
- Generated documentation from this PR:
  - [guide](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_1779/en/guides/inference_endpoints)
  - [package_reference](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_1779/en/package_reference/inference_endpoints)
- Swagger API docs: https://api.endpoints.huggingface.cloud/
- Inference Endpoints: https://ui.endpoints.huggingface.co/
- Inference Endpoints documentation: https://huggingface.co/docs/inference-endpoints/index

HuggingFaceDocBuilderDev: The docs for this PR live [here](https://moon-ci-docs.huggingface.co/docs/huggingface_hub/pr_1779). All of your documentation changes will be reflected on that endpoint.
Wauplin: @philschmid @stevhliu I have added [a guide](https://github.com/huggingface/huggingface_hub/pull/1779/files#diff-c5362eb24f1ddc2bd0d8f160318a468076e997c5f9f979bda55c7663832caa40) about how to manage Inference Endpoints from `huggingface_hub`. Would you mind having a look at it? :pray: 
Wauplin: Thanks @stevhliu and @McPatate for your feedback on the documentation, It helped a lot! I've made the requested changes :)

Patch:
diff --git a/.github/workflows/python-quality.yml b/.github/workflows/python-quality.yml
index 283f7a82..24e3cc21 100644
--- a/.github/workflows/python-quality.yml
+++ b/.github/workflows/python-quality.yml
@@ -27,7 +27,6 @@ jobs:
         run: |
           pip install --upgrade pip
           pip install .[dev]
-      - run: black --check tests src contrib
       - run: ruff tests src contrib
       - run: python utils/check_contrib_list.py
       - run: python utils/check_static_imports.py
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7963b172..e2496608 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -8,11 +8,7 @@ repos:
       - id: trailing-whitespace
       - id: check-case-conflict
       - id: check-merge-conflict
-  - repo: https://github.com/psf/black
-    rev: 23.1.0
-    hooks:
-      - id: black
   - repo: https://github.com/charliermarsh/ruff-pre-commit # https://github.com/charliermarsh/ruff#usage
-    rev: 'v0.0.243'
+    rev: 'v0.1.3'
     hooks:
       - id: ruff
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index eef1a379..20b1bb05 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -145,9 +145,8 @@ Follow these steps to start contributing:
 
 7. Format your code.
 
-   `hugginface_hub` relies on [`black`](https://github.com/psf/black) and [`ruff`](https://github.com/astral-sh/ruff)
-   to format its source code consistently. You can apply automatic style corrections and code verifications
-   with the following command:
+   `hugginface_hub` relies on [`ruff`](https://github.com/astral-sh/ruff) to format its source code consistently. You
+   can apply automatic style corrections and code verifications with the following command:
 
    ```bash
    $ make style
diff --git a/Makefile b/Makefile
index bed03fef..a03259d7 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,6 @@ check_dirs := contrib src tests utils setup.py
 
 
 quality:
-	black --check $(check_dirs)
 	ruff $(check_dirs)
 	mypy src
 	python utils/check_contrib_list.py
@@ -13,8 +12,7 @@ quality:
 	python utils/generate_async_inference_client.py
 
 style:
-	black $(check_dirs)
-	ruff $(check_dirs) --fix
+	ruff --fix $(check_dirs)
 	python utils/check_contrib_list.py --update
 	python utils/check_static_imports.py --update
 	python utils/generate_async_inference_client.py --update
diff --git a/docs/source/en/_toctree.yml b/docs/source/en/_toctree.yml
index 632bbf50..814037df 100644
--- a/docs/source/en/_toctree.yml
+++ b/docs/source/en/_toctree.yml
@@ -22,6 +22,8 @@
       title: Search
     - local: guides/inference
       title: Inference
+    - local: guides/inference_endpoints
+      title: Inference Endpoints
     - local: guides/community
       title: Community Tab
     - local: guides/collections
@@ -58,6 +60,8 @@
       title: Mixins & serialization methods
     - local: package_reference/inference_client
       title: Inference Client
+    - local: package_reference/inference_endpoints
+      title: Inference Endpoints
     - local: package_reference/hf_file_system
       title: HfFileSystem
     - local: package_reference/utilities
diff --git a/docs/source/en/guides/inference_endpoints.md b/docs/source/en/guides/inference_endpoints.md
new file mode 100644
index 00000000..a377c408
--- /dev/null
+++ b/docs/source/en/guides/inference_endpoints.md
@@ -0,0 +1,226 @@
+# Inference Endpoints
+
+Inference Endpoints provides a secure production solution to easily deploy any `transformers`, `sentence-transformers`, and `diffusers` models on a dedicated and autoscaling infrastructure managed by Hugging Face. An Inference Endpoint is built from a model from the [Hub](https://huggingface.co/models). 
+In this guide, we will learn how to programmatically manage Inference Endpoints with `huggingface_hub`. For more information about the Inference Endpoints product itself, check out its [official documentation](https://huggingface.co/docs/inference-endpoints/index).
+
+This guide assumes `huggingface_hub` is correctly installed and that your machine is logged in. Check out the [Quick Start guide](https://huggingface.co/docs/huggingface_hub/quick-start#quickstart) if that's not the case yet. The minimal version supporting Inference Endpoints API is `v0.19.0`.
+
+
+## Create an Inference Endpoint
+
+The first step is to create an Inference Endpoint using [`create_inference_endpoint`]:
+
+```py
+>>> from huggingface_hub import create_inference_endpoint
+
+>>> endpoint = create_inference_endpoint(
+...     "my-endpoint-name",
+...     repository="gpt2",
+...     framework="pytorch",
+...     task="text-generation",
+...     accelerator="cpu",
+...     vendor="aws",
+...     region="us-east-1",
+...     type="protected",
+...     instance_size="medium",
+...     instance_type="c6i"
+... )
+```
+
+In this example, we created a `protected` Inference Endpoint named `"my-endpoint-name"`, to serve [gpt2](https://huggingface.co/gpt2) for `text-generation`. A `protected` Inference Endpoint means your token is required to access the API. We also need to provide additional information to configure the hardware requirements, such as vendor, region, accelerator, instance type, and size. You can check out the list of available resources [here](https://api.endpoints.huggingface.cloud/#/v2%3A%3Aprovider/list_vendors). Alternatively, you can create an Inference Endpoint manually using the [Web interface](https://ui.endpoints.huggingface.co/new) for convenience. Refer to this [guide](https://huggingface.co/docs/inference-endpoints/guides/advanced) for details on advanced settings and their usage.
+
+The value returned by [`create_inference_endpoint`] is an [`InferenceEndpoint`] object:
+
+```py
+>>> endpoint
+InferenceEndpoint(name='my-endpoint-name', namespace='Wauplin', repository='gpt2', status='pending', url=None)
+```
+
+It's a dataclass that holds information about the endpoint. You can access important attributes such as `name`, `repository`, `status`, `task`, `created_at`, `updated_at`, etc. If you need it, you can also access the raw response from the server with `endpoint.raw`.
+
+Once your Inference Endpoint is created, you can find it on your [personal dashboard](https://ui.endpoints.huggingface.co/).
+
+![](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/huggingface_hub/inference_endpoints_created.png)
+
+### Get or list existing Inference Endpoints
+
+In some cases, you might need to manage Inference Endpoints you created previously. If you know the name, you can fetch it using [`get_inference_endpoint`], which returns an [`InferenceEndpoint`] object. Alternatively, you can use [`list_inference_endpoints`] to retrieve a list of all Inference Endpoints. Both methods accept an optional `namespace` parameter. You can set the `namespace` to any organization you are a part of. Otherwise, it defaults to your username.
+
+```py
+>>> from huggingface_hub import get_inference_endpoint, list_inference_endpoints
+
+# Get one
+>>> get_inference_endpoint("my-endpoint-name")
+InferenceEndpoint(name='my-endpoint-name', namespace='Wauplin', repository='gpt2', status='pending', url=None)
+
+# List all endpoints from an organization
+>>> list_inference_endpoints(namespace="huggingface")
+[InferenceEndpoint(name='aws-starchat-beta', namespace='huggingface', repository='HuggingFaceH4/starchat-beta', status='paused', url=None), ...]
+
+# List all endpoints from all organizations the user belongs to
+>>> list_inference_endpoints(namespace="*")
+[InferenceEndpoint(name='aws-starchat-beta', namespace='huggingface', repository='HuggingFaceH4/starchat-beta', status='paused', url=None), ...]
+```
+
+## Check deployment status
+
+In the rest of this guide, we will assume that we have a [`InferenceEndpoint`] object called `endpoint`. You might have noticed that the endpoint has a `status` attribute of type [`InferenceEndpointStatus`]. When the Inference Endpoint is deployed and accessible, the status should be `"running"` and the `url` attribute is set:
+
+```py
+>>> endpoint
+InferenceEndpoint(name='my-endpoint-name', namespace='Wauplin', repository='gpt2', status='running', url='https://jpj7k2q4j805b727.us-east-1.aws.endpoints.huggingface.cloud')
+```
+
+Before reaching a `"running"` state, the Inference Endpoint typically goes through an `"initializing"` or `"pending"` phase. You can fetch the new state of the endpoint by running [`~InferenceEndpoint.fetch`]. Like every other method from [`InferenceEndpoint`] that makes a request to the server, the internal attributes of `endpoint` are mutated in place:
+
+```py
+>>> endpoint.fetch()
+InferenceEndpoint(name='my-endpoint-name', namespace='Wauplin', repository='gpt2', status='pending', url=None)
+```
+
+Instead of fetching the Inference Endpoint status while waiting for it to run, you can directly call [`~InferenceEndpoint.wait`]. This helper takes as input a `timeout` and a `fetch_every` parameter (in seconds) and will block the thread until the Inference Endpoint is deployed. Default values are respectively `None` (no timeout) and `5` seconds.
+
+```py
+# Pending endpoint
+>>> endpoint
+InferenceEndpoint(name='my-endpoint-name', namespace='Wauplin', repository='gpt2', status='pending', url=None)
+
+# Wait 10s => raises a InferenceEndpointTimeoutError
+>>> endpoint.wait(timeout=10)
+    raise InferenceEndpointTimeoutError("Timeout while waiting for Inference Endpoint to be deployed.")
+huggingface_hub._inference_endpoints.InferenceEndpointTimeoutError: Timeout while waiting for Inference Endpoint to be deployed.
+
+# Wait more
+>>> endpoint.wait()
+InferenceEndpoint(name='my-endpoint-name', namespace='Wauplin', repository='gpt2', status='running', url='https://jpj7k2q4j805b727.us-east-1.aws.endpoints.huggingface.cloud')
+```
+
+If `timeout` is set and the Inference Endpoint takes too much time to load, a [`InferenceEndpointTimeoutError`] timeout error is raised.
+
+## Run inference
+
+Once your Inference Endpoint is up and running, you can finally run inference on it! 
+
+[`InferenceEndpoint`] has two properties `client` and `async_client` returning respectively an [`InferenceClient`] and an [`AsyncInferenceClient`] objects.
+
+```py
+# Run text_generation task:
+>>> endpoint.client.text_generation("I am")
+' not a fan of the idea of a "big-budget" movie. I think it\'s a'
+
+# Or in an asyncio context:
+>>> await endpoint.async_client.text_generation("I am")
+```
+
+If the Inference Endpoint is not running, an [`InferenceEndpointError`] exception is raised:
+
+```py
+>>> endpoint.client
+huggingface_hub._inference_endpoints.InferenceEndpointError: Cannot create a client for this Inference Endpoint as it is not yet deployed. Please wait for the Inference Endpoint to be deployed using `endpoint.wait()` and try again.
+```
+
+For more details about how to use the [`InferenceClient`], check out the [Inference guide](../guides/inference).
+
+## Manage lifecycle
+
+Now that we saw how to create an Inference Endpoint and run inference on it, let's see how to manage its lifecycle.
+
+<Tip>
+
+In this section, we will see methods like [`~InferenceEndpoint.pause`], [`~InferenceEndpoint.resume`], [`~InferenceEndpoint.scale_to_zero`], [`~InferenceEndpoint.update`] and [`~InferenceEndpoint.delete`]. All of those methods are aliases added to [`InferenceEndpoint`] for convenience. If you prefer, you can also use the generic methods defined in `HfApi`: [`pause_inference_endpoint`], [`resume_inference_endpoint`], [`scale_to_zero_inference_endpoint`], [`update_inference_endpoint`], and [`delete_inference_endpoint`].
+
+</Tip>
+
+## Pause or scale to zero
+
+To reduce costs when your Inference Endpoint is not in use, you can choose to either pause it using [`~InferenceEndpoint.pause`] or scale it to zero using [`~InferenceEndpoint.scale_to_zero`].
+
+<Tip>
+
+An Inference Endpoint that is *paused* or *scaled to zero* doesn't cost anything. The difference between those two is that a *paused* endpoint needs to be explicitly *resumed* using [`~InferenceEndpoint.resume`]. On the contrary, a *scaled to zero* endpoint will automatically start if an inference call is made to it, with an additional cold start delay. An Inference Endpoint can also be configured to scale to zero automatically after a certain period of inactivity.
+
+</Tip>
+
+```py
+# Pause and resume endpoint
+>>> endpoint.pause()
+InferenceEndpoint(name='my-endpoint-name', namespace='Wauplin', repository='gpt2', status='paused', url=None)
+>>> endpoint.resume()
+InferenceEndpoint(name='my-endpoint-name', namespace='Wauplin', repository='gpt2', status='pending', url=None)
+>>> endpoint.wait().client.text_generation(...)
+...
+
+# Scale to zero
+>>> endpoint.scale_to_zero()
+InferenceEndpoint(name='my-endpoint-name', namespace='Wauplin', repository='gpt2', status='scaledToZero', url='https://jpj7k2q4j805b727.us-east-1.aws.endpoints.huggingface.cloud')
+# Endpoint is not 'running' but still has a URL and will restart on first call.
+```
+
+## Update model or hardware requirements
+
+In some cases, you might also want to update your Inference Endpoint without creating a new one. You can either update the hosted model or the hardware requirements to run the model. You can do this using [`~InferenceEndpoint.update`]:
+
+```py
+# Change target model
+>>> endpoint.update(repository="gpt2-large")
+InferenceEndpoint(name='my-endpoint-name', namespace='Wauplin', repository='gpt2-large', status='pending', url=None)
+
+# Update number of replicas
+>>> endpoint.update(min_replica=2, max_replica=6)
+InferenceEndpoint(name='my-endpoint-name', namespace='Wauplin', repository='gpt2-large', status='pending', url=None)
+
+# Update to larger instance
+>>> endpoint.update(accelerator="cpu", instance_size="large", instance_type="c6i")
+InferenceEndpoint(name='my-endpoint-name', namespace='Wauplin', repository='gpt2-large', status='pending', url=None)
+```
+
+## Delete the endpoint
+
+Finally if you won't use the Inference Endpoint anymore, you can simply call [`~InferenceEndpoint.delete()`].
+
+<Tip warning={true}>
+
+This is a non-revertible action that will completely remove the endpoint, including its configuration, logs and usage metrics. You cannot restore a deleted Inference Endpoint.
+
+</Tip>
+
+
+## An end-to-end example
+
+A typical use case of Inference Endpoints is to process a batch of jobs at once to limit the infrastructure costs. You can automate this process using what we saw in this guide:
+
+```py
+>>> import asyncio
+>>> from huggingface_hub import create_inference_endpoint
+
+# Start endpoint + wait until initialized
+>>> endpoint = create_inference_endpoint(name="batch-endpoint",...).wait()
+
+# Run inference
+>>> client = endpoint.client
+>>> results = [client.text_generation(...) for job in jobs]
+
+# Or with asyncio
+>>> async_client = endpoint.async_client
+>>> results = asyncio.gather(*[async_client.text_generation(...) for job in jobs])
+
+# Pause endpoint
+>>> endpoint.pause()
+```
+
+Or if your Inference Endpoint already exists and is paused:
+
+```py
+>>> import asyncio
+>>> from huggingface_hub import get_inference_endpoint
+
+# Get endpoint + wait until initialized
+>>> endpoint = get_inference_endpoint("batch-endpoint").resume().wait()
+
+# Run inference
+>>> async_client = endpoint.async_client
+>>> results = asyncio.gather(*[async_client.text_generation(...) for job in jobs])
+
+# Pause endpoint
+>>> endpoint.pause()
+```
\ No newline at end of file
diff --git a/docs/source/en/package_reference/inference_endpoints.md b/docs/source/en/package_reference/inference_endpoints.md
new file mode 100644
index 00000000..37521f8b
--- /dev/null
+++ b/docs/source/en/package_reference/inference_endpoints.md
@@ -0,0 +1,42 @@
+# Inference Endpoints
+
+Inference Endpoints provides a secure production solution to easily deploy models on a dedicated and autoscaling infrastructure managed by Hugging Face. An Inference Endpoint is built from a model from the [Hub](https://huggingface.co/models). This page is a reference for `huggingface_hub`'s integration with Inference Endpoints. For more information about the Inference Endpoints product, check out its [official documentation](https://huggingface.co/docs/inference-endpoints/index).
+
+<Tip>
+
+Check out the [related guide](../guides/inference_endpoints) to learn how to use `huggingface_hub` to manage your Inference Endpoints programmatically.
+
+</Tip>
+
+Inference Endpoints can be fully managed via API. The endpoints are documented with [Swagger](https://api.endpoints.huggingface.cloud/). The [`InferenceEndpoint`] class is a simple wrapper built on top on this API.
+
+## Methods
+
+A subset of the Inference Endpoint features are implemented in [`HfApi`]:
+
+- [`get_inference_endpoint`] and [`list_inference_endpoints`] to get information about your Inference Endpoints
+- [`create_inference_endpoint`], [`update_inference_endpoint`] and [`delete_inference_endpoint`] to deploy and manage Inference Endpoints
+- [`pause_inference_endpoint`] and [`resume_inference_endpoint`] to pause and resume an Inference Endpoint
+- [`scale_to_zero_inference_endpoint`] to manually scale an Endpoint to 0 replicas
+
+## InferenceEndpoint
+
+The main dataclass is [`InferenceEndpoint`]. It contains information about a deployed `InferenceEndpoint`, including its configuration and current state. Once deployed, you can run inference on the Endpoint using the  [`InferenceEndpoint.client`] and [`InferenceEndpoint.async_client`] properties that respectively return an [`InferenceClient`] and an [`AsyncInferenceClient`] object.
+
+[[autodoc]] InferenceEndpoint
+  - from_raw
+  - client
+  - async_client
+  - all
+
+## InferenceEndpointStatus
+
+[[autodoc]] InferenceEndpointStatus
+
+## InferenceEndpointType
+
+[[autodoc]] InferenceEndpointType
+
+## InferenceEndpointError
+
+[[autodoc]] InferenceEndpointError
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index e21bd2d4..877e628c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,8 +1,3 @@
-[tool.black]
-line-length = 119
-target_version = ['py37', 'py38', 'py39', 'py310']
-preview = true
-
 [tool.mypy]
 ignore_missing_imports = true
 no_implicit_optional = true
@@ -14,11 +9,31 @@ plugins = [
 [tool.ruff]
 # Ignored rules:
 #   "E501" -> line length violation
-#   "F821" -> undefined named in type annotation (e.g. Literal["something"])
-ignore = ["E501", "F821"]
+ignore = ["E501"]
 select = ["E", "F", "I", "W"]
 line-length = 119
+exclude = [
+    ".eggs",
+    ".git",
+    ".git-rewrite",
+    ".hg",
+    ".mypy_cache",
+    ".nox",
+    ".pants.d",
+    ".pytype",
+    ".ruff_cache",
+    ".svn",
+    ".tox",
+    ".venv",
+    ".venv*",
+    "__pypackages__",
+    "_build",
+    "build",
+    "dist",
+    "venv",
+]
 
 [tool.ruff.isort]
 lines-after-imports = 2
 known-first-party = ["huggingface_hub"]
+
diff --git a/setup.py b/setup.py
index cfba9e05..be98b92c 100644
--- a/setup.py
+++ b/setup.py
@@ -80,8 +80,7 @@ extras["typing"] = [
 ]
 
 extras["quality"] = [
-    "black==23.7",
-    "ruff>=0.0.241",
+    "ruff>=0.1.3",
     "mypy==1.5.1",
 ]
 
diff --git a/src/huggingface_hub/__init__.py b/src/huggingface_hub/__init__.py
index 35d912d8..d5a1801c 100644
--- a/src/huggingface_hub/__init__.py
+++ b/src/huggingface_hub/__init__.py
@@ -55,6 +55,13 @@ _SUBMOD_ATTRS = {
     "_commit_scheduler": [
         "CommitScheduler",
     ],
+    "_inference_endpoints": [
+        "InferenceEndpoint",
+        "InferenceEndpointError",
+        "InferenceEndpointStatus",
+        "InferenceEndpointTimeoutError",
+        "InferenceEndpointType",
+    ],
     "_login": [
         "interpreter_login",
         "login",
@@ -153,6 +160,7 @@ _SUBMOD_ATTRS = {
         "create_commit",
         "create_commits_on_pr",
         "create_discussion",
+        "create_inference_endpoint",
         "create_pull_request",
         "create_repo",
         "create_tag",
@@ -162,6 +170,7 @@ _SUBMOD_ATTRS = {
         "delete_collection_item",
         "delete_file",
         "delete_folder",
+        "delete_inference_endpoint",
         "delete_repo",
         "delete_space_secret",
         "delete_space_storage",
@@ -174,6 +183,7 @@ _SUBMOD_ATTRS = {
         "get_dataset_tags",
         "get_discussion_details",
         "get_full_repo_name",
+        "get_inference_endpoint",
         "get_model_tags",
         "get_repo_discussions",
         "get_space_runtime",
@@ -182,6 +192,7 @@ _SUBMOD_ATTRS = {
         "like",
         "list_datasets",
         "list_files_info",
+        "list_inference_endpoints",
         "list_liked_repos",
         "list_metrics",
         "list_models",
@@ -193,6 +204,7 @@ _SUBMOD_ATTRS = {
         "merge_pull_request",
         "model_info",
         "move_repo",
+        "pause_inference_endpoint",
         "pause_space",
         "preupload_lfs_files",
         "rename_discussion",
@@ -202,13 +214,16 @@ _SUBMOD_ATTRS = {
         "request_space_hardware",
         "request_space_storage",
         "restart_space",
+        "resume_inference_endpoint",
         "run_as_future",
+        "scale_to_zero_inference_endpoint",
         "set_space_sleep_time",
         "space_info",
         "super_squash_history",
         "unlike",
         "update_collection_item",
         "update_collection_metadata",
+        "update_inference_endpoint",
         "update_repo_visibility",
         "upload_file",
         "upload_folder",
@@ -378,6 +393,13 @@ __getattr__, __dir__, __all__ = _attach(__name__, submodules=[], submod_attrs=_S
 # ```
 if TYPE_CHECKING:  # pragma: no cover
     from ._commit_scheduler import CommitScheduler  # noqa: F401
+    from ._inference_endpoints import (
+        InferenceEndpoint,  # noqa: F401
+        InferenceEndpointError,  # noqa: F401
+        InferenceEndpointStatus,  # noqa: F401
+        InferenceEndpointTimeoutError,  # noqa: F401
+        InferenceEndpointType,  # noqa: F401
+    )
     from ._login import (
         interpreter_login,  # noqa: F401
         login,  # noqa: F401
@@ -472,6 +494,7 @@ if TYPE_CHECKING:  # pragma: no cover
         create_commit,  # noqa: F401
         create_commits_on_pr,  # noqa: F401
         create_discussion,  # noqa: F401
+        create_inference_endpoint,  # noqa: F401
         create_pull_request,  # noqa: F401
         create_repo,  # noqa: F401
         create_tag,  # noqa: F401
@@ -481,6 +504,7 @@ if TYPE_CHECKING:  # pragma: no cover
         delete_collection_item,  # noqa: F401
         delete_file,  # noqa: F401
         delete_folder,  # noqa: F401
+        delete_inference_endpoint,  # noqa: F401
         delete_repo,  # noqa: F401
         delete_space_secret,  # noqa: F401
         delete_space_storage,  # noqa: F401
@@ -493,6 +517,7 @@ if TYPE_CHECKING:  # pragma: no cover
         get_dataset_tags,  # noqa: F401
         get_discussion_details,  # noqa: F401
         get_full_repo_name,  # noqa: F401
+        get_inference_endpoint,  # noqa: F401
         get_model_tags,  # noqa: F401
         get_repo_discussions,  # noqa: F401
         get_space_runtime,  # noqa: F401
@@ -501,6 +526,7 @@ if TYPE_CHECKING:  # pragma: no cover
         like,  # noqa: F401
         list_datasets,  # noqa: F401
         list_files_info,  # noqa: F401
+        list_inference_endpoints,  # noqa: F401
         list_liked_repos,  # noqa: F401
         list_metrics,  # noqa: F401
         list_models,  # noqa: F401
@@ -512,6 +538,7 @@ if TYPE_CHECKING:  # pragma: no cover
         merge_pull_request,  # noqa: F401
         model_info,  # noqa: F401
         move_repo,  # noqa: F401
+        pause_inference_endpoint,  # noqa: F401
         pause_space,  # noqa: F401
         preupload_lfs_files,  # noqa: F401
         rename_discussion,  # noqa: F401
@@ -521,13 +548,16 @@ if TYPE_CHECKING:  # pragma: no cover
         request_space_hardware,  # noqa: F401
         request_space_storage,  # noqa: F401
         restart_space,  # noqa: F401
+        resume_inference_endpoint,  # noqa: F401
         run_as_future,  # noqa: F401
+        scale_to_zero_inference_endpoint,  # noqa: F401
         set_space_sleep_time,  # noqa: F401
         space_info,  # noqa: F401
         super_squash_history,  # noqa: F401
         unlike,  # noqa: F401
         update_collection_item,  # noqa: F401
         update_collection_metadata,  # noqa: F401
+        update_inference_endpoint,  # noqa: F401
         update_repo_visibility,  # noqa: F401
         upload_file,  # noqa: F401
         upload_folder,  # noqa: F401
diff --git a/src/huggingface_hub/_inference_endpoints.py b/src/huggingface_hub/_inference_endpoints.py
new file mode 100644
index 00000000..c1b44439
--- /dev/null
+++ b/src/huggingface_hub/_inference_endpoints.py
@@ -0,0 +1,348 @@
+import time
+from dataclasses import dataclass, field
+from datetime import datetime
+from enum import Enum
+from typing import TYPE_CHECKING, Dict, Optional
+
+from .inference._client import InferenceClient
+from .inference._generated._async_client import AsyncInferenceClient
+from .utils import logging, parse_datetime
+
+
+if TYPE_CHECKING:
+    from .hf_api import HfApi
+
+
+logger = logging.get_logger(__name__)
+
+
+class InferenceEndpointError(Exception):
+    """Generic exception when dealing with Inference Endpoints."""
+
+
+class InferenceEndpointTimeoutError(InferenceEndpointError, TimeoutError):
+    """Exception for timeouts while waiting for Inference Endpoint."""
+
+
+class InferenceEndpointStatus(str, Enum):
+    PENDING = "pending"
+    INITIALIZING = "initializing"
+    UPDATING = "updating"
+    UPDATE_FAILED = "updateFailed"
+    RUNNING = "running"
+    PAUSED = "paused"
+    FAILED = "failed"
+    SCALED_TO_ZERO = "scaledToZero"
+
+
+class InferenceEndpointType(str, Enum):
+    PUBlIC = "public"
+    PROTECTED = "protected"
+    PRIVATE = "private"
+
+
+@dataclass
+class InferenceEndpoint:
+    """
+    Contains information about a deployed Inference Endpoint.
+
+    Args:
+        name (`str`):
+            The unique name of the Inference Endpoint.
+        namespace (`str`):
+            The namespace where the Inference Endpoint is located.
+        repository (`str`):
+            The name of the model repository deployed on this Inference Endpoint.
+        status ([`InferenceEndpointStatus`]):
+            The current status of the Inference Endpoint.
+        url (`str`, *optional*):
+            The URL of the Inference Endpoint, if available. Only a deployed Inference Endpoint will have a URL.
+        framework (`str`):
+            The machine learning framework used for the model.
+        revision (`str`):
+            The specific model revision deployed on the Inference Endpoint.
+        task (`str`):
+            The task associated with the deployed model.
+        created_at (`datetime.datetime`):
+            The timestamp when the Inference Endpoint was created.
+        updated_at (`datetime.datetime`):
+            The timestamp of the last update of the Inference Endpoint.
+        type ([`InferenceEndpointType`]):
+            The type of the Inference Endpoint (public, protected, private).
+        raw (`Dict`):
+            The raw dictionary data returned from the API.
+        token (`str`, *optional*):
+            Authentication token for the Inference Endpoint, if set when requesting the API.
+
+    Example:
+    ```python
+    >>> from huggingface_hub import get_inference_endpoint
+    >>> endpoint = get_inference_endpoint("my-text-to-image")
+    >>> endpoint
+    InferenceEndpoint(name='my-text-to-image', ...)
+
+    # Get status
+    >>> endpoint.status
+    'running'
+    >>> endpoint.url
+    'https://my-text-to-image.region.vendor.endpoints.huggingface.cloud'
+
+    # Run inference
+    >>> endpoint.client.text_to_image(...)
+
+    # Pause endpoint to save $$$
+    >>> endpoint.pause()
+
+    # ...
+    # Resume and wait for deployment
+    >>> endpoint.resume()
+    >>> endpoint.wait()
+    >>> endpoint.client.text_to_image(...)
+    ```
+    """
+
+    # Field in __repr__
+    name: str = field(init=False)
+    namespace: str
+    repository: str = field(init=False)
+    status: InferenceEndpointStatus = field(init=False)
+    url: Optional[str] = field(init=False)
+
+    # Other fields
+    framework: str = field(repr=False, init=False)
+    revision: str = field(repr=False, init=False)
+    task: str = field(repr=False, init=False)
+    created_at: datetime = field(repr=False, init=False)
+    updated_at: datetime = field(repr=False, init=False)
+    type: InferenceEndpointType = field(repr=False, init=False)
+
+    # Raw dict from the API
+    raw: Dict = field(repr=False)
+
+    # Internal fields
+    _token: Optional[str] = field(repr=False, compare=False)
+    _api: "HfApi" = field(repr=False, compare=False)
+
+    @classmethod
+    def from_raw(
+        cls, raw: Dict, namespace: str, token: Optional[str] = None, api: Optional["HfApi"] = None
+    ) -> "InferenceEndpoint":
+        """Initialize object from raw dictionary."""
+        if api is None:
+            from .hf_api import HfApi
+
+            api = HfApi()
+        if token is None:
+            token = api.token
+
+        # All other fields are populated in __post_init__
+        return cls(raw=raw, namespace=namespace, _token=token, _api=api)
+
+    def __post_init__(self) -> None:
+        """Populate fields from raw dictionary."""
+        self._populate_from_raw()
+
+    @property
+    def client(self) -> InferenceClient:
+        """Returns a client to make predictions on this Inference Endpoint.
+
+        Raises:
+            [`InferenceEndpointError`]: If the Inference Endpoint is not yet deployed.
+        """
+        if self.url is None:
+            raise InferenceEndpointError(
+                "Cannot create a client for this Inference Endpoint as it is not yet deployed. "
+                "Please wait for the Inference Endpoint to be deployed using `endpoint.wait()` and try again."
+            )
+        return InferenceClient(model=self.url, token=self._token)
+
+    @property
+    def async_client(self) -> AsyncInferenceClient:
+        """Returns a client to make predictions on this Inference Endpoint.
+
+        Raises:
+            [`InferenceEndpointError`]: If the Inference Endpoint is not yet deployed.
+        """
+        if self.url is None:
+            raise InferenceEndpointError(
+                "Cannot create a client for this Inference Endpoint as it is not yet deployed. "
+                "Please wait for the Inference Endpoint to be deployed using `endpoint.wait()` and try again."
+            )
+        return AsyncInferenceClient(model=self.url, token=self._token)
+
+    def wait(self, timeout: Optional[int] = None, refresh_every: int = 5) -> None:
+        """Wait for the Inference Endpoint to be deployed.
+
+        Information from the server will be fetched every 1s. If the Inference Endpoint is not deployed after `timeout`
+        seconds, a [`InferenceEndpointTimeoutError`] will be raised. The [`InferenceEndpoint`] will be mutated in place with the latest
+        data.
+
+        Args:
+            timeout (`int`, *optional*):
+                The maximum time to wait for the Inference Endpoint to be deployed, in seconds. If `None`, will wait
+                indefinitely.
+            refresh_every (`int`, *optional*):
+                The time to wait between each fetch of the Inference Endpoint status, in seconds. Defaults to 5s.
+        """
+        if self.url is not None:  # Means the endpoint is deployed
+            logger.info("Inference Endpoint is ready to be used.")
+            return
+
+        if timeout is not None and timeout < 0:
+            raise ValueError("`timeout` cannot be negative.")
+        if refresh_every <= 0:
+            raise ValueError("`refresh_every` must be positive.")
+
+        start = time.time()
+        while True:
+            self.fetch()
+            if self.url is not None:  # Means the endpoint is deployed
+                logger.info("Inference Endpoint is ready to be used.")
+                return
+            if timeout is not None:
+                if time.time() - start > timeout:
+                    raise InferenceEndpointTimeoutError("Timeout while waiting for Inference Endpoint to be deployed.")
+            logger.info(f"Inference Endpoint is not deployed yet ({self.status}). Waiting {refresh_every}s...")
+            time.sleep(refresh_every)
+
+    def fetch(self) -> "InferenceEndpoint":
+        """Fetch latest information about the Inference Endpoint."""
+        obj = self._api.get_inference_endpoint(name=self.name, namespace=self.namespace, token=self._token)
+        self.raw = obj.raw
+        self._populate_from_raw()
+        return self
+
+    def update(
+        self,
+        *,
+        # Compute update
+        accelerator: Optional[str] = None,
+        instance_size: Optional[str] = None,
+        instance_type: Optional[str] = None,
+        min_replica: Optional[int] = None,
+        max_replica: Optional[int] = None,
+        # Model update
+        repository: Optional[str] = None,
+        framework: Optional[str] = None,
+        revision: Optional[str] = None,
+        task: Optional[str] = None,
+    ) -> "InferenceEndpoint":
+        """Update the Inference Endpoint.
+
+        This method allows the update of either the compute configuration, the deployed model, or both. All arguments are
+        optional but at least one must be provided.
+
+        This is an alias for [`HfApi.update_inference_endpoint`]. The current object is mutated in place with the
+        latest data from the server.
+
+        Args:
+            accelerator (`str`, *optional*):
+                The hardware accelerator to be used for inference (e.g. `"cpu"`).
+            instance_size (`str`, *optional*):
+                The size or type of the instance to be used for hosting the model (e.g. `"large"`).
+            instance_type (`str`, *optional*):
+                The cloud instance type where the Inference Endpoint will be deployed (e.g. `"c6i"`).
+            min_replica (`int`, *optional*):
+                The minimum number of replicas (instances) to keep running for the Inference Endpoint.
+            max_replica (`int`, *optional*):
+                The maximum number of replicas (instances) to scale to for the Inference Endpoint.
+
+            repository (`str`, *optional*):
+                The name of the model repository associated with the Inference Endpoint (e.g. `"gpt2"`).
+            framework (`str`, *optional*):
+                The machine learning framework used for the model (e.g. `"custom"`).
+            revision (`str`, *optional*):
+                The specific model revision to deploy on the Inference Endpoint (e.g. `"6c0e6080953db56375760c0471a8c5f2929baf11"`).
+            task (`str`, *optional*):
+                The task on which to deploy the model (e.g. `"text-classification"`).
+        """
+        # Make API call
+        obj = self._api.update_inference_endpoint(
+            name=self.name,
+            namespace=self.namespace,
+            accelerator=accelerator,
+            instance_size=instance_size,
+            instance_type=instance_type,
+            min_replica=min_replica,
+            max_replica=max_replica,
+            repository=repository,
+            framework=framework,
+            revision=revision,
+            task=task,
+            token=self._token,
+        )
+
+        # Mutate current object
+        self.raw = obj.raw
+        self._populate_from_raw()
+        return self
+
+    def pause(self) -> "InferenceEndpoint":
+        """Pause the Inference Endpoint.
+
+        A paused Inference Endpoint will not be charged. It can be resumed at any time using [`InferenceEndpoint.resume`].
+        This is different than scaling the Inference Endpoint to zero with [`InferenceEndpoint.scale_to_zero`], which
+        would be automatically restarted when a request is made to it.
+
+        This is an alias for [`HfApi.pause_inference_endpoint`]. The current object is mutated in place with the
+        latest data from the server.
+        """
+        obj = self._api.pause_inference_endpoint(name=self.name, namespace=self.namespace, token=self._token)
+        self.raw = obj.raw
+        self._populate_from_raw()
+        return self
+
+    def resume(self) -> "InferenceEndpoint":
+        """Resume the Inference Endpoint.
+
+        This is an alias for [`HfApi.resume_inference_endpoint`]. The current object is mutated in place with the
+        latest data from the server.
+        """
+        obj = self._api.resume_inference_endpoint(name=self.name, namespace=self.namespace, token=self._token)
+        self.raw = obj.raw
+        self._populate_from_raw()
+        return self
+
+    def scale_to_zero(self) -> "InferenceEndpoint":
+        """Scale Inference Endpoint to zero.
+
+        An Inference Endpoint scaled to zero will not be charged. It will be resume on the next request to it, with a
+        cold start delay. This is different than pausing the Inference Endpoint with [`InferenceEndpoint.pause`], which
+        would require a manual resume with [`InferenceEndpoint.resume`].
+
+        This is an alias for [`HfApi.scale_to_zero_inference_endpoint`]. The current object is mutated in place with the
+        latest data from the server.
+        """
+        obj = self._api.scale_to_zero_inference_endpoint(name=self.name, namespace=self.namespace, token=self._token)
+        self.raw = obj.raw
+        self._populate_from_raw()
+        return self
+
+    def delete(self) -> None:
+        """Delete the Inference Endpoint.
+
+        This operation is not reversible. If you don't want to be charged for an Inference Endpoint, it is preferable
+        to pause it with [`InferenceEndpoint.pause`] or scale it to zero with [`InferenceEndpoint.scale_to_zero`].
+
+        This is an alias for [`HfApi.delete_inference_endpoint`].
+        """
+        self._api.delete_inference_endpoint(name=self.name, namespace=self.namespace, token=self._token)
+
+    def _populate_from_raw(self) -> None:
+        """Populate fields from raw dictionary.
+
+        Called in __post_init__ + each time the Inference Endpoint is updated.
+        """
+        # Repr fields
+        self.name = self.raw["name"]
+        self.repository = self.raw["model"]["repository"]
+        self.status = self.raw["status"]["state"]
+        self.url = self.raw["status"].get("url")
+
+        # Other fields
+        self.framework = self.raw["model"]["framework"]
+        self.revision = self.raw["model"]["revision"]
+        self.task = self.raw["model"]["task"]
+        self.created_at = parse_datetime(self.raw["status"]["createdAt"])
+        self.updated_at = parse_datetime(self.raw["status"]["updatedAt"])
+        self.type = self.raw["type"]
diff --git a/src/huggingface_hub/_login.py b/src/huggingface_hub/_login.py
index 21856548..81a667a6 100644
--- a/src/huggingface_hub/_login.py
+++ b/src/huggingface_hub/_login.py
@@ -36,6 +36,14 @@ from .utils import (
 
 logger = logging.get_logger(__name__)
 
+_HF_LOGO_ASCII = """
+    _|    _|  _|    _|    _|_|_|    _|_|_|  _|_|_|  _|      _|    _|_|_|      _|_|_|_|    _|_|      _|_|_|  _|_|_|_|
+    _|    _|  _|    _|  _|        _|          _|    _|_|    _|  _|            _|        _|    _|  _|        _|
+    _|_|_|_|  _|    _|  _|  _|_|  _|  _|_|    _|    _|  _|  _|  _|  _|_|      _|_|_|    _|_|_|_|  _|        _|_|_|
+    _|    _|  _|    _|  _|    _|  _|    _|    _|    _|    _|_|  _|    _|      _|        _|    _|  _|        _|
+    _|    _|    _|_|      _|_|_|    _|_|_|  _|_|_|  _|      _|    _|_|_|      _|        _|    _|    _|_|_|  _|_|_|_|
+"""
+
 
 def login(
     token: Optional[str] = None,
@@ -144,13 +152,7 @@ def interpreter_login(new_session: bool = True, write_permission: bool = False)
         print("User is already logged in.")
         return
 
-    print("""
-    _|    _|  _|    _|    _|_|_|    _|_|_|  _|_|_|  _|      _|    _|_|_|      _|_|_|_|    _|_|      _|_|_|  _|_|_|_|
-    _|    _|  _|    _|  _|        _|          _|    _|_|    _|  _|            _|        _|    _|  _|        _|
-    _|_|_|_|  _|    _|  _|  _|_|  _|  _|_|    _|    _|  _|  _|  _|  _|_|      _|_|_|    _|_|_|_|  _|        _|_|_|
-    _|    _|  _|    _|  _|    _|  _|    _|    _|    _|    _|_|  _|    _|      _|        _|    _|  _|        _|
-    _|    _|    _|_|      _|_|_|    _|_|_|  _|_|_|  _|      _|    _|_|_|      _|        _|    _|    _|_|_|  _|_|_|_|
-    """)
+    print(_HF_LOGO_ASCII)
     if HfFolder.get_token() is not None:
         print(
             "    A token is already saved on your machine. Run `huggingface-cli"
diff --git a/src/huggingface_hub/_multi_commits.py b/src/huggingface_hub/_multi_commits.py
index c41d2a36..cc04d547 100644
--- a/src/huggingface_hub/_multi_commits.py
+++ b/src/huggingface_hub/_multi_commits.py
@@ -15,13 +15,13 @@
 """Contains utilities to multi-commits (i.e. push changes iteratively on a PR)."""
 import re
 from dataclasses import dataclass, field
-from hashlib import sha256
 from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Union
 
 from ._commit_api import CommitOperationAdd, CommitOperationDelete
 from .community import DiscussionWithDetails
 from .utils import experimental
 from .utils._cache_manager import _format_size
+from .utils.insecure_hashlib import sha256
 
 
 if TYPE_CHECKING:
diff --git a/src/huggingface_hub/constants.py b/src/huggingface_hub/constants.py
index f5672996..ee1cb98c 100644
--- a/src/huggingface_hub/constants.py
+++ b/src/huggingface_hub/constants.py
@@ -51,6 +51,10 @@ HUGGINGFACE_HEADER_X_LINKED_SIZE = "X-Linked-Size"
 
 INFERENCE_ENDPOINT = os.environ.get("HF_INFERENCE_ENDPOINT", "https://api-inference.huggingface.co")
 
+# See https://huggingface.co/docs/inference-endpoints/index
+INFERENCE_ENDPOINTS_ENDPOINT = "https://api.endpoints.huggingface.cloud/v2"
+
+
 REPO_ID_SEPARATOR = "--"
 # ^ this substring is not allowed in repo_ids on hf.co
 # and is the canonical one we use for serialization of repo ids elsewhere.
diff --git a/src/huggingface_hub/file_download.py b/src/huggingface_hub/file_download.py
index 7083cb3a..49a55cd1 100644
--- a/src/huggingface_hub/file_download.py
+++ b/src/huggingface_hub/file_download.py
@@ -13,7 +13,6 @@ import warnings
 from contextlib import contextmanager
 from dataclasses import dataclass
 from functools import partial
-from hashlib import sha256
 from pathlib import Path
 from typing import Any, BinaryIO, Dict, Generator, Literal, Optional, Tuple, Union
 from urllib.parse import quote, urlparse
@@ -75,6 +74,7 @@ from .utils import (
 from .utils._headers import _http_user_agent
 from .utils._runtime import _PY_VERSION  # noqa: F401 # for backward compatibility
 from .utils._typing import HTTP_METHOD_T
+from .utils.insecure_hashlib import sha256
 
 
 logger = logging.get_logger(__name__)
@@ -1606,12 +1606,9 @@ def get_hf_file_metadata(
     # Return
     return HfFileMetadata(
         commit_hash=r.headers.get(HUGGINGFACE_HEADER_X_REPO_COMMIT),
-        etag=_normalize_etag(
-            # We favor a custom header indicating the etag of the linked resource, and
-            # we fallback to the regular etag header.
-            r.headers.get(HUGGINGFACE_HEADER_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.
+        etag=_normalize_etag(r.headers.get(HUGGINGFACE_HEADER_X_LINKED_ETAG) or r.headers.get("ETag")),
         # Either from response headers (if redirected) or defaults to request url
         # Do not use directly `url`, as `_request_wrapper` might have followed relative
         # redirects.
diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py
index fb1ede10..e381721f 100644
--- a/src/huggingface_hub/hf_api.py
+++ b/src/huggingface_hub/hf_api.py
@@ -69,6 +69,7 @@ from ._commit_api import (
     _upload_lfs_files,
     _warn_on_overwriting_operations,
 )
+from ._inference_endpoints import InferenceEndpoint, InferenceEndpointType
 from ._multi_commits import (
     MULTI_COMMIT_PR_CLOSE_COMMENT_FAILURE_BAD_REQUEST_TEMPLATE,
     MULTI_COMMIT_PR_CLOSE_COMMENT_FAILURE_NO_CHANGES_TEMPLATE,
@@ -95,6 +96,7 @@ from .constants import (
     DEFAULT_ETAG_TIMEOUT,
     DEFAULT_REVISION,
     ENDPOINT,
+    INFERENCE_ENDPOINTS_ENDPOINT,
     REGEX_COMMIT_OID,
     REPO_TYPE_MODEL,
     REPO_TYPES,
@@ -5896,6 +5898,446 @@ class HfApi:
         hf_raise_for_status(r)
         return SpaceRuntime(r.json())
 
+    #######################
+    # Inference Endpoints #
+    #######################
+
+    def list_inference_endpoints(
+        self, namespace: Optional[str] = None, *, token: Optional[str] = None
+    ) -> List[InferenceEndpoint]:
+        """Lists all inference endpoints for the given namespace.
+
+        Args:
+            namespace (`str`, *optional*):
+                The namespace to list endpoints for. Defaults to the current user. Set to `"*"` to list all endpoints
+                from all namespaces (i.e. personal namespace and all orgs the user belongs to).
+            token (`str`, *optional*):
+                An authentication token (See https://huggingface.co/settings/token).
+
+        Returns:
+            List[`InferenceEndpoint`]: A list of all inference endpoints for the given namespace.
+
+        Example:
+        ```python
+        >>> from huggingface_hub import HfApi
+        >>> api = HfApi()
+        >>> api.list_inference_endpoints()
+        [InferenceEndpoint(name='my-endpoint', ...), ...]
+        ```
+        """
+        # Special case: list all endpoints for all namespaces the user has access to
+        if namespace == "*":
+            user = self.whoami(token=token)
+
+            # List personal endpoints first
+            endpoints: List[InferenceEndpoint] = list_inference_endpoints(namespace=self._get_namespace(token=token))
+
+            # Then list endpoints for all orgs the user belongs to and ignore 401 errors (no billing or no access)
+            for org in user.get("orgs", []):
+                try:
+                    endpoints += list_inference_endpoints(namespace=org["name"], token=token)
+                except HfHubHTTPError as error:
+                    if error.response.status_code == 401:  # Either no billing or user don't have access)
+                        logger.debug("Cannot list Inference Endpoints for org '%s': %s", org["name"], error)
+                    pass
+
+            return endpoints
+
+        # Normal case: list endpoints for a specific namespace
+        namespace = namespace or self._get_namespace(token=token)
+
+        response = get_session().get(
+            f"{INFERENCE_ENDPOINTS_ENDPOINT}/endpoint/{namespace}",
+            headers=self._build_hf_headers(token=token),
+        )
+        hf_raise_for_status(response)
+
+        return [
+            InferenceEndpoint.from_raw(endpoint, namespace=namespace, token=token)
+            for endpoint in response.json()["items"]
+        ]
+
+    def create_inference_endpoint(
+        self,
+        name: str,
+        *,
+        repository: str,
+        framework: str,
+        accelerator: str,
+        instance_size: str,
+        instance_type: str,
+        region: str,
+        vendor: str,
+        account_id: Optional[str] = None,
+        min_replica: int = 0,
+        max_replica: int = 1,
+        revision: Optional[str] = None,
+        task: Optional[str] = None,
+        type: InferenceEndpointType = InferenceEndpointType.PROTECTED,
+        namespace: Optional[str] = None,
+        token: Optional[str] = None,
+    ) -> InferenceEndpoint:
+        """Create a new Inference Endpoint.
+
+        Args:
+            name (`str`):
+                The unique name for the new Inference Endpoint.
+            repository (`str`):
+                The name of the model repository associated with the Inference Endpoint (e.g. `"gpt2"`).
+            framework (`str`):
+                The machine learning framework used for the model (e.g. `"custom"`).
+            accelerator (`str`):
+                The hardware accelerator to be used for inference (e.g. `"cpu"`).
+            instance_size (`str`):
+                The size or type of the instance to be used for hosting the model (e.g. `"large"`).
+            instance_type (`str`):
+                The cloud instance type where the Inference Endpoint will be deployed (e.g. `"c6i"`).
+            region (`str`):
+                The cloud region in which the Inference Endpoint will be created (e.g. `"us-east-1"`).
+            vendor (`str`):
+                The cloud provider or vendor where the Inference Endpoint will be hosted (e.g. `"aws"`).
+            account_id (`str`, *optional*):
+                The account ID used to link a VPC to a private Inference Endpoint (if applicable).
+            min_replica (`int`, *optional*):
+                The minimum number of replicas (instances) to keep running for the Inference Endpoint. Defaults to 0.
+            max_replica (`int`, *optional*):
+                The maximum number of replicas (instances) to scale to for the Inference Endpoint. Defaults to 1.
+            revision (`str`, *optional*):
+                The specific model revision to deploy on the Inference Endpoint (e.g. `"6c0e6080953db56375760c0471a8c5f2929baf11"`).
+            task (`str`, *optional*):
+                The task on which to deploy the model (e.g. `"text-classification"`).
+            type ([`InferenceEndpointType]`, *optional*):
+                The type of the Inference Endpoint, which can be `"protected"` (default), `"public"` or `"private"`.
+            namespace (`str`, *optional*):
+                The namespace where the Inference Endpoint will be created. Defaults to the current user's namespace.
+            token (`str`, *optional*):
+                An authentication token (See https://huggingface.co/settings/token).
+
+            Returns:
+                [`InferenceEndpoint`]: information about the updated Inference Endpoint.
+
+            Example:
+            ```python
+            >>> from huggingface_hub import HfApi
+            >>> api = HfApi()
+            >>> create_inference_endpoint(
+            ...     "my-endpoint-name",
+            ...     repository="gpt2",
+            ...     framework="pytorch",
+            ...     task="text-generation",
+            ...     accelerator="cpu",
+            ...     vendor="aws",
+            ...     region="us-east-1",
+            ...     type="protected",
+            ...     instance_size="medium",
+            ...     instance_type="c6i"
+            ... )
+            >>> endpoint
+            InferenceEndpoint(name='my-endpoint-name', status="pending",...)
+
+            # Run inference on the endpoint
+            >>> endpoint.client.text_generation(...)
+            "..."
+            ```
+        """
+        namespace = namespace or self._get_namespace(token=token)
+
+        payload: Dict = {
+            "accountId": account_id,
+            "compute": {
+                "accelerator": accelerator,
+                "instanceSize": instance_size,
+                "instanceType": instance_type,
+                "scaling": {
+                    "maxReplica": max_replica,
+                    "minReplica": min_replica,
+                },
+            },
+            "model": {
+                "framework": framework,
+                "repository": repository,
+                "revision": revision,
+                "task": task,
+                "image": {"huggingface": {}},
+            },
+            "name": name,
+            "provider": {
+                "region": region,
+                "vendor": vendor,
+            },
+            "type": type,
+        }
+
+        response = get_session().post(
+            f"{INFERENCE_ENDPOINTS_ENDPOINT}/endpoint/{namespace}",
+            headers=self._build_hf_headers(token=token),
+            json=payload,
+        )
+        hf_raise_for_status(response)
+
+        return InferenceEndpoint.from_raw(response.json(), namespace=namespace, token=token)
+
+    def get_inference_endpoint(
+        self, name: str, *, namespace: Optional[str] = None, token: Optional[str] = None
+    ) -> InferenceEndpoint:
+        """Get information about an Inference Endpoint.
+
+        Args:
+            name (`str`):
+                The name of the Inference Endpoint to retrieve information about.
+            namespace (`str`, *optional*):
+                The namespace in which the Inference Endpoint is located. Defaults to the current user.
+            token (`str`, *optional*):
+                An authentication token (See https://huggingface.co/settings/token).
+
+        Returns:
+            [`InferenceEndpoint`]: information about the requested Inference Endpoint.
+
+        Example:
+        ```python
+        >>> from huggingface_hub import HfApi
+        >>> api = HfApi()
+        >>> endpoint = api.get_inference_endpoint("my-text-to-image")
+        >>> endpoint
+        InferenceEndpoint(name='my-text-to-image', ...)
+
+        # Get status
+        >>> endpoint.status
+        'running'
+        >>> endpoint.url
+        'https://my-text-to-image.region.vendor.endpoints.huggingface.cloud'
+
+        # Run inference
+        >>> endpoint.client.text_to_image(...)
+        ```
+        """
+        namespace = namespace or self._get_namespace(token=token)
+
+        response = get_session().get(
+            f"{INFERENCE_ENDPOINTS_ENDPOINT}/endpoint/{namespace}/{name}",
+            headers=self._build_hf_headers(token=token),
+        )
+        hf_raise_for_status(response)
+
+        return InferenceEndpoint.from_raw(response.json(), namespace=namespace, token=token)
+
+    def update_inference_endpoint(
+        self,
+        name: str,
+        *,
+        # Compute update
+        accelerator: Optional[str] = None,
+        instance_size: Optional[str] = None,
+        instance_type: Optional[str] = None,
+        min_replica: Optional[int] = None,
+        max_replica: Optional[int] = None,
+        # Model update
+        repository: Optional[str] = None,
+        framework: Optional[str] = None,
+        revision: Optional[str] = None,
+        task: Optional[str] = None,
+        # Other
+        namespace: Optional[str] = None,
+        token: Optional[str] = None,
+    ) -> InferenceEndpoint:
+        """Update an Inference Endpoint.
+
+        This method allows the update of either the compute configuration, the deployed model, or both. All arguments are
+        optional but at least one must be provided.
+
+        For convenience, you can also update an Inference Endpoint using [`InferenceEndpoint.update`].
+
+        Args:
+            name (`str`):
+                The name of the Inference Endpoint to update.
+
+            accelerator (`str`, *optional*):
+                The hardware accelerator to be used for inference (e.g. `"cpu"`).
+            instance_size (`str`, *optional*):
+                The size or type of the instance to be used for hosting the model (e.g. `"large"`).
+            instance_type (`str`, *optional*):
+                The cloud instance type where the Inference Endpoint will be deployed (e.g. `"c6i"`).
+            min_replica (`int`, *optional*):
+                The minimum number of replicas (instances) to keep running for the Inference Endpoint.
+            max_replica (`int`, *optional*):
+                The maximum number of replicas (instances) to scale to for the Inference Endpoint.
+
+            repository (`str`, *optional*):
+                The name of the model repository associated with the Inference Endpoint (e.g. `"gpt2"`).
+            framework (`str`, *optional*):
+                The machine learning framework used for the model (e.g. `"custom"`).
+            revision (`str`, *optional*):
+                The specific model revision to deploy on the Inference Endpoint (e.g. `"6c0e6080953db56375760c0471a8c5f2929baf11"`).
+            task (`str`, *optional*):
+                The task on which to deploy the model (e.g. `"text-classification"`).
+
+            namespace (`str`, *optional*):
+                The namespace where the Inference Endpoint will be updated. Defaults to the current user's namespace.
+            token (`str`, *optional*):
+                An authentication token (See https://huggingface.co/settings/token).
+
+        Returns:
+            [`InferenceEndpoint`]: information about the updated Inference Endpoint.
+        """
+        namespace = namespace or self._get_namespace(token=token)
+
+        payload: Dict = {}
+        if any(value is not None for value in (accelerator, instance_size, instance_type, min_replica, max_replica)):
+            payload["compute"] = {
+                "accelerator": accelerator,
+                "instanceSize": instance_size,
+                "instanceType": instance_type,
+                "scaling": {
+                    "maxReplica": max_replica,
+                    "minReplica": min_replica,
+                },
+            }
+        if any(value is not None for value in (repository, framework, revision, task)):
+            payload["model"] = {
+                "framework": framework,
+                "repository": repository,
+                "revision": revision,
+                "task": task,
+                "image": {"huggingface": {}},
+            }
+
+        response = get_session().put(
+            f"{INFERENCE_ENDPOINTS_ENDPOINT}/endpoint/{namespace}/{name}",
+            headers=self._build_hf_headers(token=token),
+            json=payload,
+        )
+        hf_raise_for_status(response)
+
+        return InferenceEndpoint.from_raw(response.json(), namespace=namespace, token=token)
+
+    def delete_inference_endpoint(
+        self, name: str, *, namespace: Optional[str] = None, token: Optional[str] = None
+    ) -> None:
+        """Delete an Inference Endpoint.
+
+        This operation is not reversible. If you don't want to be charged for an Inference Endpoint, it is preferable
+        to pause it with [`pause_inference_endpoint`] or scale it to zero with [`scale_to_zero_inference_endpoint`].
+
+        For convenience, you can also delete an Inference Endpoint using [`InferenceEndpoint.delete`].
+
+        Args:
+            name (`str`):
+                The name of the Inference Endpoint to delete.
+            namespace (`str`, *optional*):
+                The namespace in which the Inference Endpoint is located. Defaults to the current user.
+            token (`str`, *optional*):
+                An authentication token (See https://huggingface.co/settings/token).
+        """
+        namespace = namespace or self._get_namespace(token=token)
+        response = get_session().delete(
+            f"{INFERENCE_ENDPOINTS_ENDPOINT}/endpoint/{namespace}/{name}",
+            headers=self._build_hf_headers(token=token),
+        )
+        hf_raise_for_status(response)
+
+    def pause_inference_endpoint(
+        self, name: str, *, namespace: Optional[str] = None, token: Optional[str] = None
+    ) -> InferenceEndpoint:
+        """Pause an Inference Endpoint.
+
+        A paused Inference Endpoint will not be charged. It can be resumed at any time using [`resume_inference_endpoint`].
+        This is different than scaling the Inference Endpoint to zero with [`scale_to_zero_inference_endpoint`], which
+        would be automatically restarted when a request is made to it.
+
+        For convenience, you can also pause an Inference Endpoint using [`pause_inference_endpoint`].
+
+        Args:
+            name (`str`):
+                The name of the Inference Endpoint to pause.
+            namespace (`str`, *optional*):
+                The namespace in which the Inference Endpoint is located. Defaults to the current user.
+            token (`str`, *optional*):
+                An authentication token (See https://huggingface.co/settings/token).
+
+        Returns:
+            [`InferenceEndpoint`]: information about the paused Inference Endpoint.
+        """
+        namespace = namespace or self._get_namespace(token=token)
+
+        response = get_session().post(
+            f"{INFERENCE_ENDPOINTS_ENDPOINT}/endpoint/{namespace}/{name}/pause",
+            headers=self._build_hf_headers(token=token),
+        )
+        hf_raise_for_status(response)
+
+        return InferenceEndpoint.from_raw(response.json(), namespace=namespace, token=token)
+
+    def resume_inference_endpoint(
+        self, name: str, *, namespace: Optional[str] = None, token: Optional[str] = None
+    ) -> InferenceEndpoint:
+        """Resume an Inference Endpoint.
+
+        For convenience, you can also resume an Inference Endpoint using [`InferenceEndpoint.resume`].
+
+        Args:
+            name (`str`):
+                The name of the Inference Endpoint to resume.
+            namespace (`str`, *optional*):
+                The namespace in which the Inference Endpoint is located. Defaults to the current user.
+            token (`str`, *optional*):
+                An authentication token (See https://huggingface.co/settings/token).
+
+        Returns:
+            [`InferenceEndpoint`]: information about the resumed Inference Endpoint.
+        """
+        namespace = namespace or self._get_namespace(token=token)
+
+        response = get_session().post(
+            f"{INFERENCE_ENDPOINTS_ENDPOINT}/endpoint/{namespace}/{name}/resume",
+            headers=self._build_hf_headers(token=token),
+        )
+        hf_raise_for_status(response)
+
+        return InferenceEndpoint.from_raw(response.json(), namespace=namespace, token=token)
+
+    def scale_to_zero_inference_endpoint(
+        self, name: str, *, namespace: Optional[str] = None, token: Optional[str] = None
+    ) -> InferenceEndpoint:
+        """Scale Inference Endpoint to zero.
+
+        An Inference Endpoint scaled to zero will not be charged. It will be resume on the next request to it, with a
+        cold start delay. This is different than pausing the Inference Endpoint with [`pause_inference_endpoint`], which
+        would require a manual resume with [`resume_inference_endpoint`].
+
+        For convenience, you can also scale an Inference Endpoint to zero using [`InferenceEndpoint.scale_to_zero`].
+
+        Args:
+            name (`str`):
+                The name of the Inference Endpoint to scale to zero.
+            namespace (`str`, *optional*):
+                The namespace in which the Inference Endpoint is located. Defaults to the current user.
+            token (`str`, *optional*):
+                An authentication token (See https://huggingface.co/settings/token).
+
+        Returns:
+            [`InferenceEndpoint`]: information about the scaled-to-zero Inference Endpoint.
+        """
+        namespace = namespace or self._get_namespace(token=token)
+
+        response = get_session().post(
+            f"{INFERENCE_ENDPOINTS_ENDPOINT}/endpoint/{namespace}/{name}/scale-to-zero",
+            headers=self._build_hf_headers(token=token),
+        )
+        hf_raise_for_status(response)
+
+        return InferenceEndpoint.from_raw(response.json(), namespace=namespace, token=token)
+
+    def _get_namespace(self, token: Optional[str] = None) -> str:
+        """Get the default namespace for the current user."""
+        me = self.whoami(token=token)
+        if me["type"] == "user":
+            return me["name"]
+        else:
+            raise ValueError(
+                "Cannot determine default namespace. You must provide a 'namespace' as input or be logged in as a"
+                " user."
+            )
+
     ########################
     # Collection Endpoints #
     ########################
@@ -6469,6 +6911,16 @@ duplicate_space = api.duplicate_space
 request_space_storage = api.request_space_storage
 delete_space_storage = api.delete_space_storage
 
+# Inference Endpoint API
+list_inference_endpoints = api.list_inference_endpoints
+create_inference_endpoint = api.create_inference_endpoint
+get_inference_endpoint = api.get_inference_endpoint
+update_inference_endpoint = api.update_inference_endpoint
+delete_inference_endpoint = api.delete_inference_endpoint
+pause_inference_endpoint = api.pause_inference_endpoint
+resume_inference_endpoint = api.resume_inference_endpoint
+scale_to_zero_inference_endpoint = api.scale_to_zero_inference_endpoint
+
 # Collections API
 get_collection = api.get_collection
 create_collection = api.create_collection
diff --git a/src/huggingface_hub/hf_file_system.py b/src/huggingface_hub/hf_file_system.py
index b4b12888..9666c00e 100644
--- a/src/huggingface_hub/hf_file_system.py
+++ b/src/huggingface_hub/hf_file_system.py
@@ -11,6 +11,7 @@ import fsspec
 
 from ._commit_api import CommitOperationCopy, CommitOperationDelete
 from .constants import DEFAULT_REVISION, ENDPOINT, REPO_TYPE_MODEL, REPO_TYPES_MAPPING, REPO_TYPES_URL_PREFIXES
+from .file_download import hf_hub_url
 from .hf_api import HfApi
 from .utils import (
     EntryNotFoundError,
@@ -45,10 +46,8 @@ class HfFileSystemResolvedPath:
     path_in_repo: str
 
     def unresolve(self) -> str:
-        return (
-            f"{REPO_TYPES_URL_PREFIXES.get(self.repo_type, '') + self.repo_id}@{safe_revision(self.revision)}/{self.path_in_repo}"
-            .rstrip("/")
-        )
+        repo_path = REPO_TYPES_URL_PREFIXES.get(self.repo_type, "") + self.repo_id
+        return f"{repo_path}@{safe_revision(self.revision)}/{self.path_in_repo}".rstrip("/")
 
 
 class HfFileSystem(fsspec.AbstractFileSystem):
@@ -421,8 +420,11 @@ class HfFileSystemFile(fsspec.spec.AbstractBufferedFile):
             "range": f"bytes={start}-{end - 1}",
             **self.fs._api._build_hf_headers(),
         }
-        url = (
-            f"{self.fs.endpoint}/{REPO_TYPES_URL_PREFIXES.get(self.resolved_path.repo_type, '') + self.resolved_path.repo_id}/resolve/{safe_quote(self.resolved_path.revision)}/{safe_quote(self.resolved_path.path_in_repo)}"
+        url = hf_hub_url(
+            repo_id=self.resolved_path.repo_id,
+            revision=self.resolved_path.revision,
+            filename=self.resolved_path.path_in_repo,
+            endpoint=self.fs.endpoint,
         )
         r = http_backoff("GET", url, headers=headers)
         hf_raise_for_status(r)
diff --git a/src/huggingface_hub/utils/_datetime.py b/src/huggingface_hub/utils/_datetime.py
index 57c3524d..716f373e 100644
--- a/src/huggingface_hub/utils/_datetime.py
+++ b/src/huggingface_hub/utils/_datetime.py
@@ -55,7 +55,9 @@ def parse_datetime(date_string: str) -> datetime:
         # timezone and then move it to the appropriate UTC timezone.
         # See https://en.wikipedia.org/wiki/ISO_8601#Coordinated_Universal_Time_(UTC)
         # Taken from https://stackoverflow.com/a/3168394.
-
+        if len(date_string) == 30:
+            # Means timezoned-timestamp with nanoseconds precision. We need to truncate the last 3 digits.
+            date_string = date_string[:-4] + "Z"
         dt = datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%S.%fZ")
         dt += UTC_OFFSET  # By default, datetime is not timezoned -> move to UTC time
         return dt.astimezone(timezone.utc)  # Set explicit timezone
diff --git a/src/huggingface_hub/utils/_errors.py b/src/huggingface_hub/utils/_errors.py
index bfb199bb..47afab1c 100644
--- a/src/huggingface_hub/utils/_errors.py
+++ b/src/huggingface_hub/utils/_errors.py
@@ -2,6 +2,7 @@ from typing import Optional
 
 from requests import HTTPError, Response
 
+from ..constants import INFERENCE_ENDPOINTS_ENDPOINT
 from ._fixes import JSONDecodeError
 
 
@@ -293,6 +294,15 @@ def hf_raise_for_status(response: Response, endpoint_name: Optional[str] = None)
             # This prevent from raising a misleading `RepositoryNotFoundError` (see below).
             pass
 
+        elif (
+            response.status_code == 401
+            and response.request.url is not None
+            and INFERENCE_ENDPOINTS_ENDPOINT in response.request.url
+        ):
+            # Not enough permission to list Inference Endpoints from this org. We don't raise a custom error for this.
+            # This prevent from raising a misleading `RepositoryNotFoundError` (see below).
+            pass
+
         elif error_code == "RepoNotFound" or response.status_code == 401:
             # 401 is misleading as it is returned for:
             #    - private and gated repos if user is not authenticated
diff --git a/src/huggingface_hub/utils/insecure_hashlib.py b/src/huggingface_hub/utils/insecure_hashlib.py
new file mode 100644
index 00000000..f232ee0a
--- /dev/null
+++ b/src/huggingface_hub/utils/insecure_hashlib.py
@@ -0,0 +1,34 @@
+# Taken from https://github.com/mlflow/mlflow/pull/10119
+#
+# DO NOT use this function for security purposes (e.g., password hashing).
+#
+# In Python >= 3.9, insecure hashing algorithms such as MD5 fail in FIPS-compliant
+# environments unless `usedforsecurity=False` is explicitly passed.
+#
+# References:
+# - https://github.com/mlflow/mlflow/issues/9905
+# - https://github.com/mlflow/mlflow/pull/10119
+# - https://docs.python.org/3/library/hashlib.html
+# - https://github.com/huggingface/transformers/pull/27038
+#
+# Usage:
+#     ```python
+#     # Use
+#     from huggingface_hub.utils.insecure_hashlib import sha256
+#     # instead of
+#     from hashlib import sha256
+#
+#     # Use
+#     from huggingface_hub.utils import insecure_hashlib
+#     # instead of
+#     import hashlib
+#     ```
+import functools
+import hashlib
+import sys
+
+
+_kwargs = {"usedforsecurity": False} if sys.version_info >= (3, 9) else {}
+md5 = functools.partial(hashlib.md5, **_kwargs)
+sha1 = functools.partial(hashlib.sha1, **_kwargs)
+sha256 = functools.partial(hashlib.sha256, **_kwargs)
diff --git a/src/huggingface_hub/utils/sha.py b/src/huggingface_hub/utils/sha.py
index 157ccb03..adab9351 100644
--- a/src/huggingface_hub/utils/sha.py
+++ b/src/huggingface_hub/utils/sha.py
@@ -1,7 +1,8 @@
 """Utilities to efficiently compute the SHA 256 hash of a bunch of bytes."""
-from hashlib import sha256
 from typing import BinaryIO, Optional
 
+from .insecure_hashlib import sha256
+
 
 def sha_fileobj(fileobj: BinaryIO, chunk_size: Optional[int] = None) -> bytes:
     """
diff --git a/utils/_legacy_check_future_compatible_signatures.py b/utils/_legacy_check_future_compatible_signatures.py
index bbafc935..a0e2a280 100644
--- a/utils/_legacy_check_future_compatible_signatures.py
+++ b/utils/_legacy_check_future_compatible_signatures.py
@@ -35,7 +35,6 @@ import tempfile
 from pathlib import Path
 from typing import Callable, NoReturn
 
-import black
 from ruff.__main__ import find_ruff_bin
 
 from huggingface_hub.hf_api import HfApi
@@ -173,17 +172,14 @@ def generate_hf_api_module() -> str:
     # Generate code with stubs
     generated_code = STUBS_SECTION_TEMPLATE_REGEX.sub(STUBS_SECTION_TEMPLATE.format(stubs=all_stubs_source), raw_code)
 
-    # Format (black+ruff)
+    # Format (ruff)
     return format_generated_code(generated_code)
 
 
 def format_generated_code(code: str) -> str:
     """
-    Format some code with black+ruff. Cannot be done "on the fly" so we first save the code in a temporary file.
+    Format some code with ruff. Cannot be done "on the fly" so we first save the code in a temporary file.
     """
-    # Format with black
-    code = black.format_file_contents(code, fast=False, mode=black.FileMode(line_length=119))
-
     # Format with ruff
     with tempfile.TemporaryDirectory() as tmpdir:
         filepath = Path(tmpdir) / "__init__.py"


Test patch:
diff --git a/tests/test_inference_endpoints.py b/tests/test_inference_endpoints.py
new file mode 100644
index 00000000..66da6740
--- /dev/null
+++ b/tests/test_inference_endpoints.py
@@ -0,0 +1,206 @@
+from datetime import datetime, timezone
+from unittest.mock import Mock, patch
+
+import pytest
+
+from huggingface_hub import (
+    AsyncInferenceClient,
+    HfApi,
+    InferenceClient,
+    InferenceEndpoint,
+    InferenceEndpointError,
+    InferenceEndpointTimeoutError,
+)
+
+
+MOCK_INITIALIZING = {
+    "name": "my-endpoint-name",
+    "type": "protected",
+    "accountId": None,
+    "provider": {"vendor": "aws", "region": "us-east-1"},
+    "compute": {
+        "accelerator": "cpu",
+        "instanceType": "c6i",
+        "instanceSize": "medium",
+        "scaling": {"minReplica": 0, "maxReplica": 1},
+    },
+    "model": {
+        "repository": "gpt2",
+        "revision": "11c5a3d5811f50298f278a704980280950aedb10",
+        "task": "text-generation",
+        "framework": "pytorch",
+        "image": {"huggingface": {}},
+    },
+    "status": {
+        "createdAt": "2023-10-26T12:41:53.263078506Z",
+        "createdBy": {"id": "6273f303f6d63a28483fde12", "name": "Wauplin"},
+        "updatedAt": "2023-10-26T12:41:53.263079138Z",
+        "updatedBy": {"id": "6273f303f6d63a28483fde12", "name": "Wauplin"},
+        "private": None,
+        "state": "pending",
+        "message": "Endpoint waiting to be scheduled",
+        "readyReplica": 0,
+        "targetReplica": 0,
+    },
+}
+
+MOCK_RUNNING = {
+    "name": "my-endpoint-name",
+    "type": "protected",
+    "accountId": None,
+    "provider": {"vendor": "aws", "region": "us-east-1"},
+    "compute": {
+        "accelerator": "cpu",
+        "instanceType": "c6i",
+        "instanceSize": "medium",
+        "scaling": {"minReplica": 0, "maxReplica": 1},
+    },
+    "model": {
+        "repository": "gpt2",
+        "revision": "11c5a3d5811f50298f278a704980280950aedb10",
+        "task": "text-generation",
+        "framework": "pytorch",
+        "image": {"huggingface": {}},
+    },
+    "status": {
+        "createdAt": "2023-10-26T12:41:53.263Z",
+        "createdBy": {"id": "6273f303f6d63a28483fde12", "name": "Wauplin"},
+        "updatedAt": "2023-10-26T12:41:53.263Z",
+        "updatedBy": {"id": "6273f303f6d63a28483fde12", "name": "Wauplin"},
+        "private": None,
+        "state": "running",
+        "message": "Endpoint is ready",
+        "url": "https://vksrvs8pc1xnifhq.us-east-1.aws.endpoints.huggingface.cloud",
+        "readyReplica": 1,
+        "targetReplica": 1,
+    },
+}
+
+
+def test_from_raw_initialization():
+    """Test InferenceEndpoint is correctly initialized from raw dict."""
+    endpoint = InferenceEndpoint.from_raw(MOCK_INITIALIZING, namespace="foo")
+
+    # Main attributes parsed correctly
+    assert endpoint.name == "my-endpoint-name"
+    assert endpoint.namespace == "foo"
+    assert endpoint.repository == "gpt2"
+    assert endpoint.framework == "pytorch"
+    assert endpoint.status == "pending"
+    assert endpoint.revision == "11c5a3d5811f50298f278a704980280950aedb10"
+    assert endpoint.task == "text-generation"
+    assert endpoint.type == "protected"
+
+    # Datetime parsed correctly
+    assert endpoint.created_at == datetime(2023, 10, 26, 12, 41, 53, 263078, tzinfo=timezone.utc)
+    assert endpoint.updated_at == datetime(2023, 10, 26, 12, 41, 53, 263079, tzinfo=timezone.utc)
+
+    # Not initialized yet
+    assert endpoint.url is None
+
+    # Raw dict still accessible
+    assert endpoint.raw == MOCK_INITIALIZING
+
+
+def test_from_raw_with_hf_api():
+    """Test that the HfApi is correctly passed to the InferenceEndpoint."""
+    endpoint = InferenceEndpoint.from_raw(
+        MOCK_INITIALIZING, namespace="foo", api=HfApi(library_name="my-library", token="hf_***")
+    )
+    assert endpoint._api.library_name == "my-library"
+    assert endpoint._api.token == "hf_***"
+
+
+def test_get_client_not_ready():
+    """Test clients are not created when endpoint is not ready."""
+    endpoint = InferenceEndpoint.from_raw(MOCK_INITIALIZING, namespace="foo")
+
+    with pytest.raises(InferenceEndpointError):
+        assert endpoint.client
+
+    with pytest.raises(InferenceEndpointError):
+        assert endpoint.async_client
+
+
+def test_get_client_ready():
+    """Test clients are created correctly when endpoint is ready."""
+    endpoint = InferenceEndpoint.from_raw(MOCK_RUNNING, namespace="foo", token="my-token")
+
+    # Endpoint is ready
+    assert endpoint.status == "running"
+    assert endpoint.url == "https://vksrvs8pc1xnifhq.us-east-1.aws.endpoints.huggingface.cloud"
+
+    # => Client available
+    client = endpoint.client
+    assert isinstance(client, InferenceClient)
+    assert "my-token" in client.headers["Authorization"]
+
+    # => AsyncClient available
+    async_client = endpoint.async_client
+    assert isinstance(async_client, AsyncInferenceClient)
+    assert "my-token" in async_client.headers["Authorization"]
+
+
+@patch("huggingface_hub.hf_api.HfApi.get_inference_endpoint")
+def test_fetch(mock_get: Mock):
+    endpoint = InferenceEndpoint.from_raw(MOCK_INITIALIZING, namespace="foo")
+
+    mock_get.return_value = InferenceEndpoint.from_raw(MOCK_RUNNING, namespace="foo")
+    endpoint.fetch()
+
+    assert endpoint.status == "running"
+    assert endpoint.url == "https://vksrvs8pc1xnifhq.us-east-1.aws.endpoints.huggingface.cloud"
+
+
+@patch("huggingface_hub.hf_api.HfApi.get_inference_endpoint")
+def test_wait_until_running(mock_get: Mock):
+    """Test waits waits until the endpoint is ready."""
+    endpoint = InferenceEndpoint.from_raw(MOCK_INITIALIZING, namespace="foo")
+
+    mock_get.side_effect = [
+        InferenceEndpoint.from_raw(MOCK_INITIALIZING, namespace="foo"),
+        InferenceEndpoint.from_raw(MOCK_INITIALIZING, namespace="foo"),
+        InferenceEndpoint.from_raw(MOCK_INITIALIZING, namespace="foo"),
+        InferenceEndpoint.from_raw(MOCK_INITIALIZING, namespace="foo"),
+        InferenceEndpoint.from_raw(MOCK_RUNNING, namespace="foo"),
+    ]
+    endpoint.wait(refresh_every=0.01)
+
+    assert endpoint.status == "running"
+    assert len(mock_get.call_args_list) == 5
+
+
+@patch("huggingface_hub.hf_api.HfApi.get_inference_endpoint")
+def test_wait_timeout(mock_get: Mock):
+    """Test waits until timeout error is raised."""
+    endpoint = InferenceEndpoint.from_raw(MOCK_INITIALIZING, namespace="foo")
+
+    mock_get.side_effect = [
+        InferenceEndpoint.from_raw(MOCK_INITIALIZING, namespace="foo"),
+        InferenceEndpoint.from_raw(MOCK_INITIALIZING, namespace="foo"),
+        InferenceEndpoint.from_raw(MOCK_INITIALIZING, namespace="foo"),
+        InferenceEndpoint.from_raw(MOCK_INITIALIZING, namespace="foo"),
+    ]
+    with pytest.raises(InferenceEndpointTimeoutError):
+        endpoint.wait(timeout=0.1, refresh_every=0.05)
+
+    assert endpoint.status == "pending"
+    assert len(mock_get.call_args_list) == 3
+
+
+@patch("huggingface_hub.hf_api.HfApi.pause_inference_endpoint")
+def test_pause(mock: Mock):
+    """Test `pause` calls the correct alias."""
+    endpoint = InferenceEndpoint.from_raw(MOCK_RUNNING, namespace="foo")
+    mock.return_value = InferenceEndpoint.from_raw(MOCK_INITIALIZING, namespace="foo")
+    endpoint.pause()
+    mock.assert_called_once_with(namespace="foo", name="my-endpoint-name", token=None)
+
+
+@patch("huggingface_hub.hf_api.HfApi.resume_inference_endpoint")
+def test_resume(mock: Mock):
+    """Test `resume` calls the correct alias."""
+    endpoint = InferenceEndpoint.from_raw(MOCK_RUNNING, namespace="foo")
+    mock.return_value = InferenceEndpoint.from_raw(MOCK_INITIALIZING, namespace="foo")
+    endpoint.resume()
+    mock.assert_called_once_with(namespace="foo", name="my-endpoint-name", token=None)
diff --git a/tests/test_utils_datetime.py b/tests/test_utils_datetime.py
index aac379ac..3cb6c048 100644
--- a/tests/test_utils_datetime.py
+++ b/tests/test_utils_datetime.py
@@ -14,6 +14,12 @@ class TestDatetimeUtils(unittest.TestCase):
             datetime(2022, 8, 19, 7, 19, 38, 123000, tzinfo=timezone.utc),
         )
 
+        # Test nanoseconds precision (should be truncated to microseconds)
+        self.assertEqual(
+            parse_datetime("2022-08-19T07:19:38.123456789Z"),
+            datetime(2022, 8, 19, 7, 19, 38, 123456, tzinfo=timezone.utc),
+        )
+
         with pytest.raises(ValueError, match=r".*Cannot parse '2022-08-19T07:19:38' as a datetime.*"):
             parse_datetime("2022-08-19T07:19:38")