src2purl 1.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,302 @@
1
+ """Package coordinate extraction from Software Heritage origins."""
2
+
3
+ import re
4
+ from typing import Dict, List, Optional
5
+ from urllib.parse import urlparse
6
+
7
+ from src2id.core.models import SHOriginMatch
8
+
9
+
10
+ class PackageCoordinateExtractor:
11
+ """Extracts package metadata from SH origin URLs and metadata."""
12
+
13
+ # Known official organizations
14
+ OFFICIAL_ORGS = {
15
+ 'github.com': [
16
+ 'opencv', 'microsoft', 'google', 'facebook', 'apple', 'meta',
17
+ 'llvm', 'boost-org', 'protocolbuffers', 'grpc', 'apache',
18
+ 'python', 'nodejs', 'golang', 'rust-lang', 'torvalds',
19
+ 'tensorflow', 'pytorch', 'numpy', 'scipy', 'pandas-dev',
20
+ 'FFmpeg', 'VideoLAN', 'libav', 'x264', 'x265',
21
+ ],
22
+ 'gitlab.com': [
23
+ 'freedesktop-sdk', 'gnome', 'kde', 'freedesktop', 'gstreamer',
24
+ ],
25
+ 'git.kernel.org': [
26
+ 'pub/scm/linux/kernel',
27
+ ],
28
+ }
29
+
30
+ def extract_coordinates(self, origin: SHOriginMatch) -> Dict[str, Optional[str]]:
31
+ """
32
+ Extract name, version, download_url from origin.
33
+
34
+ Args:
35
+ origin: Origin match from Software Heritage
36
+
37
+ Returns:
38
+ Dictionary with extracted coordinates
39
+ """
40
+ url = origin.origin_url
41
+
42
+ # Start with generic extraction
43
+ coordinates = self._extract_generic_coordinates(url, origin.metadata)
44
+
45
+ # Enhance with platform-specific extraction using proper URL parsing
46
+ parsed_url = urlparse(url)
47
+ hostname = parsed_url.hostname.lower() if parsed_url.hostname else ''
48
+
49
+ if hostname == 'github.com':
50
+ coordinates.update(self._extract_github_coordinates(url, origin.metadata))
51
+ elif hostname == 'gitlab.com':
52
+ coordinates.update(self._extract_gitlab_coordinates(url, origin.metadata))
53
+ elif hostname == 'sourceforge.net' or hostname.endswith('.sourceforge.net'):
54
+ coordinates.update(self._extract_sourceforge_coordinates(url, origin.metadata))
55
+ elif hostname == 'pypi.org' or hostname.endswith('.pypi.org'):
56
+ coordinates.update(self._extract_pypi_coordinates(url))
57
+ elif hostname == 'registry.npmjs.org':
58
+ coordinates.update(self._extract_npm_coordinates(url))
59
+ elif hostname == 'git.kernel.org' or hostname == 'kernel.org':
60
+ coordinates.update(self._extract_kernel_coordinates(url, origin.metadata))
61
+
62
+ return coordinates
63
+
64
+ def _extract_generic_coordinates(
65
+ self, url: str, metadata: Dict
66
+ ) -> Dict[str, Optional[str]]:
67
+ """
68
+ Universal extraction that works for any repository.
69
+
70
+ Args:
71
+ url: Repository URL
72
+ metadata: Origin metadata
73
+
74
+ Returns:
75
+ Generic coordinates
76
+ """
77
+ coordinates = {
78
+ 'download_url': url,
79
+ 'name': None,
80
+ 'version': None,
81
+ 'license': None,
82
+ }
83
+
84
+ # Try to extract name from URL
85
+ parsed = urlparse(url)
86
+ path_parts = parsed.path.strip('/').split('/')
87
+
88
+ if path_parts:
89
+ # Get the last meaningful part
90
+ name = path_parts[-1]
91
+ # Remove common suffixes
92
+ for suffix in ['.git', '.hg', '.svn', '.bzr']:
93
+ if name.endswith(suffix):
94
+ name = name[:-len(suffix)]
95
+ coordinates['name'] = name
96
+
97
+ # Try to extract version from metadata
98
+ if metadata:
99
+ # Look for version in various metadata fields
100
+ for key in ['version', 'tag', 'release', 'ref']:
101
+ if key in metadata:
102
+ coordinates['version'] = str(metadata[key])
103
+ break
104
+
105
+ return coordinates
106
+
107
+ def _extract_github_coordinates(
108
+ self, url: str, metadata: Dict
109
+ ) -> Dict[str, Optional[str]]:
110
+ """Extract coordinates from GitHub URLs."""
111
+ coordinates = {}
112
+
113
+ # Parse GitHub URL: https://github.com/owner/repo
114
+ match = re.search(r'github\.com[:/]([^/]+)/([^/\s]+)', url)
115
+ if match:
116
+ owner = match.group(1)
117
+ repo = match.group(2).rstrip('.git')
118
+ coordinates['name'] = repo
119
+ coordinates['download_url'] = f"https://github.com/{owner}/{repo}"
120
+
121
+ # Extract version from tags or releases in metadata
122
+ if metadata:
123
+ tags = metadata.get('tags', [])
124
+ if tags:
125
+ version = self._extract_version_from_tags(tags)
126
+ if version:
127
+ coordinates['version'] = version
128
+
129
+ return coordinates
130
+
131
+ def _extract_gitlab_coordinates(
132
+ self, url: str, metadata: Dict
133
+ ) -> Dict[str, Optional[str]]:
134
+ """Extract coordinates from GitLab URLs."""
135
+ coordinates = {}
136
+
137
+ # Parse GitLab URL
138
+ match = re.search(r'gitlab\.com[:/]([^/]+)/([^/\s]+)', url)
139
+ if match:
140
+ owner = match.group(1)
141
+ repo = match.group(2).rstrip('.git')
142
+ coordinates['name'] = repo
143
+ coordinates['download_url'] = f"https://gitlab.com/{owner}/{repo}"
144
+
145
+ # Extract version from metadata
146
+ if metadata:
147
+ tags = metadata.get('tags', [])
148
+ if tags:
149
+ version = self._extract_version_from_tags(tags)
150
+ if version:
151
+ coordinates['version'] = version
152
+
153
+ return coordinates
154
+
155
+ def _extract_sourceforge_coordinates(
156
+ self, url: str, metadata: Dict
157
+ ) -> Dict[str, Optional[str]]:
158
+ """Extract coordinates from SourceForge URLs."""
159
+ coordinates = {}
160
+
161
+ # Parse SourceForge URL patterns
162
+ # e.g., https://svn.code.sf.net/p/projectname/code/trunk
163
+ match = re.search(r'sourceforge\.net/p/([^/]+)', url)
164
+ if not match:
165
+ match = re.search(r'sf\.net/p/([^/]+)', url)
166
+
167
+ if match:
168
+ project = match.group(1)
169
+ coordinates['name'] = project
170
+ coordinates['download_url'] = f"https://sourceforge.net/projects/{project}/"
171
+
172
+ return coordinates
173
+
174
+ def _extract_kernel_coordinates(
175
+ self, url: str, metadata: Dict
176
+ ) -> Dict[str, Optional[str]]:
177
+ """Extract coordinates from kernel.org URLs."""
178
+ coordinates = {}
179
+
180
+ if 'linux/kernel' in url:
181
+ coordinates['name'] = 'linux'
182
+ coordinates['download_url'] = url
183
+
184
+ # Linux kernel uses specific version naming
185
+ if metadata:
186
+ tags = metadata.get('tags', [])
187
+ for tag in tags:
188
+ if re.match(r'v\d+\.\d+(\.\d+)?', tag):
189
+ coordinates['version'] = tag.lstrip('v')
190
+ break
191
+
192
+ return coordinates
193
+
194
+ def _extract_pypi_coordinates(self, url: str) -> Dict[str, Optional[str]]:
195
+ """Extract coordinates from PyPI URLs."""
196
+ coordinates = {}
197
+
198
+ # Parse PyPI URL
199
+ match = re.search(r'pypi\.org/project/([^/]+)/?([^/]+)?', url)
200
+ if match:
201
+ package = match.group(1)
202
+ version = match.group(2) if match.group(2) else None
203
+
204
+ coordinates['name'] = package
205
+ if version:
206
+ coordinates['version'] = version
207
+
208
+ return coordinates
209
+
210
+ def _extract_npm_coordinates(self, url: str) -> Dict[str, Optional[str]]:
211
+ """Extract coordinates from npm registry URLs."""
212
+ coordinates = {}
213
+
214
+ # Parse npm URL
215
+ match = re.search(r'registry\.npmjs\.org/([^/]+)/?([^/]+)?', url)
216
+ if match:
217
+ package = match.group(1)
218
+ version = match.group(2) if match.group(2) else None
219
+
220
+ coordinates['name'] = package.replace('%40', '@') # Handle scoped packages
221
+ if version:
222
+ coordinates['version'] = version
223
+
224
+ return coordinates
225
+
226
+ def _extract_version_from_tags(self, tags: List[str]) -> Optional[str]:
227
+ """
228
+ Extract semantic version from git tags.
229
+
230
+ Args:
231
+ tags: List of git tags
232
+
233
+ Returns:
234
+ Best version match or None
235
+ """
236
+ if not tags:
237
+ return None
238
+
239
+ # Priority patterns for version matching
240
+ patterns = [
241
+ r'^v?(\d+\.\d+\.\d+)$', # Semantic version
242
+ r'^v?(\d+\.\d+)$', # Major.minor
243
+ r'^release-(\d+\.\d+\.\d+)$', # Release prefix
244
+ r'^r(\d+\.\d+\.\d+)$', # r prefix
245
+ ]
246
+
247
+ for pattern in patterns:
248
+ for tag in tags:
249
+ match = re.match(pattern, tag)
250
+ if match:
251
+ return match.group(1)
252
+
253
+ # If no pattern matches, return the first tag that looks like a version
254
+ for tag in tags:
255
+ if re.search(r'\d+\.\d+', tag):
256
+ return tag
257
+
258
+ return None
259
+
260
+ def is_official_organization(self, url: str) -> bool:
261
+ """
262
+ Check if URL belongs to known official organization.
263
+
264
+ Args:
265
+ url: Repository URL
266
+
267
+ Returns:
268
+ True if from official organization
269
+ """
270
+ try:
271
+ parsed_url = urlparse(url)
272
+ hostname = parsed_url.hostname.lower() if parsed_url.hostname else ''
273
+
274
+ for domain, orgs in self.OFFICIAL_ORGS.items():
275
+ if hostname == domain or hostname.endswith('.' + domain):
276
+ org = self._extract_organization(url, domain)
277
+ if org and org.lower() in [o.lower() for o in orgs]:
278
+ return True
279
+ return False
280
+ except Exception:
281
+ return False
282
+
283
+ def _extract_organization(self, url: str, domain: str) -> Optional[str]:
284
+ """
285
+ Extract organization name from repository URL.
286
+
287
+ Args:
288
+ url: Repository URL
289
+ domain: Domain name
290
+
291
+ Returns:
292
+ Organization name or None
293
+ """
294
+ if domain == 'github.com' or domain == 'gitlab.com':
295
+ match = re.search(rf'{domain}[:/]([^/]+)/', url)
296
+ if match:
297
+ return match.group(1)
298
+ elif domain == 'git.kernel.org':
299
+ if 'pub/scm/linux/kernel' in url:
300
+ return 'pub/scm/linux/kernel'
301
+
302
+ return None
src2id/core/models.py ADDED
@@ -0,0 +1,93 @@
1
+ """Data models for SHPI."""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional
8
+
9
+
10
+ class MatchType(str, Enum):
11
+ """Type of match found in Software Heritage."""
12
+ EXACT = "exact"
13
+ FUZZY = "fuzzy"
14
+
15
+
16
+ @dataclass
17
+ class PackageMatch:
18
+ """Represents a package match found in Software Heritage."""
19
+
20
+ download_url: str
21
+ match_type: MatchType
22
+ confidence_score: float
23
+ name: Optional[str] = None
24
+ version: Optional[str] = None
25
+ license: Optional[str] = None
26
+ sh_url: Optional[str] = None
27
+ frequency_count: int = 0
28
+ is_official_org: bool = False
29
+ purl: Optional[str] = None
30
+
31
+ def to_dict(self) -> Dict[str, Any]:
32
+ """Serialize to dictionary for JSON output."""
33
+ return {
34
+ "download_url": self.download_url,
35
+ "name": self.name,
36
+ "version": self.version,
37
+ "license": self.license,
38
+ "sh_url": self.sh_url,
39
+ "match_type": self.match_type.value,
40
+ "confidence_score": self.confidence_score,
41
+ "frequency_count": self.frequency_count,
42
+ "is_official_org": self.is_official_org,
43
+ "purl": self.purl,
44
+ }
45
+
46
+
47
+ @dataclass
48
+ class DirectoryCandidate:
49
+ """Represents a directory candidate for SWHID matching."""
50
+
51
+ path: Path
52
+ swhid: str
53
+ depth: int
54
+ specificity_score: float
55
+ file_count: int
56
+
57
+
58
+ @dataclass
59
+ class ContentCandidate:
60
+ """Represents a file/content candidate for SWHID matching."""
61
+
62
+ path: Path
63
+ swhid: str
64
+ depth: int
65
+ size: int
66
+
67
+
68
+ @dataclass
69
+ class SHOriginMatch:
70
+ """Represents an origin match from Software Heritage."""
71
+
72
+ origin_url: str
73
+ swhid: str
74
+ last_seen: datetime
75
+ match_type: MatchType = MatchType.EXACT
76
+ visit_count: int = 1
77
+ metadata: Dict[str, Any] = None
78
+ similarity_score: float = 1.0
79
+
80
+ def __post_init__(self):
81
+ """Initialize default metadata if None."""
82
+ if self.metadata is None:
83
+ self.metadata = {}
84
+
85
+
86
+ @dataclass
87
+ class SHAPIResponse:
88
+ """Wrapper for Software Heritage API responses."""
89
+
90
+ data: Any
91
+ headers: Dict[str, str]
92
+ status: int
93
+ cached: bool = False