0
# Ecosystem-Specific Utilities
1
2
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.
3
4
## Capabilities
5
6
### Go Module Utilities
7
8
Specialized utilities for working with Go modules and packages, supporting both go.mod format and import path parsing.
9
10
```python { .api }
11
from packageurl.utils import get_golang_purl
12
13
def get_golang_purl(go_package: str):
14
"""
15
Create a PackageURL object from a Go package import path or go.mod entry.
16
17
Supports both import paths and versioned go.mod entries:
18
- Import path: "github.com/gorilla/mux"
19
- go.mod entry: "github.com/gorilla/mux v1.8.1"
20
21
Args:
22
go_package (str): Go package import path or "name version" string from go.mod
23
24
Returns:
25
PackageURL | None: PackageURL with type='golang', or None if invalid input
26
27
Raises:
28
Exception: If go_package contains '@' character (invalid for Go modules)
29
"""
30
```
31
32
### Advanced Routing System
33
34
Extended routing capabilities for custom URL pattern matching and processing workflows.
35
36
```python { .api }
37
from packageurl.contrib.route import Router, NoRouteAvailable, Rule, RouteAlreadyDefined, MultipleRoutesDefined
38
39
class Rule:
40
"""
41
A mapping between a URL pattern and a callable endpoint.
42
43
The pattern is a regex string that must match entirely for the rule
44
to be considered and the endpoint to be invoked.
45
"""
46
47
def __init__(self, pattern, endpoint):
48
"""
49
Initialize rule with pattern and endpoint.
50
51
Args:
52
pattern (str): Regular expression pattern to match URLs
53
endpoint (callable): Function or class to handle matched URLs
54
"""
55
56
def match(self, string):
57
"""
58
Match a string with the rule pattern.
59
60
Args:
61
string (str): String to match against pattern
62
63
Returns:
64
Match object or None if no match
65
"""
66
67
class RouteAlreadyDefined(TypeError):
68
"""Raised when a route Rule already exists in the route map."""
69
70
class MultipleRoutesDefined(TypeError):
71
"""Raised when multiple routes match the same string."""
72
73
class Router:
74
"""
75
Advanced URL routing system supporting regex patterns and custom handlers.
76
77
Enables pattern-based URL matching for extensible PURL inference and
78
custom URL processing workflows.
79
"""
80
81
def __init__(self, route_map=None):
82
"""
83
Initialize router with optional route map.
84
85
Args:
86
route_map (dict, optional): Ordered mapping of pattern -> Rule
87
"""
88
89
def append(self, pattern, endpoint):
90
"""
91
Add a URL route with pattern and endpoint function.
92
93
Args:
94
pattern (str): Regular expression pattern to match URLs
95
endpoint (callable): Function to process matched URLs
96
"""
97
98
def process(self, string, *args, **kwargs):
99
"""
100
Process a URL through registered patterns to find matching handler.
101
102
Args:
103
string (str): URL to route and process
104
*args, **kwargs: Additional arguments passed to endpoint
105
106
Returns:
107
Result of the matched endpoint function
108
109
Raises:
110
NoRouteAvailable: If no registered pattern matches the URL
111
"""
112
113
def route(self, *patterns):
114
"""
115
Decorator to make a callable routed to one or more patterns.
116
117
Args:
118
*patterns (str): URL patterns to match
119
120
Returns:
121
Decorator function for registering endpoints
122
"""
123
124
def resolve(self, string):
125
"""
126
Resolve a string to an endpoint function.
127
128
Args:
129
string (str): URL to resolve
130
131
Returns:
132
callable: Endpoint function for the URL
133
134
Raises:
135
NoRouteAvailable: If no pattern matches the URL
136
MultipleRoutesDefined: If multiple patterns match the URL
137
"""
138
139
def is_routable(self, string):
140
"""
141
Check if a string is routable by this router.
142
143
Args:
144
string (str): URL to check
145
146
Returns:
147
bool: True if URL matches any route pattern
148
"""
149
150
def keys(self):
151
"""
152
Return route pattern keys.
153
154
Returns:
155
dict_keys: Pattern keys from the route map
156
"""
157
158
def __iter__(self):
159
"""
160
Iterate over route map items.
161
162
Returns:
163
iterator: Iterator over (pattern, rule) pairs
164
"""
165
166
class NoRouteAvailable(Exception):
167
"""
168
Exception raised when URL routing fails to find a matching pattern.
169
170
Attributes:
171
url (str): The URL that failed to match any route
172
message (str): Error description
173
"""
174
175
def __init__(self, url, message="No route available"):
176
self.url = url
177
self.message = message
178
super().__init__(f"{message}: {url}")
179
```
180
181
### Route Decorators
182
183
Decorator utilities for registering route handlers with routers.
184
185
```python { .api }
186
def route(pattern, router=None):
187
"""
188
Decorator for registering a function as a route handler.
189
190
Args:
191
pattern (str): URL pattern to match
192
router (Router, optional): Router instance to register with
193
194
Returns:
195
Decorated function registered as route handler
196
"""
197
```
198
199
## Usage Examples
200
201
### Go Module Processing
202
203
```python
204
from packageurl.utils import get_golang_purl
205
206
# Parse Go import paths
207
purl1 = get_golang_purl("github.com/gorilla/mux")
208
print(purl1)
209
# PackageURL(type='golang', namespace='github.com/gorilla', name='mux', version=None, qualifiers={}, subpath=None)
210
211
# Parse go.mod entries with versions
212
purl2 = get_golang_purl("github.com/gorilla/mux v1.8.1")
213
print(purl2)
214
# PackageURL(type='golang', namespace='github.com/gorilla', name='mux', version='v1.8.1', qualifiers={}, subpath=None)
215
216
# Handle multi-level namespaces
217
purl3 = get_golang_purl("go.uber.org/zap/zapcore")
218
print(purl3)
219
# PackageURL(type='golang', namespace='go.uber.org/zap', name='zapcore', version=None, qualifiers={}, subpath=None)
220
221
# Handle standard library (empty namespace)
222
purl4 = get_golang_purl("fmt")
223
print(purl4)
224
# PackageURL(type='golang', namespace='', name='fmt', version=None, qualifiers={}, subpath=None)
225
```
226
227
### Advanced Routing
228
229
```python
230
from packageurl.contrib.route import Router, NoRouteAvailable, route
231
from packageurl import PackageURL
232
import re
233
234
# Create and configure router
235
router = Router()
236
237
# Manual route registration
238
def handle_custom_npm(url):
239
"""Handle custom npm registry URLs."""
240
match = re.search(r'/package/(@[^/]+/[^/]+|[^/]+)', url)
241
if match:
242
name = match.group(1)
243
return PackageURL(type="npm", name=name)
244
return None
245
246
router.append(r'https://custom-npm\.example\.com/package/', handle_custom_npm)
247
248
# Decorator-based registration
249
@route(r'https://internal-pypi\.company\.com/simple/([^/]+)/?', router)
250
def handle_internal_pypi(url, match):
251
"""Handle internal PyPI URLs."""
252
package_name = match.group(1)
253
return PackageURL(type="pypi", name=package_name)
254
255
# Route URLs
256
try:
257
npm_purl = router.process("https://custom-npm.example.com/package/@angular/core")
258
pypi_purl = router.process("https://internal-pypi.company.com/simple/requests/")
259
print(f"NPM: {npm_purl}")
260
print(f"PyPI: {pypi_purl}")
261
except NoRouteAvailable as e:
262
print(f"Routing failed: {e}")
263
264
# Check route matches without execution
265
match_result = router.match("https://custom-npm.example.com/package/lodash")
266
if match_result:
267
pattern, handler, groups = match_result
268
print(f"Matched pattern: {pattern}")
269
```
270
271
### Complex Routing Scenarios
272
273
```python
274
from packageurl.contrib.route import Router
275
from packageurl import PackageURL
276
import re
277
from urllib.parse import urlparse, parse_qs
278
279
# Enterprise routing setup
280
enterprise_router = Router()
281
282
def handle_nexus_repository(url):
283
"""Handle Sonatype Nexus repository URLs."""
284
parsed = urlparse(url)
285
path_parts = parsed.path.strip('/').split('/')
286
287
if 'maven' in path_parts:
288
# Maven artifact URL
289
try:
290
idx = path_parts.index('maven')
291
group_parts = path_parts[idx+1:-2]
292
artifact_id = path_parts[-2]
293
version = path_parts[-1]
294
namespace = '.'.join(group_parts)
295
return PackageURL(type="maven", namespace=namespace, name=artifact_id, version=version)
296
except (ValueError, IndexError):
297
return None
298
299
return None
300
301
def handle_artifactory(url):
302
"""Handle JFrog Artifactory URLs."""
303
# Custom Artifactory URL parsing logic
304
match = re.search(r'/artifactory/([^/]+)/(.+)', url)
305
if match:
306
repo_name, path = match.groups()
307
# Parse path based on repository type
308
if 'npm' in repo_name:
309
return PackageURL(type="npm", name=path.split('/')[-1])
310
elif 'pypi' in repo_name:
311
return PackageURL(type="pypi", name=path.split('/')[-1])
312
return None
313
314
# Register enterprise handlers
315
enterprise_router.append(r'https://nexus\.company\.com/repository/', handle_nexus_repository)
316
enterprise_router.append(r'https://artifactory\.company\.com/artifactory/', handle_artifactory)
317
318
# Multi-step routing with fallbacks
319
def route_with_fallback(url, routers):
320
"""Try multiple routers in sequence."""
321
for router in routers:
322
try:
323
return router.process(url)
324
except NoRouteAvailable:
325
continue
326
raise NoRouteAvailable(url, "No router could handle URL")
327
328
# Usage
329
routers = [enterprise_router, purl_router] # purl_router from url2purl module
330
result = route_with_fallback("https://nexus.company.com/repository/maven/org/springframework/spring-core/5.3.21/", routers)
331
print(result)
332
```
333
334
### Ecosystem Pattern Matching
335
336
```python
337
from packageurl.utils import get_golang_purl
338
from packageurl.contrib.route import Router
339
340
# Combine ecosystem utilities with routing
341
golang_router = Router()
342
343
@route(r'https://pkg\.go\.dev/([^@]+)(?:@([^?]+))?', golang_router)
344
def handle_pkg_go_dev(url, match):
345
"""Handle pkg.go.dev URLs."""
346
import_path = match.group(1)
347
version = match.group(2)
348
349
if version:
350
go_spec = f"{import_path} {version}"
351
else:
352
go_spec = import_path
353
354
return get_golang_purl(go_spec)
355
356
# Test Go package URL handling
357
go_purl = golang_router.process("https://pkg.go.dev/github.com/gin-gonic/gin@v1.8.1")
358
print(go_purl)
359
# PackageURL(type='golang', namespace='github.com/gin-gonic', name='gin', version='v1.8.1', qualifiers={}, subpath=None)
360
```
361
362
### Error Handling and Validation
363
364
```python
365
from packageurl.utils import get_golang_purl
366
from packageurl.contrib.route import NoRouteAvailable
367
368
def safe_golang_purl(go_package):
369
"""Safely create Go PURL with error handling."""
370
try:
371
if '@' in go_package:
372
raise ValueError("Go packages should not contain '@' character")
373
374
purl = get_golang_purl(go_package)
375
if purl is None:
376
raise ValueError(f"Could not parse Go package: {go_package}")
377
378
return purl
379
except Exception as e:
380
print(f"Error processing Go package '{go_package}': {e}")
381
return None
382
383
# Safe usage
384
valid_purl = safe_golang_purl("github.com/stretchr/testify v1.7.0")
385
invalid_purl = safe_golang_purl("invalid@package") # Will return None
386
387
def safe_route(router, url):
388
"""Safely route URL with comprehensive error handling."""
389
try:
390
return router.route(url)
391
except NoRouteAvailable as e:
392
print(f"No route available for {url}: {e}")
393
return None
394
except Exception as e:
395
print(f"Routing error for {url}: {e}")
396
return None
397
398
# Safe routing usage
399
result = safe_route(router, "https://unknown-registry.com/package/foo")
400
```