Skip to content

Commit d0805c1

Browse files
authored
Add support for download URLs for a batch of package types (#195)
* Add support for batch 1 download URLs Signed-off-by: Tushar Goel <[email protected]> * Fix linting issues Signed-off-by: Tushar Goel <[email protected]> * Support golang purls with upper case characters Signed-off-by: Tushar Goel <[email protected]> * Add todo issue Signed-off-by: Tushar Goel <[email protected]> * Address review comments Signed-off-by: Tushar Goel <[email protected]> * Add CHANGELOG Signed-off-by: Tushar Goel <[email protected]> * Bump version Signed-off-by: Tushar Goel <[email protected]> --------- Signed-off-by: Tushar Goel <[email protected]>
1 parent 9fb5350 commit d0805c1

File tree

5 files changed

+106
-4
lines changed

5 files changed

+106
-4
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Changelog
22
=========
33

4+
0.17.2 (2025-07-29)
5+
-------------------
6+
7+
- Add support for getting download URL for Golang, Hex, Pub and Swift in ``purl2url``.
8+
https://github.com/package-url/packageurl-python/pull/195
9+
410
0.17.1 (2025-06-06)
511
-------------------
612

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = packageurl-python
3-
version = 0.17.1
3+
version = 0.17.2
44
license = MIT
55
description = A purl aka. Package URL parser and builder
66
long_description = file:README.rst

src/packageurl/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def unquote(s: AnyStr) -> str:
7272
Return a percent-decoded unicode string, given an `s` byte or unicode
7373
string.
7474
"""
75-
unquoted = _percent_unquote(s) # type:ignore[arg-type] # typeshed is incorrect here
75+
unquoted = _percent_unquote(s)
7676
if not isinstance(unquoted, str):
7777
unquoted = unquoted.decode("utf-8")
7878
return unquoted

src/packageurl/contrib/purl2url.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,76 @@ def build_repo_download_url(purl):
443443
return get_repo_download_url(purl)
444444

445445

446+
@download_router.route("pkg:hex/.*")
447+
def build_hex_download_url(purl):
448+
"""
449+
Return a hex download URL from the `purl` string.
450+
"""
451+
purl_data = PackageURL.from_string(purl)
452+
453+
name = purl_data.name
454+
version = purl_data.version
455+
456+
if name and version:
457+
return f"https://repo.hex.pm/tarballs/{name}-{version}.tar"
458+
459+
460+
@download_router.route("pkg:golang/.*")
461+
def build_golang_download_url(purl):
462+
"""
463+
Return a golang download URL from the `purl` string.
464+
"""
465+
purl_data = PackageURL.from_string(purl)
466+
467+
namespace = purl_data.namespace
468+
name = purl_data.name
469+
version = purl_data.version
470+
471+
if not name:
472+
return
473+
474+
# TODO: https://github.com/package-url/packageurl-python/issues/197
475+
if namespace:
476+
name = f"{namespace}/{name}"
477+
478+
ename = escape_golang_path(name)
479+
eversion = escape_golang_path(version)
480+
481+
if name and version:
482+
return f"https://proxy.golang.org/{ename}/@v/{eversion}.zip"
483+
484+
485+
@download_router.route("pkg:pub/.*")
486+
def build_pub_download_url(purl):
487+
"""
488+
Return a pub download URL from the `purl` string.
489+
"""
490+
purl_data = PackageURL.from_string(purl)
491+
492+
name = purl_data.name
493+
version = purl_data.version
494+
495+
if name and version:
496+
return f"https://pub.dev/api/archives/{name}-{version}.tar.gz"
497+
498+
499+
@download_router.route("pkg:swift/.*")
500+
def build_swift_download_url(purl):
501+
"""
502+
Return a Swift Package download URL from the `purl` string.
503+
"""
504+
purl_data = PackageURL.from_string(purl)
505+
506+
name = purl_data.name
507+
version = purl_data.version
508+
namespace = purl_data.namespace
509+
510+
if not (namespace or name or version):
511+
return
512+
513+
return f"https://{namespace}/{name}/archive/{version}.zip"
514+
515+
446516
def get_repo_download_url(purl):
447517
"""
448518
Return ``download_url`` if present in ``purl`` qualifiers or
@@ -470,3 +540,24 @@ def get_repo_download_url(purl):
470540
return get_repo_download_url_by_package_type(
471541
type=type, namespace=namespace, name=name, version=version
472542
)
543+
544+
545+
# TODO: https://github.com/package-url/packageurl-python/issues/196
546+
def escape_golang_path(path: str) -> str:
547+
"""
548+
Return an case-encoded module path or version name.
549+
550+
This is done by replacing every uppercase letter with an exclamation mark followed by the
551+
corresponding lower-case letter, in order to avoid ambiguity when serving from case-insensitive
552+
file systems.
553+
554+
See https://golang.org/ref/mod#goproxy-protocol.
555+
"""
556+
escaped_path = ""
557+
for c in path:
558+
if c >= "A" and c <= "Z":
559+
# replace uppercase with !lowercase
560+
escaped_path += "!" + chr(ord(c) + ord("a") - ord("A"))
561+
else:
562+
escaped_path += c
563+
return escaped_path

tests/contrib/test_purl2url.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ def test_purl2url_get_download_url():
9898
"pkg:maven/org.apache.commons/[email protected]?repository_url=https://repo1.maven.org/maven2": "https://repo1.maven.org/maven2/org/apache/commons/commons-io/1.3.2/commons-io-1.3.2.jar",
9999
"pkg:maven/org.apache.commons/[email protected]?type=pom": "https://repo.maven.apache.org/maven2/org/apache/commons/commons-io/1.3.2/commons-io-1.3.2.pom",
100100
"pkg:maven/org.apache.commons/[email protected]?classifier=sources": "https://repo.maven.apache.org/maven2/org/apache/commons/commons-math3/3.6.1/commons-math3-3.6.1-sources.jar",
101+
"pkg:hex/[email protected]": "https://repo.hex.pm/tarballs/plug-1.11.1.tar",
102+
"pkg:golang/xorm.io/[email protected]": "https://proxy.golang.org/xorm.io/xorm/@v/v0.8.2.zip",
103+
"pkg:golang/gopkg.in/[email protected]": "https://proxy.golang.org/gopkg.in/ldap.v3/@v/v3.1.0.zip",
104+
"pkg:golang/example.com/[email protected]": "https://proxy.golang.org/example.com/!m.v3/@v/v3.1.0.zip",
105+
"pkg:pub/[email protected]": "https://pub.dev/api/archives/http-0.13.3.tar.gz",
106+
"pkg:swift/github.com/Alamofire/[email protected]": "https://github.com/Alamofire/Alamofire/archive/5.4.3.zip",
107+
"pkg:swift/github.com/RxSwiftCommunity/[email protected]": "https://github.com/RxSwiftCommunity/RxFlow/archive/2.12.4.zip",
101108
# From `download_url` qualifier
102109
"pkg:github/yarnpkg/[email protected]?download_url=https://github.com/yarnpkg/yarn/releases/download/v1.3.2/yarn-v1.3.2.tar.gz&version_prefix=v": "https://github.com/yarnpkg/yarn/releases/download/v1.3.2/yarn-v1.3.2.tar.gz",
103110
"pkg:generic/lxc-master.tar.gz?download_url=https://salsa.debian.org/lxc-team/lxc/-/archive/master/lxc-master.tar.gz": "https://salsa.debian.org/lxc-team/lxc/-/archive/master/lxc-master.tar.gz",
@@ -112,8 +119,6 @@ def test_purl2url_get_download_url():
112119
"pkg:bitbucket/birkenfeld": None,
113120
"pkg:pypi/[email protected]": None,
114121
"pkg:composer/psr/[email protected]": None,
115-
"pkg:golang/xorm.io/[email protected]": None,
116-
"pkg:golang/gopkg.in/[email protected]": None,
117122
}
118123

119124
for purl, url in purls_url.items():

0 commit comments

Comments
 (0)