Skip to content

Commit 786756b

Browse files
committed
Add support for composer download URLs
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 03e1dfe commit 786756b

File tree

5 files changed

+164
-6
lines changed

5 files changed

+164
-6
lines changed

src/fetchcode/composer.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# fetchcode is a free software tool from nexB Inc. and others.
2+
# Visit https://github.com/aboutcode-org/fetchcode for support and download.
3+
#
4+
# Copyright (c) nexB Inc. and others. All rights reserved.
5+
# http://nexb.com and http://aboutcode.org
6+
#
7+
# This software is licensed under the Apache License version 2.0.
8+
#
9+
# You may not use this software except in compliance with the License.
10+
# You may obtain a copy of the License at:
11+
# http://apache.org/licenses/LICENSE-2.0
12+
# Unless required by applicable law or agreed to in writing, software distributed
13+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
14+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations under the License.
16+
17+
from packageurl import PackageURL
18+
19+
from fetchcode import fetch_json_response
20+
21+
22+
class Composer:
23+
24+
purl_pattern = "pkg:composer/.*"
25+
base_url = "https://repo.packagist.org"
26+
27+
@classmethod
28+
def get_download_url(cls, purl):
29+
30+
"""
31+
Return the download URL for a Composer PURL.
32+
"""
33+
purl = PackageURL.from_string(purl)
34+
35+
if not purl.name or not purl.version:
36+
raise ValueError("Composer PURL must specify a name and version")
37+
38+
name = f"{purl.namespace}/{purl.name}" if purl.namespace else purl.name
39+
40+
url = f"{cls.base_url}/p2/{name}.json "
41+
data = fetch_json_response(url)
42+
43+
if "packages" not in data:
44+
return
45+
46+
if name not in data["packages"]:
47+
return
48+
49+
for package in data["packages"][name]:
50+
if package["version"] == purl.version:
51+
download_url = package["dist"].get("url")
52+
return download_url

src/fetchcode/cpan.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# fetchcode is a free software tool from nexB Inc. and others.
2+
# Visit https://github.com/aboutcode-org/fetchcode for support and download.
3+
#
4+
# Copyright (c) nexB Inc. and others. All rights reserved.
5+
# http://nexb.com and http://aboutcode.org
6+
#
7+
# This software is licensed under the Apache License version 2.0.
8+
#
9+
# You may not use this software except in compliance with the License.
10+
# You may obtain a copy of the License at:
11+
# http://apache.org/licenses/LICENSE-2.0
12+
# Unless required by applicable law or agreed to in writing, software distributed
13+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
14+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations under the License.
16+
17+
from packageurl import PackageURL
18+
19+
from fetchcode import fetch_json_response
20+
from fetchcode.utils import _http_exists
21+
22+
23+
class CPAN:
24+
purl_pattern = "pkg:cpan/.*"
25+
base_url = "https://cpan.metacpan.org/"
26+
27+
def get_download_url(purl: str):
28+
"""
29+
Resolve a CPAN PURL to a verified, downloadable archive URL.
30+
Strategy: MetaCPAN API -> verified URL; fallback to author-based path if available.
31+
"""
32+
p = PackageURL.from_string(purl)
33+
if not p.name or not p.version:
34+
return None
35+
36+
try:
37+
api = f"https://fastapi.metacpan.org/v1/release/{urllib.parse.quote(p.name)}/{urllib.parse.quote(p.version)}"
38+
data = fetch_json_response(api, stream=False, timeout=20)
39+
url = data.get("download_url") or data.get("archive")
40+
if url and _http_exists(url):
41+
return url
42+
except Exception:
43+
pass
44+
45+
author = p.namespace
46+
if author:
47+
auth = author.upper()
48+
a = auth[0]
49+
ab = auth[:2] if len(auth) >= 2 else auth
50+
for ext in (".tar.gz", ".zip"):
51+
url = f"https://cpan.metacpan.org/authors/id/{a}/{ab}/{auth}/{p.name}-{p.version}{ext}"
52+
if _http_exists(url):
53+
return url

src/fetchcode/cran.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from fetchcode.utils import _http_exists
2020

2121

22-
class Cran:
22+
class CRAN:
2323
"""
2424
This class handles CRAN PURLs.
2525
"""
@@ -44,5 +44,3 @@ def get_download_url(cls, purl: str):
4444
archive_url = f"{cls.base_url}/src/contrib/Archive/{p.name}/{p.name}_{p.version}.tar.gz"
4545
if _http_exists(archive_url):
4646
return archive_url
47-
48-
return None

src/fetchcode/download_urls.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
from packageurl.contrib.route import NoRouteAvailable
1818
from packageurl.contrib.route import Router
1919

20+
from fetchcode.composer import Composer
21+
from fetchcode.cpan import CPAN
22+
from fetchcode.cran import CRAN
23+
from fetchcode.huggingface import Huggingface
2024
from fetchcode.pypi import Pypi
2125

22-
package_registry = [
23-
Pypi,
24-
]
26+
package_registry = [Pypi, CRAN, CPAN, Huggingface, Composer]
2527

2628
router = Router()
2729

src/fetchcode/huggingface.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# fetchcode is a free software tool from nexB Inc. and others.
2+
# Visit https://github.com/aboutcode-org/fetchcode for support and download.
3+
#
4+
# Copyright (c) nexB Inc. and others. All rights reserved.
5+
# http://nexb.com and http://aboutcode.org
6+
#
7+
# This software is licensed under the Apache License version 2.0.
8+
#
9+
# You may not use this software except in compliance with the License.
10+
# You may obtain a copy of the License at:
11+
# http://apache.org/licenses/LICENSE-2.0
12+
# Unless required by applicable law or agreed to in writing, software distributed
13+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
14+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations under the License.
16+
17+
from packageurl import PackageURL
18+
19+
from fetchcode import fetch_json_response
20+
21+
22+
class Huggingface:
23+
"""
24+
This class handles huggingface PURLs.
25+
"""
26+
27+
purl_pattern = "pkg:huggingface/.*"
28+
29+
@classmethod
30+
def get_download_url(cls, purl: str):
31+
"""
32+
Return the download URL for a Hugging Face PURL.
33+
"""
34+
p = PackageURL.from_string(purl)
35+
if not p.name:
36+
return None
37+
38+
revision = p.version or "main"
39+
model_id = p.name
40+
q = p.qualifiers or {}
41+
42+
api_url = f"https://huggingface.co/api/models/{model_id}?revision={revision}"
43+
data = fetch_json_response(api_url)
44+
siblings = data.get("siblings", [])
45+
46+
ALLOWED_EXECUTABLE_EXTS = (".bin",)
47+
48+
for sib in siblings:
49+
file_name = sib.get("rfilename")
50+
if not file_name.endswith(ALLOWED_EXECUTABLE_EXTS):
51+
continue
52+
url = f"https://huggingface.co/{model_id}/resolve/{revision}/{file_name}"
53+
return url

0 commit comments

Comments
 (0)