Skip to content

Commit 0d45a34

Browse files
committed
Support custom Artifactory repositories for package metadata
Signed-off-by: Kai Hodžić <hodzic.e.k@outlook.com>
1 parent a841c7c commit 0d45a34

3 files changed

Lines changed: 125 additions & 22 deletions

File tree

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ v0.15.1
1010

1111
- Fetch package metadata from private artifactory if specified https://github.com/aboutcode-org/python-inspector/pull/261
1212
- Add zip file cache validation https://github.com/aboutcode-org/python-inspector/pull/256
13+
- Support custom Artifactory repositories with filename-based URL matching,
14+
VCS URL extraction from project_urls, source artifact metadata extraction,
15+
and metadata enrichment from PyPI.org fallback https://github.com/aboutcode-org/python-inspector/pull/258
1316

1417

1518
v0.15.0

src/python_inspector/package_data.py

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#
1111

1212
import os
13+
import posixpath
1314
from urllib.parse import urlparse, urlunparse
1415

1516
from typing import Dict
@@ -29,6 +30,20 @@
2930
from python_inspector.utils_pypi import PypiSimpleRepository
3031

3132

33+
def get_sdist_from_urls(urls: list) -> Optional[dict]:
34+
"""Extract source distribution info from PyPI urls array."""
35+
for entry in urls or []:
36+
if entry.get("packagetype") == "sdist":
37+
return {
38+
"url": entry.get("url", ""),
39+
"sha256": entry.get("digests", {}).get("sha256", ""),
40+
"md5": entry.get("digests", {}).get("md5") or entry.get("md5_digest", ""),
41+
"size": entry.get("size"),
42+
"filename": entry.get("filename", ""),
43+
}
44+
return None
45+
46+
3247
async def get_pypi_data_from_purl(
3348
purl: str,
3449
environment: Environment,
@@ -51,33 +66,52 @@ async def get_pypi_data_from_purl(
5166
if not version:
5267
raise Exception("Version is not specified in the purl")
5368

54-
# Todo: address the case where several index URLs are passed
55-
if index_urls:
56-
# Backward compatibility: If pypi.org is passed as index url, always resolve against it.
57-
# When multiple index URLs are supported and the todo above is fixed, then this hack can be removed.
58-
if "https://pypi.org/simple" in index_urls:
59-
index_url = None
60-
else:
61-
index_url = index_urls[0]
62-
else:
63-
index_url = None
69+
api_urls = []
70+
pypi_org_url = f"https://pypi.org/pypi/{name}/{version}/json"
71+
for index_url in index_urls or []:
72+
if index_url == "https://pypi.org/simple":
73+
continue
74+
base_path = index_url.removesuffix("/simple") + "/pypi"
75+
api_urls.append((base_path, f"{base_path}/{name}/{version}/json"))
76+
api_urls.append(("https://pypi.org/pypi", pypi_org_url))
6477

65-
base_path = (
66-
index_url.removesuffix("/simple") + "/pypi" if index_url else "https://pypi.org/pypi"
67-
)
78+
from python_inspector.utils import get_response_async
6879

69-
api_url = f"{base_path}/{name}/{version}/json"
80+
response = None
81+
api_url = None
82+
base_path = None
83+
info = {}
84+
for bp, url in api_urls:
85+
repo_response = await get_response_async(url)
86+
if not repo_response:
87+
continue
7088

71-
from python_inspector.utils import get_response_async
89+
if not response:
90+
response = repo_response
91+
api_url = url
92+
base_path = bp
93+
info = response.get("info") or {}
94+
95+
if not info.get("project_urls"):
96+
repo_info = repo_response.get("info") or {}
97+
info["project_urls"] = repo_info.get("project_urls")
98+
99+
if info.get("project_urls"):
100+
break
72101

73-
response = await get_response_async(api_url)
74102
if not response:
75103
return None
76104

77-
info = response.get("info") or {}
105+
sdist_info = get_sdist_from_urls(response.get("urls", []))
78106
homepage_url = info.get("home_page")
79107
project_urls = info.get("project_urls") or {}
108+
80109
code_view_url = get_pypi_codeview_url(project_urls)
110+
vcs_url = None
111+
if code_view_url:
112+
vcs_url = code_view_url.rstrip("/")
113+
if not vcs_url.endswith(".git"):
114+
vcs_url = vcs_url + ".git"
81115
bug_tracking_url = get_pypi_bugtracker_url(project_urls)
82116
python_version = get_python_version_from_env_tag(python_version=environment.python_version)
83117
valid_distribution_urls = []
@@ -145,6 +179,12 @@ def canonicalize_url(url: str):
145179

146180
urls_sanitized[url_sanitized] = value
147181

182+
urls_by_filename = {
183+
posixpath.basename(urlparse(e.get("url")).path): e
184+
for e in response.get("urls") or []
185+
if e.get("url")
186+
}
187+
148188
def remove_credentials_from_url(url: str):
149189
# Parse the URL into its components
150190
parsed = urlparse(url)
@@ -156,17 +196,18 @@ def remove_credentials_from_url(url: str):
156196
# Create a new parsed result object, replacing the old netloc
157197
# with our new one that has no credentials.
158198
parsed = parsed._replace(netloc=new_netloc)
159-
url_without_credentials = urlunparse(parsed)
160-
161-
return url_without_credentials
199+
return urlunparse(parsed)
162200

163201
# iterate over the valid distribution urls and return the first
164202
# one that is matching.
165203
for dist_url in valid_distribution_urls:
166-
if dist_url not in urls_sanitized:
204+
url_data = urls_sanitized.get(dist_url)
205+
if not url_data:
206+
filename = posixpath.basename(urlparse(dist_url).path)
207+
url_data = urls_by_filename.get(filename)
208+
if not url_data:
167209
continue
168210

169-
url_data = urls_sanitized.get(dist_url)
170211
digests = url_data.get("digests") or {}
171212

172213
return PackageData(
@@ -176,6 +217,8 @@ def remove_credentials_from_url(url: str):
176217
api_data_url=remove_credentials_from_url(api_url),
177218
bug_tracking_url=bug_tracking_url,
178219
code_view_url=code_view_url,
220+
extra_data={"source_artifact": sdist_info} if sdist_info else {},
221+
vcs_url=vcs_url,
179222
license_expression=info.get("license_expression"),
180223
declared_license=get_declared_license(info),
181224
download_url=remove_credentials_from_url(dist_url),

tests/test_package_data.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Copyright (c) nexB Inc. and others. All rights reserved.
5+
# ScanCode is a trademark of nexB Inc.
6+
# SPDX-License-Identifier: Apache-2.0
7+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
8+
# See https://github.com/aboutcode-org/python-inspector for support or download.
9+
# See https://aboutcode.org for more information about nexB OSS projects.
10+
#
11+
12+
from python_inspector.package_data import get_pypi_codeview_url
13+
from python_inspector.package_data import get_sdist_from_urls
14+
15+
16+
def test_get_pypi_codeview_url():
17+
assert (
18+
get_pypi_codeview_url({"Source": "https://github.com/psf/requests"})
19+
== "https://github.com/psf/requests"
20+
)
21+
assert (
22+
get_pypi_codeview_url({"Code": "https://github.com/psf/requests"})
23+
== "https://github.com/psf/requests"
24+
)
25+
assert (
26+
get_pypi_codeview_url({"Source Code": "https://github.com/psf/requests"})
27+
== "https://github.com/psf/requests"
28+
)
29+
assert get_pypi_codeview_url({}) is None
30+
31+
32+
def test_get_sdist_from_urls():
33+
urls = [
34+
{"packagetype": "bdist_wheel", "url": "https://example.com/pkg-1.0.whl"},
35+
{
36+
"packagetype": "sdist",
37+
"url": "https://example.com/pkg-1.0.tar.gz",
38+
"digests": {"sha256": "abc123", "md5": "def456"},
39+
"size": 12345,
40+
"filename": "pkg-1.0.tar.gz",
41+
},
42+
]
43+
result = get_sdist_from_urls(urls)
44+
assert result["url"] == "https://example.com/pkg-1.0.tar.gz"
45+
assert result["sha256"] == "abc123"
46+
assert result["filename"] == "pkg-1.0.tar.gz"
47+
48+
49+
def test_get_sdist_from_urls_returns_none_when_missing():
50+
assert get_sdist_from_urls([]) is None
51+
assert get_sdist_from_urls(None) is None
52+
assert get_sdist_from_urls([{"packagetype": "bdist_wheel"}]) is None
53+
54+
55+
def test_get_sdist_from_urls_md5_digest_fallback():
56+
urls = [{"packagetype": "sdist", "url": "x", "md5_digest": "old", "digests": {}}]
57+
assert get_sdist_from_urls(urls)["md5"] == "old"

0 commit comments

Comments
 (0)