Skip to content

Commit 8318fe4

Browse files
lhoestqWauplin
andauthored
[Jobs] Use current or stored token in a Job secrets (#3272)
* hf_auth in jobs secrets * --secrets HF_TOKEN * also in --env for consistency * factorize * mypy * Apply suggestions from code review Co-authored-by: Lucain <[email protected]> * style * update regex * add test * Update src/huggingface_hub/utils/_dotenv.py --------- Co-authored-by: Lucain <[email protected]>
1 parent f359adc commit 8318fe4

File tree

5 files changed

+86
-33
lines changed

5 files changed

+86
-33
lines changed

docs/source/en/guides/cli.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,14 @@ You can pass environment variables to your job using
720720
>>> hf jobs run --secrets-file .env.secrets python:3.12 python -c "import os; print(os.environ['MY_SECRET'])"
721721
```
722722
723+
<Tip>
724+
725+
Use `--secrets HF_TOKEN` to pass your local Hugging Face token implicitly.
726+
With this syntax, the secret is retrieved from the environment variable.
727+
For `HF_TOKEN`, it may read the token file located in the Hugging Face home folder if the environment variable is unset.
728+
729+
</Tip>
730+
723731
### Hardware
724732
725733
Available `--flavor` options:

src/huggingface_hub/cli/jobs.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040

4141
import requests
4242

43-
from huggingface_hub import HfApi, SpaceHardware
43+
from huggingface_hub import HfApi, SpaceHardware, get_token
4444
from huggingface_hub.utils import logging
4545
from huggingface_hub.utils._dotenv import load_dotenv
4646

@@ -75,8 +75,16 @@ class RunCommand(BaseHuggingfaceCLICommand):
7575
def register_subcommand(parser: _SubParsersAction) -> None:
7676
run_parser = parser.add_parser("run", help="Run a Job")
7777
run_parser.add_argument("image", type=str, help="The Docker image to use.")
78-
run_parser.add_argument("-e", "--env", action="append", help="Set environment variables.")
79-
run_parser.add_argument("-s", "--secrets", action="append", help="Set secret environment variables.")
78+
run_parser.add_argument("-e", "--env", action="append", help="Set environment variables. E.g. --env ENV=value")
79+
run_parser.add_argument(
80+
"-s",
81+
"--secrets",
82+
action="append",
83+
help=(
84+
"Set secret environment variables. E.g. --secrets SECRET=value "
85+
"or `--secrets HF_TOKEN` to pass your Hugging Face token."
86+
),
87+
)
8088
run_parser.add_argument("--env-file", type=str, help="Read in a file of environment variables.")
8189
run_parser.add_argument("--secrets-file", type=str, help="Read in a file of secret environment variables.")
8290
run_parser.add_argument(
@@ -113,14 +121,15 @@ def __init__(self, args: Namespace) -> None:
113121
self.command: List[str] = args.command
114122
self.env: dict[str, Optional[str]] = {}
115123
if args.env_file:
116-
self.env.update(load_dotenv(Path(args.env_file).read_text()))
124+
self.env.update(load_dotenv(Path(args.env_file).read_text(), environ=os.environ.copy()))
117125
for env_value in args.env or []:
118-
self.env.update(load_dotenv(env_value))
126+
self.env.update(load_dotenv(env_value, environ=os.environ.copy()))
119127
self.secrets: dict[str, Optional[str]] = {}
128+
extended_environ = _get_extended_environ()
120129
if args.secrets_file:
121-
self.secrets.update(load_dotenv(Path(args.secrets_file).read_text()))
130+
self.secrets.update(load_dotenv(Path(args.secrets_file).read_text(), environ=extended_environ))
122131
for secret in args.secrets or []:
123-
self.secrets.update(load_dotenv(secret))
132+
self.secrets.update(load_dotenv(secret, environ=extended_environ))
124133
self.flavor: Optional[SpaceHardware] = args.flavor
125134
self.timeout: Optional[str] = args.timeout
126135
self.detach: bool = args.detach
@@ -449,7 +458,15 @@ def register_subcommand(parser):
449458
help=f"Flavor for the hardware, as in HF Spaces. Defaults to `cpu-basic`. Possible values: {', '.join(SUGGESTED_FLAVORS)}.",
450459
)
451460
run_parser.add_argument("-e", "--env", action="append", help="Environment variables")
452-
run_parser.add_argument("-s", "--secrets", action="append", help="Secret environment variables")
461+
run_parser.add_argument(
462+
"-s",
463+
"--secrets",
464+
action="append",
465+
help=(
466+
"Set secret environment variables. E.g. --secrets SECRET=value "
467+
"or `--secrets HF_TOKEN` to pass your Hugging Face token."
468+
),
469+
)
453470
run_parser.add_argument("--env-file", type=str, help="Read in a file of environment variables.")
454471
run_parser.add_argument(
455472
"--secrets-file",
@@ -480,14 +497,15 @@ def __init__(self, args: Namespace) -> None:
480497
self.image = args.image
481498
self.env: dict[str, Optional[str]] = {}
482499
if args.env_file:
483-
self.env.update(load_dotenv(Path(args.env_file).read_text()))
500+
self.env.update(load_dotenv(Path(args.env_file).read_text(), environ=os.environ.copy()))
484501
for env_value in args.env or []:
485-
self.env.update(load_dotenv(env_value))
502+
self.env.update(load_dotenv(env_value, environ=os.environ.copy()))
486503
self.secrets: dict[str, Optional[str]] = {}
504+
extended_environ = _get_extended_environ()
487505
if args.secrets_file:
488-
self.secrets.update(load_dotenv(Path(args.secrets_file).read_text()))
506+
self.secrets.update(load_dotenv(Path(args.secrets_file).read_text(), environ=extended_environ))
489507
for secret in args.secrets or []:
490-
self.secrets.update(load_dotenv(secret))
508+
self.secrets.update(load_dotenv(secret, environ=extended_environ))
491509
self.flavor: Optional[SpaceHardware] = args.flavor
492510
self.timeout: Optional[str] = args.timeout
493511
self.detach: bool = args.detach
@@ -523,3 +541,10 @@ def run(self) -> None:
523541
# Now let's stream the logs
524542
for log in api.fetch_job_logs(job_id=job.id):
525543
print(log)
544+
545+
546+
def _get_extended_environ() -> Dict[str, str]:
547+
extended_environ = os.environ.copy()
548+
if (token := get_token()) is not None:
549+
extended_environ["HF_TOKEN"] = token
550+
return extended_environ

src/huggingface_hub/hf_api.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,11 @@
132132
validate_hf_hub_args,
133133
)
134134
from .utils import tqdm as hf_tqdm
135-
from .utils._auth import _get_token_from_environment, _get_token_from_file, _get_token_from_google_colab
135+
from .utils._auth import (
136+
_get_token_from_environment,
137+
_get_token_from_file,
138+
_get_token_from_google_colab,
139+
)
136140
from .utils._deprecation import _deprecate_method
137141
from .utils._runtime import is_xet_available
138142
from .utils._typing import CallableT

src/huggingface_hub/utils/_dotenv.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# AI-generated module (ChatGPT)
22
import re
3-
from typing import Dict
3+
from typing import Dict, Optional
44

55

6-
def load_dotenv(dotenv_str: str) -> Dict[str, str]:
6+
def load_dotenv(dotenv_str: str, environ: Optional[Dict[str, str]] = None) -> Dict[str, str]:
77
"""
88
Parse a DOTENV-format string and return a dictionary of key-value pairs.
99
Handles quoted values, comments, export keyword, and blank lines.
@@ -12,17 +12,17 @@ def load_dotenv(dotenv_str: str) -> Dict[str, str]:
1212
line_pattern = re.compile(
1313
r"""
1414
^\s*
15-
(?:export\s+)? # optional export
15+
(?:export[^\S\n]+)? # optional export
1616
([A-Za-z_][A-Za-z0-9_]*) # key
17-
\s*=\s*
17+
[^\S\n]*(=)?[^\S\n]*
1818
( # value group
1919
(?:
2020
'(?:\\'|[^'])*' # single-quoted value
21-
| "(?:\\"|[^"])*" # double-quoted value
21+
| \"(?:\\\"|[^\"])*\" # double-quoted value
2222
| [^#\n\r]+? # unquoted value
2323
)
2424
)?
25-
\s*(?:\#.*)?$ # optional inline comment
25+
[^\S\n]*(?:\#.*)?$ # optional inline comment
2626
""",
2727
re.VERBOSE,
2828
)
@@ -33,19 +33,23 @@ def load_dotenv(dotenv_str: str) -> Dict[str, str]:
3333
continue # Skip comments and empty lines
3434

3535
match = line_pattern.match(line)
36-
if not match:
37-
continue # Skip malformed lines
38-
39-
key, raw_val = match.group(1), match.group(2) or ""
40-
val = raw_val.strip()
41-
42-
# Remove surrounding quotes if quoted
43-
if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")):
44-
val = val[1:-1]
45-
val = val.replace(r"\n", "\n").replace(r"\t", "\t").replace(r"\"", '"').replace(r"\\", "\\")
46-
if raw_val.startswith('"'):
47-
val = val.replace(r"\$", "$") # only in double quotes
48-
49-
env[key] = val
36+
if match:
37+
key = match.group(1)
38+
val = None
39+
if match.group(2): # if there is '='
40+
raw_val = match.group(3) or ""
41+
val = raw_val.strip()
42+
# Remove surrounding quotes if quoted
43+
if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")):
44+
val = val[1:-1]
45+
val = val.replace(r"\n", "\n").replace(r"\t", "\t").replace(r"\"", '"').replace(r"\\", "\\")
46+
if raw_val.startswith('"'):
47+
val = val.replace(r"\$", "$") # only in double quotes
48+
elif environ is not None:
49+
# Get it from the current environment
50+
val = environ.get(key)
51+
52+
if val is not None:
53+
env[key] = val
5054

5155
return env

tests/test_utils_dotenv.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,15 @@ def test_multiple_lines():
6262
D=4
6363
"""
6464
assert load_dotenv(data) == {"A": "1", "B": "two", "C": "three", "D": "4"}
65+
66+
67+
def test_environ():
68+
data = """
69+
A=1
70+
B
71+
C=3
72+
MISSING
73+
EMPTY
74+
"""
75+
environ = {"A": "one", "B": "two", "D": "four", "EMPTY": ""}
76+
assert load_dotenv(data, environ=environ) == {"A": "1", "B": "two", "C": "3", "EMPTY": ""}

0 commit comments

Comments
 (0)