A purl aka. Package URL parser and builder
—
Helper functions for specific package ecosystems and specialized use cases, including Go module handling and custom routing functionality. These utilities provide ecosystem-specific logic for parsing and constructing PackageURLs according to different package manager conventions.
Specialized utilities for working with Go modules and packages, supporting both go.mod format and import path parsing.
from packageurl.utils import get_golang_purl
def get_golang_purl(go_package: str):
"""
Create a PackageURL object from a Go package import path or go.mod entry.
Supports both import paths and versioned go.mod entries:
- Import path: "github.com/gorilla/mux"
- go.mod entry: "github.com/gorilla/mux v1.8.1"
Args:
go_package (str): Go package import path or "name version" string from go.mod
Returns:
PackageURL | None: PackageURL with type='golang', or None if invalid input
Raises:
Exception: If go_package contains '@' character (invalid for Go modules)
"""Extended routing capabilities for custom URL pattern matching and processing workflows.
from packageurl.contrib.route import Router, NoRouteAvailable, Rule, RouteAlreadyDefined, MultipleRoutesDefined
class Rule:
"""
A mapping between a URL pattern and a callable endpoint.
The pattern is a regex string that must match entirely for the rule
to be considered and the endpoint to be invoked.
"""
def __init__(self, pattern, endpoint):
"""
Initialize rule with pattern and endpoint.
Args:
pattern (str): Regular expression pattern to match URLs
endpoint (callable): Function or class to handle matched URLs
"""
def match(self, string):
"""
Match a string with the rule pattern.
Args:
string (str): String to match against pattern
Returns:
Match object or None if no match
"""
class RouteAlreadyDefined(TypeError):
"""Raised when a route Rule already exists in the route map."""
class MultipleRoutesDefined(TypeError):
"""Raised when multiple routes match the same string."""
class Router:
"""
Advanced URL routing system supporting regex patterns and custom handlers.
Enables pattern-based URL matching for extensible PURL inference and
custom URL processing workflows.
"""
def __init__(self, route_map=None):
"""
Initialize router with optional route map.
Args:
route_map (dict, optional): Ordered mapping of pattern -> Rule
"""
def append(self, pattern, endpoint):
"""
Add a URL route with pattern and endpoint function.
Args:
pattern (str): Regular expression pattern to match URLs
endpoint (callable): Function to process matched URLs
"""
def process(self, string, *args, **kwargs):
"""
Process a URL through registered patterns to find matching handler.
Args:
string (str): URL to route and process
*args, **kwargs: Additional arguments passed to endpoint
Returns:
Result of the matched endpoint function
Raises:
NoRouteAvailable: If no registered pattern matches the URL
"""
def route(self, *patterns):
"""
Decorator to make a callable routed to one or more patterns.
Args:
*patterns (str): URL patterns to match
Returns:
Decorator function for registering endpoints
"""
def resolve(self, string):
"""
Resolve a string to an endpoint function.
Args:
string (str): URL to resolve
Returns:
callable: Endpoint function for the URL
Raises:
NoRouteAvailable: If no pattern matches the URL
MultipleRoutesDefined: If multiple patterns match the URL
"""
def is_routable(self, string):
"""
Check if a string is routable by this router.
Args:
string (str): URL to check
Returns:
bool: True if URL matches any route pattern
"""
def keys(self):
"""
Return route pattern keys.
Returns:
dict_keys: Pattern keys from the route map
"""
def __iter__(self):
"""
Iterate over route map items.
Returns:
iterator: Iterator over (pattern, rule) pairs
"""
class NoRouteAvailable(Exception):
"""
Exception raised when URL routing fails to find a matching pattern.
Attributes:
url (str): The URL that failed to match any route
message (str): Error description
"""
def __init__(self, url, message="No route available"):
self.url = url
self.message = message
super().__init__(f"{message}: {url}")Decorator utilities for registering route handlers with routers.
def route(pattern, router=None):
"""
Decorator for registering a function as a route handler.
Args:
pattern (str): URL pattern to match
router (Router, optional): Router instance to register with
Returns:
Decorated function registered as route handler
"""from packageurl.utils import get_golang_purl
# Parse Go import paths
purl1 = get_golang_purl("github.com/gorilla/mux")
print(purl1)
# PackageURL(type='golang', namespace='github.com/gorilla', name='mux', version=None, qualifiers={}, subpath=None)
# Parse go.mod entries with versions
purl2 = get_golang_purl("github.com/gorilla/mux v1.8.1")
print(purl2)
# PackageURL(type='golang', namespace='github.com/gorilla', name='mux', version='v1.8.1', qualifiers={}, subpath=None)
# Handle multi-level namespaces
purl3 = get_golang_purl("go.uber.org/zap/zapcore")
print(purl3)
# PackageURL(type='golang', namespace='go.uber.org/zap', name='zapcore', version=None, qualifiers={}, subpath=None)
# Handle standard library (empty namespace)
purl4 = get_golang_purl("fmt")
print(purl4)
# PackageURL(type='golang', namespace='', name='fmt', version=None, qualifiers={}, subpath=None)from packageurl.contrib.route import Router, NoRouteAvailable, route
from packageurl import PackageURL
import re
# Create and configure router
router = Router()
# Manual route registration
def handle_custom_npm(url):
"""Handle custom npm registry URLs."""
match = re.search(r'/package/(@[^/]+/[^/]+|[^/]+)', url)
if match:
name = match.group(1)
return PackageURL(type="npm", name=name)
return None
router.append(r'https://custom-npm\.example\.com/package/', handle_custom_npm)
# Decorator-based registration
@route(r'https://internal-pypi\.company\.com/simple/([^/]+)/?', router)
def handle_internal_pypi(url, match):
"""Handle internal PyPI URLs."""
package_name = match.group(1)
return PackageURL(type="pypi", name=package_name)
# Route URLs
try:
npm_purl = router.process("https://custom-npm.example.com/package/@angular/core")
pypi_purl = router.process("https://internal-pypi.company.com/simple/requests/")
print(f"NPM: {npm_purl}")
print(f"PyPI: {pypi_purl}")
except NoRouteAvailable as e:
print(f"Routing failed: {e}")
# Check route matches without execution
match_result = router.match("https://custom-npm.example.com/package/lodash")
if match_result:
pattern, handler, groups = match_result
print(f"Matched pattern: {pattern}")from packageurl.contrib.route import Router
from packageurl import PackageURL
import re
from urllib.parse import urlparse, parse_qs
# Enterprise routing setup
enterprise_router = Router()
def handle_nexus_repository(url):
"""Handle Sonatype Nexus repository URLs."""
parsed = urlparse(url)
path_parts = parsed.path.strip('/').split('/')
if 'maven' in path_parts:
# Maven artifact URL
try:
idx = path_parts.index('maven')
group_parts = path_parts[idx+1:-2]
artifact_id = path_parts[-2]
version = path_parts[-1]
namespace = '.'.join(group_parts)
return PackageURL(type="maven", namespace=namespace, name=artifact_id, version=version)
except (ValueError, IndexError):
return None
return None
def handle_artifactory(url):
"""Handle JFrog Artifactory URLs."""
# Custom Artifactory URL parsing logic
match = re.search(r'/artifactory/([^/]+)/(.+)', url)
if match:
repo_name, path = match.groups()
# Parse path based on repository type
if 'npm' in repo_name:
return PackageURL(type="npm", name=path.split('/')[-1])
elif 'pypi' in repo_name:
return PackageURL(type="pypi", name=path.split('/')[-1])
return None
# Register enterprise handlers
enterprise_router.append(r'https://nexus\.company\.com/repository/', handle_nexus_repository)
enterprise_router.append(r'https://artifactory\.company\.com/artifactory/', handle_artifactory)
# Multi-step routing with fallbacks
def route_with_fallback(url, routers):
"""Try multiple routers in sequence."""
for router in routers:
try:
return router.process(url)
except NoRouteAvailable:
continue
raise NoRouteAvailable(url, "No router could handle URL")
# Usage
routers = [enterprise_router, purl_router] # purl_router from url2purl module
result = route_with_fallback("https://nexus.company.com/repository/maven/org/springframework/spring-core/5.3.21/", routers)
print(result)from packageurl.utils import get_golang_purl
from packageurl.contrib.route import Router
# Combine ecosystem utilities with routing
golang_router = Router()
@route(r'https://pkg\.go\.dev/([^@]+)(?:@([^?]+))?', golang_router)
def handle_pkg_go_dev(url, match):
"""Handle pkg.go.dev URLs."""
import_path = match.group(1)
version = match.group(2)
if version:
go_spec = f"{import_path} {version}"
else:
go_spec = import_path
return get_golang_purl(go_spec)
# Test Go package URL handling
go_purl = golang_router.process("https://pkg.go.dev/github.com/gin-gonic/gin@v1.8.1")
print(go_purl)
# PackageURL(type='golang', namespace='github.com/gin-gonic', name='gin', version='v1.8.1', qualifiers={}, subpath=None)from packageurl.utils import get_golang_purl
from packageurl.contrib.route import NoRouteAvailable
def safe_golang_purl(go_package):
"""Safely create Go PURL with error handling."""
try:
if '@' in go_package:
raise ValueError("Go packages should not contain '@' character")
purl = get_golang_purl(go_package)
if purl is None:
raise ValueError(f"Could not parse Go package: {go_package}")
return purl
except Exception as e:
print(f"Error processing Go package '{go_package}': {e}")
return None
# Safe usage
valid_purl = safe_golang_purl("github.com/stretchr/testify v1.7.0")
invalid_purl = safe_golang_purl("invalid@package") # Will return None
def safe_route(router, url):
"""Safely route URL with comprehensive error handling."""
try:
return router.route(url)
except NoRouteAvailable as e:
print(f"No route available for {url}: {e}")
return None
except Exception as e:
print(f"Routing error for {url}: {e}")
return None
# Safe routing usage
result = safe_route(router, "https://unknown-registry.com/package/foo")Install with Tessl CLI
npx tessl i tessl/pypi-packageurl-python