diff --git a/docs/source/en/guides/manage-spaces.md b/docs/source/en/guides/manage-spaces.md index f5d7552265..95497e420f 100644 --- a/docs/source/en/guides/manage-spaces.md +++ b/docs/source/en/guides/manage-spaces.md @@ -29,7 +29,7 @@ This can prove useful if you want to build up from an existing Space instead of It is also useful is you want control over the configuration/settings of a public Space. See [`duplicate_space`] for more details. ```py ->>> duplicate_space("multimodalart/dreambooth-training") +>>> api.duplicate_space("multimodalart/dreambooth-training") ``` **2. Upload your code using your preferred solution.** @@ -43,19 +43,21 @@ Here is an example to upload the local folder `src/` from your machine to your S At this step, your app should already be running on the Hub for free ! However, you might want to configure it further with secrets and upgraded hardware. -**3. Configure secrets** +**3. Configure secrets and variables** -Your Space might require some secret keys or token to work. +Your Space might require some secret keys, token or variables to work. See [docs](https://huggingface.co/docs/hub/spaces-overview#managing-secrets) for more details. For example, an HF token to upload an image dataset to the Hub once generated from your Space. ```py >>> api.add_space_secret(repo_id=repo_id, key="HF_TOKEN", value="hf_api_***") +>>> api.add_space_variable(repo_id=repo_id, key="MODEL_REPO_ID", value="user/repo") ``` -Secrets can be deleted as well: +Secrets and variables can be deleted as well: ```py >>> api.delete_space_secret(repo_id=repo_id, key="HF_TOKEN") +>>> api.delete_space_variable(repo_id=repo_id, key="MODEL_REPO_ID") ``` @@ -67,6 +69,28 @@ Streamlit Secrets Management if using Streamlit). No need to fetch them via the Any change in your Space configuration (secrets or hardware) will trigger a restart of your app. +**Bonus: set secrets and variables when creating or duplicating the Space!** + +Secrets and variables can be set when creating or duplicating a space: + +```py +>>> api.create_repo( +... repo_id=repo_id, +... repo_type="space", +... space_sdk="gradio", +... space_secrets=[{"key"="HF_TOKEN", "value"="hf_api_***"}, ...], +... space_variables=[{"key"="MODEL_REPO_ID", "value"="user/repo"}, ...], +... ) +``` + +```py +>>> api.duplicate_space( +... from_id=repo_id, +... secrets=[{"key"="HF_TOKEN", "value"="hf_api_***"}, ...], +... variables=[{"key"="MODEL_REPO_ID", "value"="user/repo"}, ...], +... ) +``` + **4. Configure the hardware** By default, your Space will run on a CPU environment for free. You can upgrade the hardware @@ -99,7 +123,7 @@ has been met. You now have a Space fully configured. Make sure to downgrade your Space back to "cpu-classic" when you are done using it. -**Bonus: request hardware when creating the Space!** +**Bonus: request hardware when creating or duplicating the Space!** Upgraded hardware will be automatically assigned to your Space once it's built. @@ -109,6 +133,16 @@ Upgraded hardware will be automatically assigned to your Space once it's built. ... repo_type="space", ... space_sdk="gradio" ... space_hardware="cpu-upgrade", +... space_storage="small", +... space_sleep_time="7200", # 2 hours in secs +... ) +``` +```py +>>> api.duplicate_space( +... from_id=repo_id, +... hardware="cpu-upgrade", +... storage="small", +... sleep_time="7200", # 2 hours in secs ... ) ``` @@ -147,7 +181,27 @@ Upgraded hardware will be automatically assigned to your Space once it's built. >>> api.request_space_hardware(repo_id=repo_id, hardware=SpaceHardware.T4_MEDIUM, sleep_time=3600) ``` +**Bonus: set a sleep time when creating or duplicating the Space!** + +```py +>>> api.create_repo( +... repo_id=repo_id, +... repo_type="space", +... space_sdk="gradio" +... space_hardware="t4-medium", +... space_sleep_time="3600", +... ) +``` +```py +>>> api.duplicate_space( +... from_id=repo_id, +... hardware="t4-medium", +... sleep_time="3600", +... ) +``` + **6. Add persistent storage to your Space** + You can choose the storage tier of your choice to access disk space that persists across restarts of your Space. This means you can read and write from disk like you would with a traditional hard drive. See [docs](https://huggingface.co/docs/hub/spaces-storage#persistent-storage) for more details. ```py @@ -163,6 +217,23 @@ You can also delete your storage, losing all the data permanently. Note: You cannot decrease the storage tier of your space once it's been granted. To do so, you must delete the storage first then request the new desired tier. +**Bonus: request storage when creating or duplicating the Space!** + +```py +>>> api.create_repo( +... repo_id=repo_id, +... repo_type="space", +... space_sdk="gradio" +... space_storage="large", +... ) +``` +```py +>>> api.duplicate_space( +... from_id=repo_id, +... storage="large", +... ) +``` + ## More advanced: temporarily upgrade your Space ! Spaces allow for a lot of different use cases. Sometimes, you might want diff --git a/docs/source/en/guides/repository.md b/docs/source/en/guides/repository.md index 13c324e665..3ea6655b5c 100644 --- a/docs/source/en/guides/repository.md +++ b/docs/source/en/guides/repository.md @@ -75,7 +75,7 @@ Specify the `repo_id` of the repository you want to delete: In some cases, you want to copy someone else's repo to adapt it to your use case. This is possible for Spaces using the [`duplicate_space`] method. It will duplicate the whole repository. -You will still need to configure your own settings (hardware and secrets). Check out our [Manage your Space](./manage-spaces) guide for more details. +You will still need to configure your own settings (hardware, sleep-time, storage, variables and secrets). Check out our [Manage your Space](./manage-spaces) guide for more details. ```py >>> from huggingface_hub import duplicate_space diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py index cd47ec9f6a..736e12c93b 100644 --- a/src/huggingface_hub/hf_api.py +++ b/src/huggingface_hub/hf_api.py @@ -2365,7 +2365,11 @@ def create_repo( repo_type: Optional[str] = None, exist_ok: bool = False, space_sdk: Optional[str] = None, - space_hardware: Optional[str] = None, + space_hardware: Optional[SpaceHardware] = None, + space_storage: Optional[SpaceStorage] = None, + space_sleep_time: Optional[int] = None, + space_secrets: Optional[List[Dict[str, str]]] = None, + space_variables: Optional[List[Dict[str, str]]] = None, ) -> RepoUrl: """Create an empty repo on the HuggingFace Hub. @@ -2387,6 +2391,19 @@ def create_repo( Choice of SDK to use if repo_type is "space". Can be "streamlit", "gradio", "docker", or "static". space_hardware (`SpaceHardware` or `str`, *optional*): Choice of Hardware if repo_type is "space". See [`SpaceHardware`] for a complete list. + space_storage (`SpaceStorage` or `str`, *optional*): + Choice of persistent storage tier. Example: `"small"`. See [`SpaceStorage`] for a complete list. + space_sleep_time (`int`, *optional*): + Number of seconds of inactivity to wait before a Space is put to sleep. Set to `-1` if you don't want + your Space to sleep (default behavior for upgraded hardware). For free hardware, you can't configure + the sleep time (value is fixed to 48 hours of inactivity). + See https://huggingface.co/docs/hub/spaces-gpus#sleep-time for more details. + space_secrets (`List[Dict[str, str]]`, *optional*): + A list of secret keys to set in your Space. Each item is in the form `{"key": ..., "value": ..., "description": ...}` where description is optional. + For more details, see https://huggingface.co/docs/hub/spaces-overview#managing-secrets. + space_variables (`List[Dict[str, str]]`, *optional*): + A list of public environment variables to set in your Space. Each item is in the form `{"key": ..., "value": ..., "description": ...}` where description is optional. + For more details, see https://huggingface.co/docs/hub/spaces-overview#managing-secrets-and-environment-variables. Returns: [`RepoUrl`]: URL to the newly created repo. Value is a subclass of `str` containing @@ -2399,7 +2416,7 @@ def create_repo( if repo_type not in REPO_TYPES: raise ValueError("Invalid repo type") - json = {"name": name, "organization": organization, "private": private} + json: Dict[str, Any] = {"name": name, "organization": organization, "private": private} if repo_type is not None: json["type"] = repo_type if repo_type == "space": @@ -2415,11 +2432,23 @@ def create_repo( if space_sdk is not None and repo_type != "space": warnings.warn("Ignoring provided space_sdk because repo_type is not 'space'.") - if space_hardware is not None: - if repo_type == "space": - json["hardware"] = space_hardware - else: - warnings.warn("Ignoring provided space_hardware because repo_type is not 'space'.") + function_args = [ + "space_hardware", + "space_storage", + "space_sleep_time", + "space_secrets", + "space_variables", + ] + json_keys = ["hardware", "storageTier", "sleepTimeSeconds", "secrets", "variables"] + values = [space_hardware, space_storage, space_sleep_time, space_secrets, space_variables] + + if repo_type == "space": + json.update({k: v for k, v in zip(json_keys, values) if v is not None}) + else: + provided_space_args = [key for key, value in zip(function_args, values) if value is not None] + + if provided_space_args: + warnings.warn(f"Ignoring provided {', '.join(provided_space_args)} because repo_type is not 'space'.") if getattr(self, "_lfsmultipartthresh", None): # Testing purposes only. @@ -5416,6 +5445,11 @@ def duplicate_space( private: Optional[bool] = None, token: Optional[str] = None, exist_ok: bool = False, + hardware: Optional[SpaceHardware] = None, + storage: Optional[SpaceStorage] = None, + sleep_time: Optional[int] = None, + secrets: Optional[List[Dict[str, str]]] = None, + variables: Optional[List[Dict[str, str]]] = None, ) -> RepoUrl: """Duplicate a Space. @@ -5434,6 +5468,21 @@ def duplicate_space( Hugging Face token. Will default to the locally saved token if not provided. exist_ok (`bool`, *optional*, defaults to `False`): If `True`, do not raise an error if repo already exists. + hardware (`SpaceHardware` or `str`, *optional*): + Choice of Hardware. Example: `"t4-medium"`. See [`SpaceHardware`] for a complete list. + storage (`SpaceStorage` or `str`, *optional*): + Choice of persistent storage tier. Example: `"small"`. See [`SpaceStorage`] for a complete list. + sleep_time (`int`, *optional*): + Number of seconds of inactivity to wait before a Space is put to sleep. Set to `-1` if you don't want + your Space to sleep (default behavior for upgraded hardware). For free hardware, you can't configure + the sleep time (value is fixed to 48 hours of inactivity). + See https://huggingface.co/docs/hub/spaces-gpus#sleep-time for more details. + secrets (`List[Dict[str, str]]`, *optional*): + A list of secret keys to set in your Space. Each item is in the form `{"key": ..., "value": ..., "description": ...}` where description is optional. + For more details, see https://huggingface.co/docs/hub/spaces-overview#managing-secrets. + variables (`List[Dict[str, str]]`, *optional*): + A list of public environment variables to set in your Space. Each item is in the form `{"key": ..., "value": ..., "description": ...}` where description is optional. + For more details, see https://huggingface.co/docs/hub/spaces-overview#managing-secrets-and-environment-variables. Returns: [`RepoUrl`]: URL to the newly created repo. Value is a subclass of `str` containing @@ -5473,9 +5522,17 @@ def duplicate_space( # repository must be a valid repo_id (namespace/repo_name). payload: Dict[str, Any] = {"repository": f"{to_namespace}/{to_repo_name}"} - # private is optional with this endpoint, with None defaulting to the original space's privacy. - if private is not None: - payload["private"] = private + keys = ["private", "hardware", "storageTier", "sleepTimeSeconds", "secrets", "variables"] + values = [private, hardware, storage, sleep_time, secrets, variables] + payload.update({k: v for k, v in zip(keys, values) if v is not None}) + + if sleep_time is not None and hardware == SpaceHardware.CPU_BASIC: + warnings.warn( + "If your Space runs on the default 'cpu-basic' hardware, it will go to sleep if inactive for more" + " than 48 hours. This value is not configurable. If you don't want your Space to deactivate or if" + " you want to set a custom sleep time, you need to upgrade to a paid Hardware.", + UserWarning, + ) r = get_session().post( f"{self.endpoint}/api/spaces/{from_id}/duplicate", diff --git a/tests/test_hf_api.py b/tests/test_hf_api.py index 018cba0411..9102f8d963 100644 --- a/tests/test_hf_api.py +++ b/tests/test_hf_api.py @@ -2713,6 +2713,119 @@ def test_create_space_with_hardware(self) -> None: }, ) + def test_create_space_with_hardware_and_sleep_time(self) -> None: + self.api.create_repo( + self.repo_id, + repo_type="space", + space_sdk="gradio", + space_hardware=SpaceHardware.T4_MEDIUM, + space_sleep_time=123, + ) + self.post_mock.assert_called_once_with( + f"{self.api.endpoint}/api/repos/create", + headers=self.api._build_hf_headers(), + json={ + "name": self.repo_id, + "organization": None, + "private": False, + "type": "space", + "sdk": "gradio", + "hardware": "t4-medium", + "sleepTimeSeconds": 123, + }, + ) + + def test_create_space_with_storage(self) -> None: + self.api.create_repo( + self.repo_id, + repo_type="space", + space_sdk="gradio", + space_storage=SpaceStorage.LARGE, + ) + self.post_mock.assert_called_once_with( + f"{self.api.endpoint}/api/repos/create", + headers=self.api._build_hf_headers(), + json={ + "name": self.repo_id, + "organization": None, + "private": False, + "type": "space", + "sdk": "gradio", + "storageTier": "large", + }, + ) + + def test_create_space_with_secrets_and_variables(self) -> None: + self.api.create_repo( + self.repo_id, + repo_type="space", + space_sdk="gradio", + space_secrets=[ + {"key": "Testsecret", "value": "Testvalue", "description": "Testdescription"}, + {"key": "Testsecret2", "value": "Testvalue"}, + ], + space_variables=[ + {"key": "Testvariable", "value": "Testvalue", "description": "Testdescription"}, + {"key": "Testvariable2", "value": "Testvalue"}, + ], + ) + self.post_mock.assert_called_once_with( + f"{self.api.endpoint}/api/repos/create", + headers=self.api._build_hf_headers(), + json={ + "name": self.repo_id, + "organization": None, + "private": False, + "type": "space", + "sdk": "gradio", + "secrets": [ + {"key": "Testsecret", "value": "Testvalue", "description": "Testdescription"}, + {"key": "Testsecret2", "value": "Testvalue"}, + ], + "variables": [ + {"key": "Testvariable", "value": "Testvalue", "description": "Testdescription"}, + {"key": "Testvariable2", "value": "Testvalue"}, + ], + }, + ) + + def test_duplicate_space(self) -> None: + self.api.duplicate_space( + self.repo_id, + to_id=f"{USER}/new_repo_id", + private=True, + hardware=SpaceHardware.T4_MEDIUM, + storage=SpaceStorage.LARGE, + sleep_time=123, + secrets=[ + {"key": "Testsecret", "value": "Testvalue", "description": "Testdescription"}, + {"key": "Testsecret2", "value": "Testvalue"}, + ], + variables=[ + {"key": "Testvariable", "value": "Testvalue", "description": "Testdescription"}, + {"key": "Testvariable2", "value": "Testvalue"}, + ], + ) + self.post_mock.assert_called_once_with( + f"{self.api.endpoint}/api/spaces/{self.repo_id}/duplicate", + headers=self.api._build_hf_headers(), + json={ + "repository": f"{USER}/new_repo_id", + "private": True, + "hardware": "t4-medium", + "storageTier": "large", + "sleepTimeSeconds": 123, + "secrets": [ + {"key": "Testsecret", "value": "Testvalue", "description": "Testdescription"}, + {"key": "Testsecret2", "value": "Testvalue"}, + ], + "variables": [ + {"key": "Testvariable", "value": "Testvalue", "description": "Testdescription"}, + {"key": "Testvariable2", "value": "Testvalue"}, + ], + }, + ) + def test_request_space_hardware_no_sleep_time(self) -> None: self.api.request_space_hardware(self.repo_id, SpaceHardware.T4_MEDIUM) self.post_mock.assert_called_once_with(