codekingpro/portable-devtools
114k
1import os2import re3from pathlib import Path4from typing import Any, Literal5 6import yaml7 8from huggingface_hub.file_download import hf_hub_download9from huggingface_hub.hf_api import upload_file10from huggingface_hub.repocard_data import (11 CardData,12 DatasetCardData,13 EvalResult,14 ModelCardData,15 SpaceCardData,16 eval_results_to_model_index,17 model_index_to_eval_results,18)19from huggingface_hub.utils import HfHubHTTPError, get_session, hf_raise_for_status, is_jinja_available, yaml_dump20 21from . import constants22from .errors import EntryNotFoundError23from .utils import SoftTemporaryDirectory, logging, validate_hf_hub_args24 25 26logger = logging.get_logger(__name__)27 28 29TEMPLATE_MODELCARD_PATH = Path(__file__).parent / "templates" / "modelcard_template.md"30TEMPLATE_DATASETCARD_PATH = Path(__file__).parent / "templates" / "datasetcard_template.md"31 32# exact same regex as in the Hub server. Please keep in sync.33# See https://github.com/huggingface/moon-landing/blob/main/server/lib/ViewMarkdown.ts#L1834REGEX_YAML_BLOCK = re.compile(r"^(\s*---[\r\n]+)([\S\s]*?)([\r\n]+---(\r\n|\n|$))")35 36 37class RepoCard:38 card_data_class = CardData39 default_template_path = TEMPLATE_MODELCARD_PATH40 repo_type = "model"41 42 def __init__(self, content: str, ignore_metadata_errors: bool = False):43 """Initialize a RepoCard from string content. The content should be a44 Markdown file with a YAML block at the beginning and a Markdown body.45 46 Args:47 content (`str`): The content of the Markdown file.48 49 Example:50 ```python51 >>> from huggingface_hub.repocard import RepoCard52 >>> text = '''53 ... ---54 ... language: en55 ... license: mit56 ... ---57 ...58 ... # My repo59 ... '''60 >>> card = RepoCard(text)61 >>> card.data.to_dict()62 {'language': 'en', 'license': 'mit'}63 >>> card.text64 '\\n# My repo\\n'65 66 ```67 > [!TIP]68 > Raises the following error:69 >70 > - [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)71 > when the content of the repo card metadata is not a dictionary.72 """73 74 # Set the content of the RepoCard, as well as underlying .data and .text attributes.75 # See the `content` property setter for more details.76 self.ignore_metadata_errors = ignore_metadata_errors77 self.content = content78 79 @property80 def content(self):81 """The content of the RepoCard, including the YAML block and the Markdown body."""82 line_break = _detect_line_ending(self._content) or "\n"83 return f"---{line_break}{self.data.to_yaml(line_break=line_break, original_order=self._original_order)}{line_break}---{line_break}{self.text}"84 85 @content.setter86 def content(self, content: str):87 """Set the content of the RepoCard."""88 self._content = content89 90 match = REGEX_YAML_BLOCK.search(content)91 if match:92 # Metadata found in the YAML block93 yaml_block = match.group(2)94 self.text = content[match.end() :]95 data_dict = yaml.safe_load(yaml_block)96 97 if data_dict is None:98 data_dict = {}99 100 # The YAML block's data should be a dictionary101 if not isinstance(data_dict, dict):102 raise ValueError("repo card metadata block should be a dict")103 else:104 # Model card without metadata... create empty metadata105 logger.warning("Repo card metadata block was not found. Setting CardData to empty.")106 data_dict = {}107 self.text = content108 109 self.data = self.card_data_class(**data_dict, ignore_metadata_errors=self.ignore_metadata_errors)110 self._original_order = list(data_dict.keys())111 112 def __str__(self):113 return self.content114 115 def save(self, filepath: Path | str):116 r"""Save a RepoCard to a file.117 118 Args:119 filepath (`Union[Path, str]`): Filepath to the markdown file to save.120 121 Example:122 ```python123 >>> from huggingface_hub.repocard import RepoCard124 >>> card = RepoCard("---\nlanguage: en\n---\n# This is a test repo card")125 >>> card.save("/tmp/test.md")126 127 ```128 """129 filepath = Path(filepath)130 filepath.parent.mkdir(parents=True, exist_ok=True)131 # Preserve newlines as in the existing file.132 with open(filepath, mode="w", newline="", encoding="utf-8") as f:133 f.write(str(self))134 135 @classmethod136 def load(137 cls,138 repo_id_or_path: str | Path,139 repo_type: str | None = None,140 token: str | None = None,141 ignore_metadata_errors: bool = False,142 ):143 """Initialize a RepoCard from a Hugging Face Hub repo's README.md or a local filepath.144 145 Args:146 repo_id_or_path (`Union[str, Path]`):147 The repo ID associated with a Hugging Face Hub repo or a local filepath.148 repo_type (`str`, *optional*):149 The type of Hugging Face repo to push to. Defaults to None, which will use "model". Other options150 are "dataset" and "space". Not used when loading from a local filepath. If this is called from a child151 class, the default value will be the child class's `repo_type`.152 token (`str`, *optional*):153 Authentication token, obtained with `huggingface_hub.HfApi.login` method. Will default to the stored token.154 ignore_metadata_errors (`str`):155 If True, errors while parsing the metadata section will be ignored. Some information might be lost during156 the process. Use it at your own risk.157 158 Returns:159 [`huggingface_hub.repocard.RepoCard`]: The RepoCard (or subclass) initialized from the repo's160 README.md file or filepath.161 162 Example:163 ```python164 >>> from huggingface_hub.repocard import RepoCard165 >>> card = RepoCard.load("nateraw/food")166 >>> assert card.data.tags == ["generated_from_trainer", "image-classification", "pytorch"]167 168 ```169 """170 171 if Path(repo_id_or_path).is_file():172 card_path = Path(repo_id_or_path)173 elif isinstance(repo_id_or_path, str):174 card_path = Path(175 hf_hub_download(176 repo_id_or_path,177 constants.REPOCARD_NAME,178 repo_type=repo_type or cls.repo_type,179 token=token,180 )181 )182 else:183 raise ValueError(f"Cannot load RepoCard: path not found on disk ({repo_id_or_path}).")184 185 # Preserve newlines in the existing file.186 with card_path.open(mode="r", newline="", encoding="utf-8") as f:187 return cls(f.read(), ignore_metadata_errors=ignore_metadata_errors)188 189 def validate(self, repo_type: str | None = None):190 """Validates card against Hugging Face Hub's card validation logic.191 Using this function requires access to the internet, so it is only called192 internally by [`huggingface_hub.repocard.RepoCard.push_to_hub`].193 194 Args:195 repo_type (`str`, *optional*, defaults to "model"):196 The type of Hugging Face repo to push to. Options are "model", "dataset", and "space".197 If this function is called from a child class, the default will be the child class's `repo_type`.198 199 > [!TIP]200 > Raises the following errors:201 >202 > - [`ValueError`](https://docs.python.org/3/library/exceptions.html#ValueError)203 > if the card fails validation checks.204 > - [`HTTPError`](https://requests.readthedocs.io/en/latest/api/#requests.HTTPError)205 > if the request to the Hub API fails for any other reason.206 """207 208 # If repo type is provided, otherwise, use the repo type of the card.209 repo_type = repo_type or self.repo_type210 211 body = {212 "repoType": repo_type,213 "content": str(self),214 }215 headers = {"Accept": "text/plain"}216 217 try:218 response = get_session().post("https://huggingface.co/api/validate-yaml", json=body, headers=headers)219 hf_raise_for_status(response)220 except HfHubHTTPError as exc:221 if response.status_code == 400:222 raise ValueError(response.text)223 else:224 raise exc225 226 def push_to_hub(227 self,228 repo_id: str,229 token: str | None = None,230 repo_type: str | None = None,231 commit_message: str | None = None,232 commit_description: str | None = None,233 revision: str | None = None,234 create_pr: bool | None = None,235 parent_commit: str | None = None,236 ):237 """Push a RepoCard to a Hugging Face Hub repo.238 239 Args:240 repo_id (`str`):241 The repo ID of the Hugging Face Hub repo to push to. Example: "nateraw/food".242 token (`str`, *optional*):243 Authentication token, obtained with `huggingface_hub.HfApi.login` method. Will default to244 the stored token.245 repo_type (`str`, *optional*, defaults to "model"):246 The type of Hugging Face repo to push to. Options are "model", "dataset", and "space". If this247 function is called by a child class, it will default to the child class's `repo_type`.248 commit_message (`str`, *optional*):249 The summary / title / first line of the generated commit.250 commit_description (`str`, *optional*)251 The description of the generated commit.252 revision (`str`, *optional*):253 The git revision to commit from. Defaults to the head of the `"main"` branch.254 create_pr (`bool`, *optional*):255 Whether or not to create a Pull Request with this commit. Defaults to `False`.256 parent_commit (`str`, *optional*):257 The OID / SHA of the parent commit, as a hexadecimal string. Shorthands (7 first characters) are also supported.258 If specified and `create_pr` is `False`, the commit will fail if `revision` does not point to `parent_commit`.259 If specified and `create_pr` is `True`, the pull request will be created from `parent_commit`.260 Specifying `parent_commit` ensures the repo has not changed before committing the changes, and can be261 especially useful if the repo is updated / committed too concurrently.262 Returns:263 `str`: URL of the commit which updated the card metadata.264 """265 266 # If repo type is provided, otherwise, use the repo type of the card.267 repo_type = repo_type or self.repo_type268 269 # Validate card before pushing to hub270 self.validate(repo_type=repo_type)271 272 with SoftTemporaryDirectory() as tmpdir:273 tmp_path = Path(tmpdir) / constants.REPOCARD_NAME274 tmp_path.write_text(str(self), encoding="utf-8")275 url = upload_file(276 path_or_fileobj=str(tmp_path),277 path_in_repo=constants.REPOCARD_NAME,278 repo_id=repo_id,279 token=token,280 repo_type=repo_type,281 commit_message=commit_message,282 commit_description=commit_description,283 create_pr=create_pr,284 revision=revision,285 parent_commit=parent_commit,286 )287 return url288 289 @classmethod290 def from_template(291 cls,292 card_data: CardData,293 template_path: str | None = None,294 template_str: str | None = None,295 **template_kwargs,296 ):297 """Initialize a RepoCard from a template. By default, it uses the default template.298 299 Templates are Jinja2 templates that can be customized by passing keyword arguments.300 301 Args:302 card_data (`huggingface_hub.CardData`):303 A huggingface_hub.CardData instance containing the metadata you want to include in the YAML304 header of the repo card on the Hugging Face Hub.305 template_path (`str`, *optional*):306 A path to a markdown file with optional Jinja template variables that can be filled307 in with `template_kwargs`. Defaults to the default template.308 309 Returns:310 [`huggingface_hub.repocard.RepoCard`]: A RepoCard instance with the specified card data and content from the311 template.312 """313 if is_jinja_available():314 import jinja2315 else:316 raise ImportError(317 "Using RepoCard.from_template requires Jinja2 to be installed. Please"318 " install it with `pip install Jinja2`."319 )320 321 kwargs = card_data.to_dict().copy()322 kwargs.update(template_kwargs) # Template_kwargs have priority323 324 if template_path is not None:325 template_str = Path(template_path).read_text()326 if template_str is None:327 template_str = Path(cls.default_template_path).read_text()328 template = jinja2.Template(template_str)329 content = template.render(card_data=card_data.to_yaml(), **kwargs)330 return cls(content)331 332 333class ModelCard(RepoCard):334 card_data_class = ModelCardData # type: ignore[assignment]335 default_template_path = TEMPLATE_MODELCARD_PATH336 repo_type = "model"337 338 @classmethod339 def from_template( # type: ignore # violates Liskov property but easier to use340 cls,341 card_data: ModelCardData,342 template_path: str | None = None,343 template_str: str | None = None,344 **template_kwargs,345 ):346 """Initialize a ModelCard from a template. By default, it uses the default template, which can be found here:347 https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/templates/modelcard_template.md348 349 Templates are Jinja2 templates that can be customized by passing keyword arguments.350 351 Args:352 card_data (`huggingface_hub.ModelCardData`):353 A huggingface_hub.ModelCardData instance containing the metadata you want to include in the YAML354 header of the model card on the Hugging Face Hub.355 template_path (`str`, *optional*):356 A path to a markdown file with optional Jinja template variables that can be filled357 in with `template_kwargs`. Defaults to the default template.358 359 Returns:360 [`huggingface_hub.ModelCard`]: A ModelCard instance with the specified card data and content from the361 template.362 363 Example:364 ```python365 >>> from huggingface_hub import ModelCard, ModelCardData, EvalResult366 367 >>> # Using the Default Template368 >>> card_data = ModelCardData(369 ... language='en',370 ... license='mit',371 ... library_name='timm',372 ... tags=['image-classification', 'resnet'],373 ... datasets=['beans'],374 ... metrics=['accuracy'],375 ... )376 >>> card = ModelCard.from_template(377 ... card_data,378 ... model_description='This model does x + y...'379 ... )380 381 >>> # Including Evaluation Results382 >>> card_data = ModelCardData(383 ... language='en',384 ... tags=['image-classification', 'resnet'],385 ... eval_results=[386 ... EvalResult(387 ... task_type='image-classification',388 ... dataset_type='beans',389 ... dataset_name='Beans',390 ... metric_type='accuracy',391 ... metric_value=0.9,392 ... ),393 ... ],394 ... model_name='my-cool-model',395 ... )396 >>> card = ModelCard.from_template(card_data)397 398 >>> # Using a Custom Template399 >>> card_data = ModelCardData(400 ... language='en',401 ... tags=['image-classification', 'resnet']402 ... )403 >>> card = ModelCard.from_template(404 ... card_data=card_data,405 ... template_path='./src/huggingface_hub/templates/modelcard_template.md',406 ... custom_template_var='custom value', # will be replaced in template if it exists407 ... )408 409 ```410 """411 return super().from_template(card_data, template_path, template_str, **template_kwargs)412 413 414class DatasetCard(RepoCard):415 card_data_class = DatasetCardData # type: ignore[assignment]416 default_template_path = TEMPLATE_DATASETCARD_PATH417 repo_type = "dataset"418 419 @classmethod420 def from_template( # type: ignore # violates Liskov property but easier to use421 cls,422 card_data: DatasetCardData,423 template_path: str | None = None,424 template_str: str | None = None,425 **template_kwargs,426 ):427 """Initialize a DatasetCard from a template. By default, it uses the default template, which can be found here:428 https://github.com/huggingface/huggingface_hub/blob/main/src/huggingface_hub/templates/datasetcard_template.md429 430 Templates are Jinja2 templates that can be customized by passing keyword arguments.431 432 Args:433 card_data (`huggingface_hub.DatasetCardData`):434 A huggingface_hub.DatasetCardData instance containing the metadata you want to include in the YAML435 header of the dataset card on the Hugging Face Hub.436 template_path (`str`, *optional*):437 A path to a markdown file with optional Jinja template variables that can be filled438 in with `template_kwargs`. Defaults to the default template.439 440 Returns:441 [`huggingface_hub.DatasetCard`]: A DatasetCard instance with the specified card data and content from the442 template.443 444 Example:445 ```python446 >>> from huggingface_hub import DatasetCard, DatasetCardData447 448 >>> # Using the Default Template449 >>> card_data = DatasetCardData(450 ... language='en',451 ... license='mit',452 ... annotations_creators='crowdsourced',453 ... task_categories=['text-classification'],454 ... task_ids=['sentiment-classification', 'text-scoring'],455 ... multilinguality='monolingual',456 ... pretty_name='My Text Classification Dataset',457 ... )458 >>> card = DatasetCard.from_template(459 ... card_data,460 ... pretty_name=card_data.pretty_name,461 ... )462 463 >>> # Using a Custom Template464 >>> card_data = DatasetCardData(465 ... language='en',466 ... license='mit',467 ... )468 >>> card = DatasetCard.from_template(469 ... card_data=card_data,470 ... template_path='./src/huggingface_hub/templates/datasetcard_template.md',471 ... custom_template_var='custom value', # will be replaced in template if it exists472 ... )473 474 ```475 """476 return super().from_template(card_data, template_path, template_str, **template_kwargs)477 478 479class SpaceCard(RepoCard):480 card_data_class = SpaceCardData # type: ignore[assignment]481 default_template_path = TEMPLATE_MODELCARD_PATH482 repo_type = "space"483 484 485def _detect_line_ending(content: str) -> Literal["\r", "\n", "\r\n", None]: # noqa: F722486 """Detect the line ending of a string. Used by RepoCard to avoid making huge diff on newlines.487 488 Uses same implementation as in Hub server, keep it in sync.489 490 Returns:491 str: The detected line ending of the string.492 """493 cr = content.count("\r")494 lf = content.count("\n")495 crlf = content.count("\r\n")496 if cr + lf == 0:497 return None498 if crlf == cr and crlf == lf:499 return "\r\n"500 if cr > lf:501 return "\r"502 else:503 return "\n"504 505 506def metadata_load(local_path: str | Path) -> dict | None:507 content = Path(local_path).read_text()508 match = REGEX_YAML_BLOCK.search(content)509 if match:510 yaml_block = match.group(2)511 data = yaml.safe_load(yaml_block)512 if data is None or isinstance(data, dict):513 return data514 raise ValueError("repo card metadata block should be a dict")515 else:516 return None517 518 519def metadata_save(local_path: str | Path, data: dict) -> None:520 """521 Save the metadata dict in the upper YAML part Trying to preserve newlines as522 in the existing file. Docs about open() with newline="" parameter:523 https://docs.python.org/3/library/functions.html?highlight=open#open Does524 not work with "^M" linebreaks, which are replaced by \n525 """526 line_break = "\n"527 content = ""528 # try to detect existing newline character529 if os.path.exists(local_path):530 with open(local_path, newline="", encoding="utf8") as readme:531 content = readme.read()532 if isinstance(readme.newlines, tuple):533 line_break = readme.newlines[0]534 elif isinstance(readme.newlines, str):535 line_break = readme.newlines536 537 # creates a new file if it not538 with open(local_path, "w", newline="", encoding="utf8") as readme:539 data_yaml = yaml_dump(data, sort_keys=False, line_break=line_break)540 # sort_keys: keep dict order541 match = REGEX_YAML_BLOCK.search(content)542 if match:543 output = content[: match.start()] + f"---{line_break}{data_yaml}---{line_break}" + content[match.end() :]544 else:545 output = f"---{line_break}{data_yaml}---{line_break}{content}"546 547 readme.write(output)548 readme.close()549 550 551def metadata_eval_result(552 *,553 model_pretty_name: str,554 task_pretty_name: str,555 task_id: str,556 metrics_pretty_name: str,557 metrics_id: str,558 metrics_value: Any,559 dataset_pretty_name: str,560 dataset_id: str,561 metrics_config: str | None = None,562 metrics_verified: bool = False,563 dataset_config: str | None = None,564 dataset_split: str | None = None,565 dataset_revision: str | None = None,566 metrics_verification_token: str | None = None,567) -> dict:568 """569 Creates a metadata dict with the result from a model evaluated on a dataset.570 571 Args:572 model_pretty_name (`str`):573 The name of the model in natural language.574 task_pretty_name (`str`):575 The name of a task in natural language.576 task_id (`str`):577 Example: automatic-speech-recognition. A task id.578 metrics_pretty_name (`str`):579 A name for the metric in natural language. Example: Test WER.580 metrics_id (`str`):581 Example: wer. A metric id from https://hf.co/metrics.582 metrics_value (`Any`):583 The value from the metric. Example: 20.0 or "20.0 ± 1.2".584 dataset_pretty_name (`str`):585 The name of the dataset in natural language.586 dataset_id (`str`):587 Example: common_voice. A dataset id from https://hf.co/datasets.588 metrics_config (`str`, *optional*):589 The name of the metric configuration used in `load_metric()`.590 Example: bleurt-large-512 in `load_metric("bleurt", "bleurt-large-512")`.591 metrics_verified (`bool`, *optional*, defaults to `False`):592 Indicates whether the metrics originate from Hugging Face's [evaluation service](https://huggingface.co/spaces/autoevaluate/model-evaluator) or not. Automatically computed by Hugging Face, do not set.593 dataset_config (`str`, *optional*):594 Example: fr. The name of the dataset configuration used in `load_dataset()`.595 dataset_split (`str`, *optional*):596 Example: test. The name of the dataset split used in `load_dataset()`.597 dataset_revision (`str`, *optional*):598 Example: 5503434ddd753f426f4b38109466949a1217c2bb. The name of the dataset dataset revision599 used in `load_dataset()`.600 metrics_verification_token (`bool`, *optional*):601 A JSON Web Token that is used to verify whether the metrics originate from Hugging Face's [evaluation service](https://huggingface.co/spaces/autoevaluate/model-evaluator) or not.602 603 Returns:604 `dict`: a metadata dict with the result from a model evaluated on a dataset.605 606 Example:607 ```python608 >>> from huggingface_hub import metadata_eval_result609 >>> results = metadata_eval_result(610 ... model_pretty_name="RoBERTa fine-tuned on ReactionGIF",611 ... task_pretty_name="Text Classification",612 ... task_id="text-classification",613 ... metrics_pretty_name="Accuracy",614 ... metrics_id="accuracy",615 ... metrics_value=0.2662102282047272,616 ... dataset_pretty_name="ReactionJPEG",617 ... dataset_id="julien-c/reactionjpeg",618 ... dataset_config="default",619 ... dataset_split="test",620 ... )621 >>> results == {622 ... 'model-index': [623 ... {624 ... 'name': 'RoBERTa fine-tuned on ReactionGIF',625 ... 'results': [626 ... {627 ... 'task': {628 ... 'type': 'text-classification',629 ... 'name': 'Text Classification'630 ... },631 ... 'dataset': {632 ... 'name': 'ReactionJPEG',633 ... 'type': 'julien-c/reactionjpeg',634 ... 'config': 'default',635 ... 'split': 'test'636 ... },637 ... 'metrics': [638 ... {639 ... 'type': 'accuracy',640 ... 'value': 0.2662102282047272,641 ... 'name': 'Accuracy',642 ... 'verified': False643 ... }644 ... ]645 ... }646 ... ]647 ... }648 ... ]649 ... }650 True651 652 ```653 """654 655 return {656 "model-index": eval_results_to_model_index(657 model_name=model_pretty_name,658 eval_results=[659 EvalResult(660 task_name=task_pretty_name,661 task_type=task_id,662 metric_name=metrics_pretty_name,663 metric_type=metrics_id,664 metric_value=metrics_value,665 dataset_name=dataset_pretty_name,666 dataset_type=dataset_id,667 metric_config=metrics_config,668 verified=metrics_verified,669 verify_token=metrics_verification_token,670 dataset_config=dataset_config,671 dataset_split=dataset_split,672 dataset_revision=dataset_revision,673 )674 ],675 )676 }677 678 679@validate_hf_hub_args680def metadata_update(681 repo_id: str,682 metadata: dict,683 *,684 repo_type: str | None = None,685 overwrite: bool = False,686 token: str | None = None,687 commit_message: str | None = None,688 commit_description: str | None = None,689 revision: str | None = None,690 create_pr: bool = False,691 parent_commit: str | None = None,692) -> str:693 """694 Updates the metadata in the README.md of a repository on the Hugging Face Hub.695 If the README.md file doesn't exist yet, a new one is created with metadata and696 the default ModelCard or DatasetCard template. For `space` repo, an error is thrown697 as a Space cannot exist without a `README.md` file.698 699 Args:700 repo_id (`str`):701 The name of the repository.702 metadata (`dict`):703 A dictionary containing the metadata to be updated.704 repo_type (`str`, *optional*):705 Set to `"dataset"` or `"space"` if updating to a dataset or space,706 `None` or `"model"` if updating to a model. Default is `None`.707 overwrite (`bool`, *optional*, defaults to `False`):708 If set to `True` an existing field can be overwritten, otherwise709 attempting to overwrite an existing field will cause an error.710 token (`str`, *optional*):711 The Hugging Face authentication token.712 commit_message (`str`, *optional*):713 The summary / title / first line of the generated commit. Defaults to714 `f"Update metadata with huggingface_hub"`715 commit_description (`str` *optional*)716 The description of the generated commit717 revision (`str`, *optional*):718 The git revision to commit from. Defaults to the head of the719 `"main"` branch.720 create_pr (`boolean`, *optional*):721 Whether or not to create a Pull Request from `revision` with that commit.722 Defaults to `False`.723 parent_commit (`str`, *optional*):724 The OID / SHA of the parent commit, as a hexadecimal string. Shorthands (7 first characters) are also supported.725 If specified and `create_pr` is `False`, the commit will fail if `revision` does not point to `parent_commit`.726 If specified and `create_pr` is `True`, the pull request will be created from `parent_commit`.727 Specifying `parent_commit` ensures the repo has not changed before committing the changes, and can be728 especially useful if the repo is updated / committed too concurrently.729 Returns:730 `str`: URL of the commit which updated the card metadata.731 732 Example:733 ```python734 >>> from huggingface_hub import metadata_update735 >>> metadata = {'model-index': [{'name': 'RoBERTa fine-tuned on ReactionGIF',736 ... 'results': [{'dataset': {'name': 'ReactionGIF',737 ... 'type': 'julien-c/reactiongif'},738 ... 'metrics': [{'name': 'Recall',739 ... 'type': 'recall',740 ... 'value': 0.7762102282047272}],741 ... 'task': {'name': 'Text Classification',742 ... 'type': 'text-classification'}}]}]}743 >>> url = metadata_update("hf-internal-testing/reactiongif-roberta-card", metadata)744 745 ```746 """747 commit_message = commit_message if commit_message is not None else "Update metadata with huggingface_hub"748 749 # Card class given repo_type750 card_class: type[RepoCard]751 if repo_type is None or repo_type == "model":752 card_class = ModelCard753 elif repo_type == "dataset":754 card_class = DatasetCard755 elif repo_type == "space":756 card_class = RepoCard757 else:758 raise ValueError(f"Unknown repo_type: {repo_type}")759 760 # Either load repo_card from the Hub or create an empty one.761 # NOTE: Will not create the repo if it doesn't exist.762 try:763 card = card_class.load(repo_id, token=token, repo_type=repo_type)764 except EntryNotFoundError:765 if repo_type == "space":766 raise ValueError("Cannot update metadata on a Space that doesn't contain a `README.md` file.")767 768 # Initialize a ModelCard or DatasetCard from default template and no data.769 # Cast to the concrete expected card type to satisfy type checkers.770 card = card_class.from_template(CardData()) # type: ignore771 772 for key, value in metadata.items():773 if key == "model-index":774 # if the new metadata doesn't include a name, either use existing one or repo name775 if "name" not in value[0]:776 value[0]["name"] = getattr(card, "model_name", repo_id)777 model_name, new_results = model_index_to_eval_results(value)778 if card.data.eval_results is None:779 card.data.eval_results = new_results780 card.data.model_name = model_name781 else:782 existing_results = card.data.eval_results783 784 # Iterate over new results785 # Iterate over existing results786 # If both results describe the same metric but value is different:787 # If overwrite=True: overwrite the metric value788 # Else: raise ValueError789 # Else: append new result to existing ones.790 for new_result in new_results:791 result_found = False792 for existing_result in existing_results:793 if new_result.is_equal_except_value(existing_result):794 if new_result != existing_result and not overwrite:795 raise ValueError(796 "You passed a new value for the existing metric"797 f" 'name: {new_result.metric_name}, type: "798 f"{new_result.metric_type}'. Set `overwrite=True`"799 " to overwrite existing metrics."800 )801 result_found = True802 existing_result.metric_value = new_result.metric_value803 if existing_result.verified is True:804 existing_result.verify_token = new_result.verify_token805 if not result_found:806 card.data.eval_results.append(new_result)807 else:808 # Any metadata that is not a result metric809 if card.data.get(key) is not None and not overwrite and card.data.get(key) != value:810 raise ValueError(811 f"You passed a new value for the existing meta data field '{key}'."812 " Set `overwrite=True` to overwrite existing metadata."813 )814 else:815 card.data[key] = value816 817 return card.push_to_hub(818 repo_id,819 token=token,820 repo_type=repo_type,821 commit_message=commit_message,822 commit_description=commit_description,823 create_pr=create_pr,824 revision=revision,825 parent_commit=parent_commit,826 )827 