Skip to content

Commit e41dce2

Browse files
Add visibility parameter to HfApi repo create/update/duplicate methods (#3951)
* Add visibility to repo visibility APIs Co-authored-by: célina <hanouticelina@users.noreply.github.com> * Add visibility options to repo CLI Co-authored-by: célina <hanouticelina@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: célina <hanouticelina@gmail.com> * review suggestions * hid private param * add hidden --private --public --protected * remove --visibility option * update tests and docs --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: célina <hanouticelina@users.noreply.github.com>
1 parent 274fabf commit e41dce2

5 files changed

Lines changed: 134 additions & 36 deletions

File tree

docs/source/en/package_reference/cli.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2916,6 +2916,8 @@ $ hf repos create [OPTIONS] REPO_ID
29162916
* `--type, --repo-type [model|dataset|space]`: The type of repository (model, dataset, or space). [default: model]
29172917
* `--space-sdk TEXT`: Hugging Face Spaces SDK type. Required when --type is set to 'space'.
29182918
* `--private / --no-private`: Whether to create a private repo if repo doesn't exist on the Hub. Ignored if the repo already exists.
2919+
* `--public`: Whether to make the repo public. Ignored if the repo already exists.
2920+
* `--protected`: Whether to make the Space protected (Spaces only). Ignored if the repo already exists.
29192921
* `--token TEXT`: A User Access Token generated from https://huggingface.co/settings/tokens.
29202922
* `--exist-ok / --no-exist-ok`: Do not raise an error if repo already exists. [default: no-exist-ok]
29212923
* `--resource-group-id TEXT`: Resource group in which to create the repo. Resource groups is only available for Enterprise Hub organizations.
@@ -2931,7 +2933,7 @@ $ hf repos create [OPTIONS] REPO_ID
29312933
Examples
29322934
$ hf repos create my-model
29332935
$ hf repos create my-dataset --repo-type dataset --private
2934-
$ hf repos create my-space --type space --space-sdk gradio --flavor t4-medium --secrets HF_TOKEN -e THEME=dark
2936+
$ hf repos create my-space --type space --space-sdk gradio --flavor t4-medium --secrets HF_TOKEN -e THEME=dark --protected
29352937

29362938
Learn more
29372939
Use `hf <command> --help` for more information about a command.
@@ -3021,6 +3023,8 @@ $ hf repos duplicate [OPTIONS] FROM_ID [TO_ID]
30213023

30223024
* `--type, --repo-type [model|dataset|space]`: The type of repository (model, dataset, or space). [default: model]
30233025
* `--private / --no-private`: Whether to create a private repo if repo doesn't exist on the Hub. Ignored if the repo already exists.
3026+
* `--public`: Whether to make the repo public. Ignored if the repo already exists.
3027+
* `--protected`: Whether to make the Space protected (Spaces only). Ignored if the repo already exists.
30243028
* `--token TEXT`: A User Access Token generated from https://huggingface.co/settings/tokens.
30253029
* `--exist-ok / --no-exist-ok`: Do not raise an error if repo already exists. [default: no-exist-ok]
30263030
* `--flavor TEXT`: Space hardware flavor (e.g. 'cpu-basic', 't4-medium', 'l4x4'). Only for Spaces.
@@ -3087,14 +3091,17 @@ $ hf repos settings [OPTIONS] REPO_ID
30873091
**Options**:
30883092

30893093
* `--gated [auto|manual|false]`: The gated status for the repository.
3090-
* `--private / --no-private`: Whether the repository should be private.
3094+
* `--private / --no-private`: Whether to create a private repo if repo doesn't exist on the Hub. Ignored if the repo already exists.
3095+
* `--public`: Whether to make the repo public. Ignored if the repo already exists.
3096+
* `--protected`: Whether to make the Space protected (Spaces only). Ignored if the repo already exists.
30913097
* `--token TEXT`: A User Access Token generated from https://huggingface.co/settings/tokens.
30923098
* `--type, --repo-type [model|dataset|space]`: The type of repository (model, dataset, or space). [default: model]
30933099
* `--help`: Show this message and exit.
30943100

30953101
Examples
30963102
$ hf repos settings my-model --private
30973103
$ hf repos settings my-model --gated auto
3104+
$ hf repos settings my-space --repo-type space --protected
30983105

30993106
Learn more
31003107
Use `hf <command> --help` for more information about a command.

src/huggingface_hub/cli/repos.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,21 @@ class GatedChoices(str, enum.Enum):
7575
false = "false"
7676

7777

78+
PublicOpt = Annotated[
79+
Optional[bool],
80+
typer.Option(
81+
"--public",
82+
help="Whether to make the repo public. Ignored if the repo already exists.",
83+
),
84+
]
85+
86+
ProtectedOpt = Annotated[
87+
Optional[bool],
88+
typer.Option(
89+
"--protected",
90+
help="Whether to make the Space protected (Spaces only). Ignored if the repo already exists.",
91+
),
92+
]
7893
SpaceHardwareOpt = Annotated[
7994
Optional[str],
8095
typer.Option(
@@ -105,7 +120,7 @@ class GatedChoices(str, enum.Enum):
105120
examples=[
106121
"hf repos create my-model",
107122
"hf repos create my-dataset --repo-type dataset --private",
108-
"hf repos create my-space --type space --space-sdk gradio --flavor t4-medium --secrets HF_TOKEN -e THEME=dark",
123+
"hf repos create my-space --type space --space-sdk gradio --flavor t4-medium --secrets HF_TOKEN -e THEME=dark --protected",
109124
],
110125
)
111126
def repo_create(
@@ -118,6 +133,8 @@ def repo_create(
118133
),
119134
] = None,
120135
private: PrivateOpt = None,
136+
public: PublicOpt = None,
137+
protected: ProtectedOpt = None,
121138
token: TokenOpt = None,
122139
exist_ok: Annotated[
123140
bool,
@@ -144,7 +161,7 @@ def repo_create(
144161
repo_url = api.create_repo(
145162
repo_id=repo_id,
146163
repo_type=repo_type.value,
147-
private=private,
164+
visibility="private" if private else "public" if public else "protected" if protected else None, # type: ignore [arg-type]
148165
token=token,
149166
exist_ok=exist_ok,
150167
resource_group_id=resource_group_id,
@@ -176,6 +193,8 @@ def repo_duplicate(
176193
] = None,
177194
repo_type: RepoTypeOpt = RepoType.model,
178195
private: PrivateOpt = None,
196+
public: PublicOpt = None,
197+
protected: ProtectedOpt = None,
179198
token: TokenOpt = None,
180199
exist_ok: Annotated[
181200
bool,
@@ -197,7 +216,7 @@ def repo_duplicate(
197216
from_id=from_id,
198217
to_id=to_id,
199218
repo_type=repo_type.value,
200-
private=private,
219+
visibility="private" if private else "public" if public else "protected" if protected else None, # type: ignore [arg-type]
201220
token=token,
202221
exist_ok=exist_ok,
203222
space_hardware=hardware, # type: ignore[arg-type]
@@ -254,6 +273,7 @@ def repo_move(
254273
examples=[
255274
"hf repos settings my-model --private",
256275
"hf repos settings my-model --gated auto",
276+
"hf repos settings my-space --repo-type space --protected",
257277
],
258278
)
259279
def repo_settings(
@@ -264,12 +284,9 @@ def repo_settings(
264284
help="The gated status for the repository.",
265285
),
266286
] = None,
267-
private: Annotated[
268-
Optional[bool],
269-
typer.Option(
270-
help="Whether the repository should be private.",
271-
),
272-
] = None,
287+
private: PrivateOpt = None,
288+
public: PublicOpt = None,
289+
protected: ProtectedOpt = None,
273290
token: TokenOpt = None,
274291
repo_type: RepoTypeOpt = RepoType.model,
275292
) -> None:
@@ -278,7 +295,7 @@ def repo_settings(
278295
api.update_repo_settings(
279296
repo_id=repo_id,
280297
gated=(gated.value if gated else None), # type: ignore [arg-type]
281-
private=private,
298+
visibility="private" if private else "public" if public else "protected" if protected else None, # type: ignore [arg-type]
282299
repo_type=repo_type.value,
283300
)
284301
print(f"Successfully updated the settings of {ANSI.bold(repo_id)} on the Hub.")

src/huggingface_hub/hf_api.py

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
R = TypeVar("R") # Return type
151151
CollectionItemType_T = Literal["model", "dataset", "space", "paper", "collection"]
152152
CollectionSort_T = Literal["lastModified", "trending", "upvotes"]
153+
RepoVisibility_T = Literal["public", "private", "protected"]
153154

154155
ExpandModelProperty_T = Literal[
155156
"author",
@@ -249,10 +250,28 @@
249250
_BUCKET_PATHS_INFO_BATCH_SIZE = 1000
250251
_BUCKET_BATCH_ADD_CHUNK_SIZE = 100
251252
_BUCKET_BATCH_DELETE_CHUNK_SIZE = 1000
252-
253253
logger = logging.get_logger(__name__)
254254

255255

256+
def _resolve_repo_visibility(
257+
*,
258+
private: Optional[bool],
259+
visibility: Optional[RepoVisibility_T],
260+
repo_type: Optional[str],
261+
) -> Optional[RepoVisibility_T]:
262+
if private is not None and visibility is not None:
263+
raise ValueError("Received both `private` and `visibility` arguments. Please provide only one of them.")
264+
265+
if visibility is None:
266+
if private is None:
267+
return None
268+
return "private" if private else "public"
269+
270+
if visibility == "protected" and repo_type != constants.REPO_TYPE_SPACE:
271+
raise ValueError("Only Spaces can be 'protected'. Please set visibility to 'public' or 'private'.")
272+
return visibility
273+
274+
256275
def repo_type_and_id_from_hf_id(hf_id: str, hub_url: Optional[str] = None) -> tuple[Optional[str], Optional[str], str]:
257276
"""
258277
Returns the repo type and ID from a huggingface.co URL linking to a
@@ -4108,6 +4127,7 @@ def create_repo(
41084127
*,
41094128
token: Union[str, bool, None] = None,
41104129
private: Optional[bool] = None,
4130+
visibility: Optional[RepoVisibility_T] = None,
41114131
repo_type: Optional[str] = None,
41124132
exist_ok: bool = False,
41134133
resource_group_id: Optional[str] = None,
@@ -4130,7 +4150,11 @@ def create_repo(
41304150
https://huggingface.co/docs/huggingface_hub/quick-start#authentication).
41314151
To disable authentication, pass `False`.
41324152
private (`bool`, *optional*):
4133-
Whether to make the repo private. If `None` (default), the repo will be public unless the organization's default is private. This value is ignored if the repo already exists.
4153+
Whether to make the repo private. If `None` (default), the repo will be public unless the organization's default is private. This value is ignored if the repo already exists. Cannot be passed together with `visibility`.
4154+
visibility (`Literal["public", "private", "protected"]`, *optional*):
4155+
Visibility of the repo. Can be `"public"` or `"private"`, or `"protected"` for Spaces. If `None`
4156+
(default), the repo will be public unless the organization's default is private. This value is ignored
4157+
if the repo already exists.
41344158
repo_type (`str`, *optional*):
41354159
Set to `"dataset"` or `"space"` if uploading to a dataset or
41364160
space, `None` or `"model"` if uploading to a model. Default is
@@ -4171,9 +4195,11 @@ def create_repo(
41714195
if repo_type not in constants.REPO_TYPES:
41724196
raise ValueError("Invalid repo type")
41734197

4198+
resolved_visibility = _resolve_repo_visibility(private=private, visibility=visibility, repo_type=repo_type)
4199+
41744200
json: dict[str, Any] = {"name": name, "organization": organization}
4175-
if private is not None:
4176-
json["private"] = private
4201+
if resolved_visibility is not None:
4202+
json["visibility"] = resolved_visibility
41774203
if repo_type is not None:
41784204
json["type"] = repo_type
41794205
if repo_type == "space":
@@ -4303,14 +4329,15 @@ def update_repo_settings(
43034329
*,
43044330
gated: Optional[Literal["auto", "manual", False]] = None,
43054331
private: Optional[bool] = None,
4332+
visibility: Optional[RepoVisibility_T] = None,
43064333
token: Union[str, bool, None] = None,
43074334
repo_type: Optional[str] = None,
43084335
) -> None:
43094336
"""
43104337
Update the settings of a repository, including gated access and visibility.
43114338

43124339
To give more control over how repos are used, the Hub allows repo authors to enable
4313-
access requests for their repos, and also to set the visibility of the repo to private.
4340+
access requests for their repos, and also to change the visibility of the repo.
43144341

43154342
Args:
43164343
repo_id (`str`):
@@ -4321,7 +4348,9 @@ def update_repo_settings(
43214348
* "manual": The repository is gated, and access requests require manual approval.
43224349
* False : The repository is not gated, and anyone can access it.
43234350
private (`bool`, *optional*):
4324-
Whether the repository should be private.
4351+
Whether the repository should be private. Cannot be passed together with `visibility`.
4352+
visibility (`Literal["public", "private", "protected"]`, *optional*):
4353+
Visibility of the repository. Can be `"public"` or `"private"`, or `"protected"` for Spaces.
43254354
token (`Union[str, bool, None]`, *optional*):
43264355
A valid user access token (string). Defaults to the locally saved token,
43274356
which is the recommended method for authentication (see
@@ -4347,6 +4376,8 @@ def update_repo_settings(
43474376
if repo_type is None:
43484377
repo_type = constants.REPO_TYPE_MODEL # default repo type
43494378

4379+
resolved_visibility = _resolve_repo_visibility(private=private, visibility=visibility, repo_type=repo_type)
4380+
43504381
# Prepare the JSON payload for the PUT request
43514382
payload: dict = {}
43524383

@@ -4355,8 +4386,8 @@ def update_repo_settings(
43554386
raise ValueError(f"Invalid gated status, must be one of 'auto', 'manual', or False. Got '{gated}'.")
43564387
payload["gated"] = gated
43574388

4358-
if private is not None:
4359-
payload["private"] = private
4389+
if resolved_visibility is not None:
4390+
payload["visibility"] = resolved_visibility
43604391

43614392
if len(payload) == 0:
43624393
raise ValueError("At least one setting must be updated.")
@@ -7776,6 +7807,7 @@ def duplicate_repo(
77767807
*,
77777808
repo_type: Optional[str] = None,
77787809
private: Optional[bool] = None,
7810+
visibility: Optional[RepoVisibility_T] = None,
77797811
token: Union[bool, str, None] = None,
77807812
exist_ok: bool = False,
77817813
space_hardware: Optional[SpaceHardware] = None,
@@ -7800,7 +7832,10 @@ def duplicate_repo(
78007832
`None` or `"model"` if duplicating a model. Default is `None`.
78017833
private (`bool`, *optional*):
78027834
Whether the new repo should be private or not. Defaults to the same
7803-
privacy as the original repo.
7835+
privacy as the original repo. Cannot be passed together with `visibility`.
7836+
visibility (`Literal["public", "private", "protected"]`, *optional*):
7837+
Visibility of the new repo. Can be `"public"` or `"private"`, or `"protected"` for Spaces. Defaults
7838+
to the same visibility as the original repo.
78047839
token (`bool` or `str`, *optional*):
78057840
A valid user access token (string). Defaults to the locally saved
78067841
token, which is the recommended method for authentication (see
@@ -7862,6 +7897,8 @@ def duplicate_repo(
78627897
if repo_type not in constants.REPO_TYPES:
78637898
raise ValueError("Invalid repo type")
78647899

7900+
resolved_visibility = _resolve_repo_visibility(private=private, visibility=visibility, repo_type=repo_type)
7901+
78657902
# Map repo_type to API path segment
78667903
api_prefix = {
78677904
None: "models",
@@ -7883,8 +7920,8 @@ def duplicate_repo(
78837920

78847921
payload: dict[str, Any] = {"repository": f"{to_namespace}/{to_repo_name}"}
78857922

7886-
if private is not None:
7887-
payload["private"] = private
7923+
if resolved_visibility is not None:
7924+
payload["visibility"] = resolved_visibility
78887925

78897926
# Space-specific options
78907927
function_args = [
@@ -7935,6 +7972,7 @@ def duplicate_space(
79357972
to_id: Optional[str] = None,
79367973
*,
79377974
private: Optional[bool] = None,
7975+
visibility: Optional[RepoVisibility_T] = None,
79387976
token: Union[bool, str, None] = None,
79397977
exist_ok: bool = False,
79407978
hardware: Optional[SpaceHardware] = None,
@@ -7955,7 +7993,10 @@ def duplicate_space(
79557993
ID of the new Space. Example: `"dog/CLIP-Interrogator"`. If not provided, the new Space will have the same
79567994
name as the original Space, but in your account.
79577995
private (`bool`, *optional*):
7958-
Whether the new Space should be private or not. Defaults to the same privacy as the original Space.
7996+
Whether the new Space should be private or not. Defaults to the same privacy as the original Space. Cannot be passed together with `visibility`.
7997+
visibility (`Literal["public", "private", "protected"]`, *optional*):
7998+
Visibility of the new Space. Can be `"public"`, `"private"`, or `"protected"`. Defaults to the same
7999+
visibility as the original Space.
79598000
token (`bool` or `str`, *optional*):
79608001
A valid user access token (string). Defaults to the locally saved
79618002
token, which is the recommended method for authentication (see
@@ -7999,7 +8040,7 @@ def duplicate_space(
79998040
RepoUrl('https://huggingface.co/spaces/nateraw/dreambooth-training',...)
80008041

80018042
# Can set custom destination id and visibility flag.
8002-
>>> duplicate_space("multimodalart/dreambooth-training", to_id="my-dreambooth", private=True)
8043+
>>> duplicate_space("multimodalart/dreambooth-training", to_id="my-dreambooth", visibility="private")
80038044
RepoUrl('https://huggingface.co/spaces/nateraw/my-dreambooth',...)
80048045
```
80058046

@@ -8013,6 +8054,7 @@ def duplicate_space(
80138054
from_id=from_id,
80148055
repo_type="space",
80158056
private=private,
8057+
visibility=visibility,
80168058
token=token,
80178059
exist_ok=exist_ok,
80188060
space_hardware=hardware,

0 commit comments

Comments
 (0)