11import re
22from typing import Any
3+ from urllib .parse import quote
34
45import httpx
56
1213# could reach an unintended API path.
1314_MODEL_ID_RE = re .compile (r"^[\w][\w.-]*/[\w][\w.-]*$" )
1415_PACKAGE_NAME_RE = re .compile (r"^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$" )
16+ _PACKAGE_VERSION_RE = re .compile (r"^[A-Za-z0-9](?:[A-Za-z0-9.!+_-]*[A-Za-z0-9])?$" )
1517
1618_UNRESOLVED = Resolution (licence = None , resolved = False , source_url = None )
1719
1820
21+ def _valid_hf_revision (revision : str ) -> bool :
22+ if any (ord (character ) < 32 or ord (character ) == 127 for character in revision ):
23+ return False
24+ return all (segment not in {"" , "." , ".." } for segment in revision .split ("/" ))
25+
26+
1927def _extract_hf_licence (data : dict [str , Any ]) -> str | None :
2028 """Verified live this session against real HF API responses: `cardData.license` can be a
2129 plain string (e.g. a gated Llama model's own non-SPDX slug, "llama3.2") or a list of
@@ -35,7 +43,13 @@ def _extract_hf_licence(data: dict[str, Any]) -> str | None:
3543 return None
3644
3745
38- def _resolve_hf_repo (client : httpx .Client , repo_id : str , * , api_segment : str ) -> Resolution :
46+ def _resolve_hf_repo (
47+ client : httpx .Client ,
48+ repo_id : str ,
49+ * ,
50+ api_segment : str ,
51+ revision : str | None = None ,
52+ ) -> Resolution :
3953 """Shared by resolve_model/resolve_dataset — both endpoints share the exact same
4054 `cardData.license` shape (confirmed live this session against real datasets too:
4155 openai/gsm8k, Salesforce/wikitext), so this is genuinely identical logic, not merely
@@ -44,6 +58,13 @@ def _resolve_hf_repo(client: httpx.Client, repo_id: str, *, api_segment: str) ->
4458 return _UNRESOLVED
4559
4660 url = f"https://huggingface.co/api/{ api_segment } /{ repo_id } "
61+ if revision is not None :
62+ if not _valid_hf_revision (revision ):
63+ return _UNRESOLVED
64+ # This is the exact revision route and quoting rule used by the current official
65+ # huggingface_hub HfApi model_info/dataset_info clients. A slash in a branch such as
66+ # refs/pr/7 is one revision value, never additional URL structure.
67+ url = f"{ url } /revision/{ quote (revision , safe = '' )} "
4768 try :
4869 response = client .get (url )
4970 except httpx .HTTPError :
@@ -62,19 +83,29 @@ def _resolve_hf_repo(client: httpx.Client, repo_id: str, *, api_segment: str) ->
6283 return Resolution (licence = licence , resolved = True , source_url = url )
6384
6485
65- def resolve_model (client : httpx .Client , model_id : str ) -> Resolution :
66- return _resolve_hf_repo (client , model_id , api_segment = "models" )
86+ def resolve_model (
87+ client : httpx .Client , model_id : str , * , revision : str | None = None
88+ ) -> Resolution :
89+ return _resolve_hf_repo (client , model_id , api_segment = "models" , revision = revision )
6790
6891
69- def resolve_dataset (client : httpx .Client , dataset_id : str ) -> Resolution :
70- return _resolve_hf_repo (client , dataset_id , api_segment = "datasets" )
92+ def resolve_dataset (
93+ client : httpx .Client , dataset_id : str , * , revision : str | None = None
94+ ) -> Resolution :
95+ return _resolve_hf_repo (client , dataset_id , api_segment = "datasets" , revision = revision )
7196
7297
73- def resolve_package (client : httpx .Client , name : str ) -> Resolution :
98+ def resolve_package (client : httpx .Client , name : str , * , version : str | None = None ) -> Resolution :
7499 if not _PACKAGE_NAME_RE .match (name ):
75100 return _UNRESOLVED
76101
77102 url = f"https://pypi.org/pypi/{ name } /json"
103+ if version is not None :
104+ if not _PACKAGE_VERSION_RE .match (version ):
105+ return _UNRESOLVED
106+ # PyPI documents this release-specific route separately from the project route,
107+ # whose info block describes only the latest release.
108+ url = f"https://pypi.org/pypi/{ name } /{ quote (version , safe = '' )} /json"
78109 try :
79110 response = client .get (url )
80111 except httpx .HTTPError :
0 commit comments