_jobs_api.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. # coding=utf-8
  2. # Copyright 2025-present, the HuggingFace Inc. team.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. from dataclasses import dataclass
  16. from datetime import datetime
  17. from enum import Enum
  18. from typing import Any, Dict, List, Optional, Union
  19. from huggingface_hub import constants
  20. from huggingface_hub._space_api import SpaceHardware
  21. from huggingface_hub.utils._datetime import parse_datetime
  22. class JobStage(str, Enum):
  23. """
  24. Enumeration of possible stage of a Job on the Hub.
  25. Value can be compared to a string:
  26. ```py
  27. assert JobStage.COMPLETED == "COMPLETED"
  28. ```
  29. Possible values are: `COMPLETED`, `CANCELED`, `ERROR`, `DELETED`, `RUNNING`.
  30. Taken from https://github.com/huggingface/moon-landing/blob/main/server/job_types/JobInfo.ts#L61 (private url).
  31. """
  32. # Copied from moon-landing > server > lib > Job.ts
  33. COMPLETED = "COMPLETED"
  34. CANCELED = "CANCELED"
  35. ERROR = "ERROR"
  36. DELETED = "DELETED"
  37. RUNNING = "RUNNING"
  38. @dataclass
  39. class JobStatus:
  40. stage: JobStage
  41. message: Optional[str]
  42. @dataclass
  43. class JobOwner:
  44. id: str
  45. name: str
  46. type: str
  47. @dataclass
  48. class JobInfo:
  49. """
  50. Contains information about a Job.
  51. Args:
  52. id (`str`):
  53. Job ID.
  54. created_at (`datetime` or `None`):
  55. When the Job was created.
  56. docker_image (`str` or `None`):
  57. The Docker image from Docker Hub used for the Job.
  58. Can be None if space_id is present instead.
  59. space_id (`str` or `None`):
  60. The Docker image from Hugging Face Spaces used for the Job.
  61. Can be None if docker_image is present instead.
  62. command (`List[str]` or `None`):
  63. Command of the Job, e.g. `["python", "-c", "print('hello world')"]`
  64. arguments (`List[str]` or `None`):
  65. Arguments passed to the command
  66. environment (`Dict[str]` or `None`):
  67. Environment variables of the Job as a dictionary.
  68. secrets (`Dict[str]` or `None`):
  69. Secret environment variables of the Job (encrypted).
  70. flavor (`str` or `None`):
  71. Flavor for the hardware, as in Hugging Face Spaces. See [`SpaceHardware`] for possible values.
  72. E.g. `"cpu-basic"`.
  73. status: (`JobStatus` or `None`):
  74. Status of the Job, e.g. `JobStatus(stage="RUNNING", message=None)`
  75. See [`JobStage`] for possible stage values.
  76. owner: (`JobOwner` or `None`):
  77. Owner of the Job, e.g. `JobOwner(id="5e9ecfc04957053f60648a3e", name="lhoestq", type="user")`
  78. Example:
  79. ```python
  80. >>> from huggingface_hub import run_job
  81. >>> job = run_job(
  82. ... image="python:3.12",
  83. ... command=["python", "-c", "print('Hello from the cloud!')"]
  84. ... )
  85. >>> job
  86. JobInfo(id='687fb701029421ae5549d998', created_at=datetime.datetime(2025, 7, 22, 16, 6, 25, 79000, tzinfo=datetime.timezone.utc), docker_image='python:3.12', space_id=None, command=['python', '-c', "print('Hello from the cloud!')"], arguments=[], environment={}, secrets={}, flavor='cpu-basic', status=JobStatus(stage='RUNNING', message=None), owner=JobOwner(id='5e9ecfc04957053f60648a3e', name='lhoestq', type='user'), endpoint='https://huggingface.co', url='https://huggingface.co/jobs/lhoestq/687fb701029421ae5549d998')
  87. >>> job.id
  88. '687fb701029421ae5549d998'
  89. >>> job.url
  90. 'https://huggingface.co/jobs/lhoestq/687fb701029421ae5549d998'
  91. >>> job.status.stage
  92. 'RUNNING'
  93. ```
  94. """
  95. id: str
  96. created_at: Optional[datetime]
  97. docker_image: Optional[str]
  98. space_id: Optional[str]
  99. command: Optional[List[str]]
  100. arguments: Optional[List[str]]
  101. environment: Optional[Dict[str, Any]]
  102. secrets: Optional[Dict[str, Any]]
  103. flavor: Optional[SpaceHardware]
  104. status: JobStatus
  105. owner: JobOwner
  106. # Inferred fields
  107. endpoint: str
  108. url: str
  109. def __init__(self, **kwargs) -> None:
  110. self.id = kwargs["id"]
  111. created_at = kwargs.get("createdAt") or kwargs.get("created_at")
  112. self.created_at = parse_datetime(created_at) if created_at else None
  113. self.docker_image = kwargs.get("dockerImage") or kwargs.get("docker_image")
  114. self.space_id = kwargs.get("spaceId") or kwargs.get("space_id")
  115. owner = kwargs.get("owner", {})
  116. self.owner = JobOwner(id=owner["id"], name=owner["name"], type=owner["type"])
  117. self.command = kwargs.get("command")
  118. self.arguments = kwargs.get("arguments")
  119. self.environment = kwargs.get("environment")
  120. self.secrets = kwargs.get("secrets")
  121. self.flavor = kwargs.get("flavor")
  122. status = kwargs.get("status", {})
  123. self.status = JobStatus(stage=status["stage"], message=status.get("message"))
  124. # Inferred fields
  125. self.endpoint = kwargs.get("endpoint", constants.ENDPOINT)
  126. self.url = f"{self.endpoint}/jobs/{self.owner.name}/{self.id}"
  127. @dataclass
  128. class JobSpec:
  129. docker_image: Optional[str]
  130. space_id: Optional[str]
  131. command: Optional[List[str]]
  132. arguments: Optional[List[str]]
  133. environment: Optional[Dict[str, Any]]
  134. secrets: Optional[Dict[str, Any]]
  135. flavor: Optional[SpaceHardware]
  136. timeout: Optional[int]
  137. tags: Optional[List[str]]
  138. arch: Optional[str]
  139. def __init__(self, **kwargs) -> None:
  140. self.docker_image = kwargs.get("dockerImage") or kwargs.get("docker_image")
  141. self.space_id = kwargs.get("spaceId") or kwargs.get("space_id")
  142. self.command = kwargs.get("command")
  143. self.arguments = kwargs.get("arguments")
  144. self.environment = kwargs.get("environment")
  145. self.secrets = kwargs.get("secrets")
  146. self.flavor = kwargs.get("flavor")
  147. self.timeout = kwargs.get("timeout")
  148. self.tags = kwargs.get("tags")
  149. self.arch = kwargs.get("arch")
  150. @dataclass
  151. class LastJobInfo:
  152. id: str
  153. at: datetime
  154. def __init__(self, **kwargs) -> None:
  155. self.id = kwargs["id"]
  156. self.at = parse_datetime(kwargs["at"])
  157. @dataclass
  158. class ScheduledJobStatus:
  159. last_job: Optional[LastJobInfo]
  160. next_job_run_at: Optional[datetime]
  161. def __init__(self, **kwargs) -> None:
  162. last_job = kwargs.get("lastJob") or kwargs.get("last_job")
  163. self.last_job = LastJobInfo(**last_job) if last_job else None
  164. next_job_run_at = kwargs.get("nextJobRunAt") or kwargs.get("next_job_run_at")
  165. self.next_job_run_at = parse_datetime(str(next_job_run_at)) if next_job_run_at else None
  166. @dataclass
  167. class ScheduledJobInfo:
  168. """
  169. Contains information about a Job.
  170. Args:
  171. id (`str`):
  172. Scheduled Job ID.
  173. created_at (`datetime` or `None`):
  174. When the scheduled Job was created.
  175. tags (`List[str]` or `None`):
  176. The tags of the scheduled Job.
  177. schedule (`str` or `None`):
  178. One of "@annually", "@yearly", "@monthly", "@weekly", "@daily", "@hourly", or a
  179. CRON schedule expression (e.g., '0 9 * * 1' for 9 AM every Monday).
  180. suspend (`bool` or `None`):
  181. Whether the scheduled job is suspended (paused).
  182. concurrency (`bool` or `None`):
  183. Whether multiple instances of this Job can run concurrently.
  184. status (`ScheduledJobStatus` or `None`):
  185. Status of the scheduled Job.
  186. owner: (`JobOwner` or `None`):
  187. Owner of the scheduled Job, e.g. `JobOwner(id="5e9ecfc04957053f60648a3e", name="lhoestq", type="user")`
  188. job_spec: (`JobSpec` or `None`):
  189. Specifications of the Job.
  190. Example:
  191. ```python
  192. >>> from huggingface_hub import run_job
  193. >>> scheduled_job = create_scheduled_job(
  194. ... image="python:3.12",
  195. ... command=["python", "-c", "print('Hello from the cloud!')"],
  196. ... schedule="@hourly",
  197. ... )
  198. >>> scheduled_job.id
  199. '687fb701029421ae5549d999'
  200. >>> scheduled_job.status.next_job_run_at
  201. datetime.datetime(2025, 7, 22, 17, 6, 25, 79000, tzinfo=datetime.timezone.utc)
  202. ```
  203. """
  204. id: str
  205. created_at: Optional[datetime]
  206. job_spec: JobSpec
  207. schedule: Optional[str]
  208. suspend: Optional[bool]
  209. concurrency: Optional[bool]
  210. status: ScheduledJobStatus
  211. owner: JobOwner
  212. def __init__(self, **kwargs) -> None:
  213. self.id = kwargs["id"]
  214. created_at = kwargs.get("createdAt") or kwargs.get("created_at")
  215. self.created_at = parse_datetime(created_at) if created_at else None
  216. self.job_spec = JobSpec(**(kwargs.get("job_spec") or kwargs.get("jobSpec", {})))
  217. self.schedule = kwargs.get("schedule")
  218. self.suspend = kwargs.get("suspend")
  219. self.concurrency = kwargs.get("concurrency")
  220. status = kwargs.get("status", {})
  221. self.status = ScheduledJobStatus(
  222. last_job=status.get("last_job") or status.get("lastJob"),
  223. next_job_run_at=status.get("next_job_run_at") or status.get("nextJobRunAt"),
  224. )
  225. owner = kwargs.get("owner", {})
  226. self.owner = JobOwner(id=owner["id"], name=owner["name"], type=owner["type"])
  227. def _create_job_spec(
  228. *,
  229. image: str,
  230. command: List[str],
  231. env: Optional[Dict[str, Any]],
  232. secrets: Optional[Dict[str, Any]],
  233. flavor: Optional[SpaceHardware],
  234. timeout: Optional[Union[int, float, str]],
  235. ) -> Dict[str, Any]:
  236. # prepare job spec to send to HF Jobs API
  237. job_spec: Dict[str, Any] = {
  238. "command": command,
  239. "arguments": [],
  240. "environment": env or {},
  241. "flavor": flavor or SpaceHardware.CPU_BASIC,
  242. }
  243. # secrets are optional
  244. if secrets:
  245. job_spec["secrets"] = secrets
  246. # timeout is optional
  247. if timeout:
  248. time_units_factors = {"s": 1, "m": 60, "h": 3600, "d": 3600 * 24}
  249. if isinstance(timeout, str) and timeout[-1] in time_units_factors:
  250. job_spec["timeoutSeconds"] = int(float(timeout[:-1]) * time_units_factors[timeout[-1]])
  251. else:
  252. job_spec["timeoutSeconds"] = int(timeout)
  253. # input is either from docker hub or from HF spaces
  254. for prefix in (
  255. "https://huggingface.co/spaces/",
  256. "https://hf.co/spaces/",
  257. "huggingface.co/spaces/",
  258. "hf.co/spaces/",
  259. ):
  260. if image.startswith(prefix):
  261. job_spec["spaceId"] = image[len(prefix) :]
  262. break
  263. else:
  264. job_spec["dockerImage"] = image
  265. return job_spec