dependabot-uv 0.301.0 → 0.301.1
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.
- checksums.yaml +4 -4
- data/lib/dependabot/uv/package/package_details_fetcher.rb +488 -0
- data/lib/dependabot/uv/{update_checker/index_finder.rb → package/package_registry_finder.rb} +12 -6
- data/lib/dependabot/uv/update_checker/latest_version_finder.rb +15 -269
- data/lib/dependabot/uv/update_checker/pip_version_resolver.rb +7 -5
- data/lib/dependabot/uv/update_checker.rb +2 -0
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 26af709186ad20b222961b28c19395f53edfcab51c9fdc0e92734cd06aa2d296
|
4
|
+
data.tar.gz: c26d70a21a979a655c952c940c0f32a10f89e58cb1d1588238b64c252bf96c48
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d0c2ac20cafd314d293a7a903947cd7fef9df95c5a85fae9f46e41d78920ae856949357cb4e6419c1804e95c19ba625c2a8fbd8bef43d9154d912c6aa31adb43
|
7
|
+
data.tar.gz: e1cb4fe17761e4f1433f9a55ae619a9271ec49359a3de0198c807f9393748633d9bfddb85002c01d782eadfc40f0a6598488fde5d9a7044e1c4e844198a26dd5
|
@@ -0,0 +1,488 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "json"
|
5
|
+
require "time"
|
6
|
+
require "cgi"
|
7
|
+
require "excon"
|
8
|
+
require "nokogiri"
|
9
|
+
require "sorbet-runtime"
|
10
|
+
require "dependabot/registry_client"
|
11
|
+
require "dependabot/uv/name_normaliser"
|
12
|
+
require "dependabot/package/package_release"
|
13
|
+
require "dependabot/package/package_details"
|
14
|
+
require "dependabot/uv/package/package_registry_finder"
|
15
|
+
|
16
|
+
# Stores metadata for a package, including all its available versions
|
17
|
+
module Dependabot
|
18
|
+
module Uv
|
19
|
+
module Package
|
20
|
+
CREDENTIALS_USERNAME = "username"
|
21
|
+
CREDENTIALS_PASSWORD = "password"
|
22
|
+
|
23
|
+
APPLICATION_JSON = "application/json"
|
24
|
+
APPLICATION_TEXT = "text/html"
|
25
|
+
CPYTHON = "cpython"
|
26
|
+
PYTHON = "python"
|
27
|
+
UNKNOWN = "unknown"
|
28
|
+
|
29
|
+
MAIN_PYPI_INDEXES = %w(
|
30
|
+
https://pypi.python.org/simple/
|
31
|
+
https://pypi.org/simple/
|
32
|
+
).freeze
|
33
|
+
VERSION_REGEX = /[0-9]+(?:\.[A-Za-z0-9\-_]+)*/
|
34
|
+
|
35
|
+
class PackageDetailsFetcher
|
36
|
+
extend T::Sig
|
37
|
+
|
38
|
+
sig do
|
39
|
+
params(
|
40
|
+
dependency: Dependabot::Dependency,
|
41
|
+
dependency_files: T::Array[Dependabot::DependencyFile],
|
42
|
+
credentials: T::Array[Dependabot::Credential]
|
43
|
+
).void
|
44
|
+
end
|
45
|
+
def initialize(
|
46
|
+
dependency:,
|
47
|
+
dependency_files:,
|
48
|
+
credentials:
|
49
|
+
)
|
50
|
+
@dependency = dependency
|
51
|
+
@dependency_files = dependency_files
|
52
|
+
@credentials = credentials
|
53
|
+
|
54
|
+
@registry_urls = T.let(nil, T.nilable(T::Array[String]))
|
55
|
+
end
|
56
|
+
|
57
|
+
sig { returns(Dependabot::Dependency) }
|
58
|
+
attr_reader :dependency
|
59
|
+
|
60
|
+
sig { returns(T::Array[T.untyped]) }
|
61
|
+
attr_reader :dependency_files
|
62
|
+
|
63
|
+
sig { returns(T::Array[T.untyped]) }
|
64
|
+
attr_reader :credentials
|
65
|
+
|
66
|
+
sig { returns(Dependabot::Package::PackageDetails) }
|
67
|
+
def fetch
|
68
|
+
package_releases = registry_urls
|
69
|
+
.select { |index_url| validate_index(index_url) } # Ensure only valid URLs
|
70
|
+
.flat_map do |index_url|
|
71
|
+
fetch_from_registry(index_url) || [] # Ensure it always returns an array
|
72
|
+
rescue Excon::Error::Timeout, Excon::Error::Socket
|
73
|
+
raise if MAIN_PYPI_INDEXES.include?(index_url)
|
74
|
+
|
75
|
+
raise PrivateSourceTimedOut, sanitized_url(index_url)
|
76
|
+
rescue URI::InvalidURIError
|
77
|
+
raise DependencyFileNotResolvable, "Invalid URL: #{sanitized_url(index_url)}"
|
78
|
+
end
|
79
|
+
|
80
|
+
Dependabot::Package::PackageDetails.new(
|
81
|
+
dependency: dependency,
|
82
|
+
releases: package_releases.reverse.uniq(&:version)
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
sig do
|
89
|
+
params(index_url: String)
|
90
|
+
.returns(T.nilable(T::Array[Dependabot::Package::PackageRelease]))
|
91
|
+
end
|
92
|
+
def fetch_from_registry(index_url)
|
93
|
+
if Dependabot::Experiments.enabled?(:enable_cooldown_for_uv)
|
94
|
+
metadata = fetch_from_json_registry(index_url)
|
95
|
+
|
96
|
+
return metadata if metadata&.any?
|
97
|
+
|
98
|
+
Dependabot.logger.warn("No valid versions found via JSON API. Falling back to HTML.")
|
99
|
+
end
|
100
|
+
fetch_from_html_registry(index_url)
|
101
|
+
rescue StandardError => e
|
102
|
+
Dependabot.logger.warn("Unexpected error in JSON fetch: #{e.message}. Falling back to HTML.")
|
103
|
+
fetch_from_html_registry(index_url)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Example JSON Response Format:
|
107
|
+
#
|
108
|
+
# {
|
109
|
+
# "info": {
|
110
|
+
# "name": "requests",
|
111
|
+
# "summary": "Python HTTP for Humans.",
|
112
|
+
# "author": "Kenneth Reitz",
|
113
|
+
# "license": "Apache-2.0"
|
114
|
+
# },
|
115
|
+
# "releases": {
|
116
|
+
# "2.32.3": [
|
117
|
+
# {
|
118
|
+
# "filename": "requests-2.32.3-py3-none-any.whl",
|
119
|
+
# "version": "2.32.3",
|
120
|
+
# "requires_python": ">=3.8",
|
121
|
+
# "yanked": false,
|
122
|
+
# "url": "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl"
|
123
|
+
# },
|
124
|
+
# {
|
125
|
+
# "filename": "requests-2.32.3.tar.gz",
|
126
|
+
# "version": "2.32.3",
|
127
|
+
# "requires_python": ">=3.8",
|
128
|
+
# "yanked": false,
|
129
|
+
# "url": "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz"
|
130
|
+
# }
|
131
|
+
# ],
|
132
|
+
# "2.27.0": [
|
133
|
+
# {
|
134
|
+
# "filename": "requests-2.27.0-py2.py3-none-any.whl",
|
135
|
+
# "version": "2.27.0",
|
136
|
+
# "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*",
|
137
|
+
# "yanked": false,
|
138
|
+
# "url": "https://files.pythonhosted.org/packages/47/01/f420e7add78110940639a958e5af0e3f8e07a8a8b62049bac55ee117aa91/requests-2.27.0-py2.py3-none-any.whl"
|
139
|
+
# },
|
140
|
+
# {
|
141
|
+
# "filename": "requests-2.27.0.tar.gz",
|
142
|
+
# "version": "2.27.0",
|
143
|
+
# "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*",
|
144
|
+
# "yanked": false,
|
145
|
+
# "url": "https://files.pythonhosted.org/packages/c0/e3/826e27b942352a74b656e8f58b4dc7ed9495ce2d4eeb498181167c615303/requests-2.27.0.tar.gz"
|
146
|
+
# }
|
147
|
+
# ]
|
148
|
+
# }
|
149
|
+
# }
|
150
|
+
sig do
|
151
|
+
params(index_url: String)
|
152
|
+
.returns(T.nilable(T::Array[Dependabot::Package::PackageRelease]))
|
153
|
+
end
|
154
|
+
def fetch_from_json_registry(index_url)
|
155
|
+
json_url = index_url.sub(%r{/simple/?$}i, "/pypi/")
|
156
|
+
|
157
|
+
Dependabot.logger.info(
|
158
|
+
"Fetching release information from json registry at #{sanitized_url(json_url)} for #{dependency.name}"
|
159
|
+
)
|
160
|
+
|
161
|
+
response = registry_json_response_for_dependency(json_url)
|
162
|
+
|
163
|
+
return nil unless response.status == 200
|
164
|
+
|
165
|
+
begin
|
166
|
+
data = JSON.parse(response.body)
|
167
|
+
|
168
|
+
version_releases = data["releases"]
|
169
|
+
|
170
|
+
releases = format_version_releases(version_releases)
|
171
|
+
|
172
|
+
releases.sort_by(&:version).reverse
|
173
|
+
rescue JSON::ParserError
|
174
|
+
Dependabot.logger.warn("JSON parsing error for #{json_url}. Falling back to HTML.")
|
175
|
+
nil
|
176
|
+
rescue StandardError => e
|
177
|
+
Dependabot.logger.warn("Unexpected error while fetching JSON data: #{e.message}.")
|
178
|
+
nil
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# This URL points to the Simple Index API for the "requests" package on PyPI.
|
183
|
+
# It provides an HTML listing of available package versions following PEP 503 (Simple Repository API).
|
184
|
+
# The information found here is useful for dependency resolution and package version retrieval.
|
185
|
+
#
|
186
|
+
# ✅ Information available in the Simple Index:
|
187
|
+
# - A list of package versions as anchor (`<a>`) elements.
|
188
|
+
# - URLs to distribution files (e.g., `.tar.gz`, `.whl`).
|
189
|
+
# - The `data-requires-python` attribute (if present) specifying the required Python version.
|
190
|
+
# - An optional `data-yanked` attribute indicating a yanked (withdrawn) version.
|
191
|
+
#
|
192
|
+
# ❌ Information NOT available in the Simple Index:
|
193
|
+
# - Release timestamps (upload time).
|
194
|
+
# - File digests (hashes like SHA256, MD5).
|
195
|
+
# - Package metadata such as description, author, or dependencies.
|
196
|
+
# - Download statistics.
|
197
|
+
# - Package type (`sdist` or `bdist_wheel`).
|
198
|
+
#
|
199
|
+
# To obtain full package metadata, use the PyPI JSON API:
|
200
|
+
# - JSON API: https://pypi.org/pypi/requests/json
|
201
|
+
#
|
202
|
+
# More details: https://www.python.org/dev/peps/pep-0503/
|
203
|
+
sig { params(index_url: String).returns(T::Array[Dependabot::Package::PackageRelease]) }
|
204
|
+
def fetch_from_html_registry(index_url)
|
205
|
+
Dependabot.logger.info(
|
206
|
+
"Fetching release information from html registry at #{sanitized_url(index_url)} for #{dependency.name}"
|
207
|
+
)
|
208
|
+
index_response = registry_response_for_dependency(index_url)
|
209
|
+
if index_response.status == 401 || index_response.status == 403
|
210
|
+
registry_index_response = registry_index_response(index_url)
|
211
|
+
|
212
|
+
if registry_index_response.status == 401 || registry_index_response.status == 403
|
213
|
+
raise PrivateSourceAuthenticationFailure, sanitized_url(index_url)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
version_releases = extract_release_details_json_from_html(index_response.body)
|
218
|
+
releases = format_version_releases(version_releases)
|
219
|
+
|
220
|
+
releases.sort_by(&:version).reverse
|
221
|
+
end
|
222
|
+
|
223
|
+
sig do
|
224
|
+
params(html_body: String)
|
225
|
+
.returns(T::Hash[String, T::Array[T::Hash[String, T.untyped]]]) # Returns JSON-like format
|
226
|
+
end
|
227
|
+
def extract_release_details_json_from_html(html_body)
|
228
|
+
doc = Nokogiri::HTML(html_body)
|
229
|
+
|
230
|
+
releases = {}
|
231
|
+
|
232
|
+
doc.css("a").each do |a_tag|
|
233
|
+
details = version_details_from_link(a_tag.to_s)
|
234
|
+
if details && details["version"]
|
235
|
+
releases[details["version"]] ||= []
|
236
|
+
releases[details["version"]] << details
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
releases
|
241
|
+
end
|
242
|
+
|
243
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
244
|
+
sig do
|
245
|
+
params(link: T.nilable(String))
|
246
|
+
.returns(T.nilable(T::Hash[String, T.untyped]))
|
247
|
+
end
|
248
|
+
def version_details_from_link(link)
|
249
|
+
return unless link
|
250
|
+
|
251
|
+
doc = Nokogiri::XML(link)
|
252
|
+
filename = doc.at_css("a")&.content
|
253
|
+
url = doc.at_css("a")&.attributes&.fetch("href", nil)&.value
|
254
|
+
|
255
|
+
return unless filename&.match?(name_regex) || url&.match?(name_regex)
|
256
|
+
|
257
|
+
version = get_version_from_filename(filename)
|
258
|
+
return unless version_class.correct?(version)
|
259
|
+
|
260
|
+
{
|
261
|
+
"version" => version,
|
262
|
+
"requires_python" => requires_python_from_link(link),
|
263
|
+
"yanked" => link.include?("data-yanked"),
|
264
|
+
"url" => link
|
265
|
+
}
|
266
|
+
end
|
267
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
268
|
+
|
269
|
+
sig do
|
270
|
+
params(
|
271
|
+
releases_json: T.nilable(T::Hash[String, T::Array[T::Hash[String, T.untyped]]])
|
272
|
+
)
|
273
|
+
.returns(T::Array[Dependabot::Package::PackageRelease])
|
274
|
+
end
|
275
|
+
def format_version_releases(releases_json)
|
276
|
+
return [] unless releases_json
|
277
|
+
|
278
|
+
releases_json.each_with_object([]) do |(version, release_data_array), versions|
|
279
|
+
release_data = release_data_array.last
|
280
|
+
|
281
|
+
next unless release_data
|
282
|
+
|
283
|
+
release = format_version_release(version, release_data)
|
284
|
+
|
285
|
+
next unless release
|
286
|
+
|
287
|
+
versions << release
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
sig do
|
292
|
+
params(
|
293
|
+
version: String,
|
294
|
+
release_data: T::Hash[String, T.untyped]
|
295
|
+
)
|
296
|
+
.returns(T.nilable(Dependabot::Package::PackageRelease))
|
297
|
+
end
|
298
|
+
def format_version_release(version, release_data)
|
299
|
+
upload_time = release_data["upload_time"]
|
300
|
+
released_at = Time.parse(upload_time) if upload_time
|
301
|
+
yanked = release_data["yanked"] || false
|
302
|
+
yanked_reason = release_data["yanked_reason"]
|
303
|
+
downloads = release_data["downloads"] || -1
|
304
|
+
url = release_data["url"]
|
305
|
+
package_type = release_data["packagetype"]
|
306
|
+
language = package_language(
|
307
|
+
python_version: release_data["python_version"],
|
308
|
+
requires_python: release_data["requires_python"]
|
309
|
+
)
|
310
|
+
|
311
|
+
release = Dependabot::Package::PackageRelease.new(
|
312
|
+
version: Dependabot::Uv::Version.new(version),
|
313
|
+
released_at: released_at,
|
314
|
+
yanked: yanked,
|
315
|
+
yanked_reason: yanked_reason,
|
316
|
+
downloads: downloads,
|
317
|
+
url: url,
|
318
|
+
package_type: package_type,
|
319
|
+
language: language
|
320
|
+
)
|
321
|
+
release
|
322
|
+
end
|
323
|
+
|
324
|
+
sig do
|
325
|
+
params(
|
326
|
+
python_version: T.nilable(String),
|
327
|
+
requires_python: T.nilable(String)
|
328
|
+
)
|
329
|
+
.returns(T.nilable(Dependabot::Package::PackageLanguage))
|
330
|
+
end
|
331
|
+
def package_language(python_version:, requires_python:)
|
332
|
+
# Extract language name and version
|
333
|
+
language_name, language_version = convert_language_version(python_version)
|
334
|
+
|
335
|
+
# Extract language requirement
|
336
|
+
language_requirement = build_python_requirement(requires_python)
|
337
|
+
|
338
|
+
return nil unless language_version || language_requirement
|
339
|
+
|
340
|
+
# Return a Language object with all details
|
341
|
+
Dependabot::Package::PackageLanguage.new(
|
342
|
+
name: language_name,
|
343
|
+
version: language_version,
|
344
|
+
requirement: language_requirement
|
345
|
+
)
|
346
|
+
end
|
347
|
+
|
348
|
+
sig { params(version: T.nilable(String)).returns([String, T.nilable(Dependabot::Version)]) }
|
349
|
+
def convert_language_version(version)
|
350
|
+
return ["python", nil] if version.nil? || version == "source"
|
351
|
+
|
352
|
+
# Extract numeric parts dynamically (e.g., "cp37" -> "3.7", "py38" -> "3.8")
|
353
|
+
extracted_version = version.scan(/\d+/).join(".")
|
354
|
+
|
355
|
+
# Detect the language implementation
|
356
|
+
language_name = if version.start_with?("cp")
|
357
|
+
"cpython" # CPython implementation
|
358
|
+
elsif version.start_with?("py")
|
359
|
+
"python" # General Python compatibility
|
360
|
+
else
|
361
|
+
"unknown" # Fallback for unknown cases
|
362
|
+
end
|
363
|
+
|
364
|
+
# Ensure extracted version is valid before converting
|
365
|
+
language_version =
|
366
|
+
extracted_version.match?(/^\d+(\.\d+)*$/) ? Dependabot::Version.new(extracted_version) : nil
|
367
|
+
|
368
|
+
Dependabot.logger.warn("Skipping invalid language_version: #{version.inspect}") if language_version.nil?
|
369
|
+
|
370
|
+
[language_name, language_version]
|
371
|
+
end
|
372
|
+
|
373
|
+
sig { returns(T::Array[String]) }
|
374
|
+
def registry_urls
|
375
|
+
@registry_urls ||=
|
376
|
+
Package::PackageRegistryFinder.new(
|
377
|
+
dependency_files: dependency_files,
|
378
|
+
credentials: credentials,
|
379
|
+
dependency: dependency
|
380
|
+
).registry_urls
|
381
|
+
end
|
382
|
+
|
383
|
+
sig { returns(String) }
|
384
|
+
def normalised_name
|
385
|
+
NameNormaliser.normalise(dependency.name)
|
386
|
+
end
|
387
|
+
|
388
|
+
sig { params(json_url: String).returns(Excon::Response) }
|
389
|
+
def registry_json_response_for_dependency(json_url)
|
390
|
+
url = "#{json_url.chomp('/')}/#{@dependency.name}/json"
|
391
|
+
Dependabot::RegistryClient.get(
|
392
|
+
url: url,
|
393
|
+
headers: { "Accept" => APPLICATION_JSON }
|
394
|
+
)
|
395
|
+
end
|
396
|
+
|
397
|
+
sig { params(index_url: String).returns(Excon::Response) }
|
398
|
+
def registry_response_for_dependency(index_url)
|
399
|
+
Dependabot::RegistryClient.get(
|
400
|
+
url: index_url + normalised_name + "/",
|
401
|
+
headers: { "Accept" => APPLICATION_TEXT }
|
402
|
+
)
|
403
|
+
end
|
404
|
+
|
405
|
+
sig { params(index_url: String).returns(Excon::Response) }
|
406
|
+
def registry_index_response(index_url)
|
407
|
+
Dependabot::RegistryClient.get(
|
408
|
+
url: index_url,
|
409
|
+
headers: { "Accept" => APPLICATION_TEXT }
|
410
|
+
)
|
411
|
+
end
|
412
|
+
|
413
|
+
sig { params(filename: String).returns(T.nilable(String)) }
|
414
|
+
def get_version_from_filename(filename)
|
415
|
+
filename
|
416
|
+
.gsub(/#{name_regex}-/i, "")
|
417
|
+
.split(/-|\.tar\.|\.zip|\.whl/)
|
418
|
+
.first
|
419
|
+
end
|
420
|
+
|
421
|
+
sig do
|
422
|
+
params(req_string: T.nilable(String))
|
423
|
+
.returns(T.nilable(Dependabot::Requirement))
|
424
|
+
end
|
425
|
+
def build_python_requirement(req_string)
|
426
|
+
return nil unless req_string
|
427
|
+
|
428
|
+
requirement_class.new(CGI.unescapeHTML(req_string))
|
429
|
+
rescue Gem::Requirement::BadRequirementError
|
430
|
+
nil
|
431
|
+
end
|
432
|
+
|
433
|
+
sig { params(link: String).returns(T.nilable(String)) }
|
434
|
+
def requires_python_from_link(link)
|
435
|
+
raw_value = Nokogiri::XML(link)
|
436
|
+
.at_css("a")
|
437
|
+
&.attribute("data-requires-python")
|
438
|
+
&.content
|
439
|
+
|
440
|
+
return nil unless raw_value
|
441
|
+
|
442
|
+
CGI.unescapeHTML(raw_value) # Decodes HTML entities like >=3 → >=3
|
443
|
+
end
|
444
|
+
|
445
|
+
sig { returns(T.class_of(Dependabot::Version)) }
|
446
|
+
def version_class
|
447
|
+
dependency.version_class
|
448
|
+
end
|
449
|
+
|
450
|
+
sig { returns(T.class_of(Dependabot::Requirement)) }
|
451
|
+
def requirement_class
|
452
|
+
dependency.requirement_class
|
453
|
+
end
|
454
|
+
|
455
|
+
sig { params(index_url: String).returns(T::Hash[String, String]) }
|
456
|
+
def auth_headers_for(index_url)
|
457
|
+
credential = @credentials.find { |cred| cred["index-url"] == index_url }
|
458
|
+
return {} unless credential
|
459
|
+
|
460
|
+
{ "Authorization" => "Basic #{Base64.strict_encode64(
|
461
|
+
"#{credential[CREDENTIALS_USERNAME]}:#{credential[CREDENTIALS_PASSWORD]}"
|
462
|
+
)}" }
|
463
|
+
end
|
464
|
+
|
465
|
+
sig { returns(Regexp) }
|
466
|
+
def name_regex
|
467
|
+
parts = normalised_name.split(/[\s_.-]/).map { |n| Regexp.quote(n) }
|
468
|
+
/#{parts.join("[\s_.-]")}/i
|
469
|
+
end
|
470
|
+
|
471
|
+
sig { params(index_url: T.nilable(String)).returns(T::Boolean) }
|
472
|
+
def validate_index(index_url)
|
473
|
+
return false unless index_url
|
474
|
+
|
475
|
+
return true if index_url.match?(URI::DEFAULT_PARSER.regexp[:ABS_URI])
|
476
|
+
|
477
|
+
raise Dependabot::DependencyFileNotResolvable,
|
478
|
+
"Invalid URL: #{sanitized_url(index_url)}"
|
479
|
+
end
|
480
|
+
|
481
|
+
sig { params(index_url: String).returns(String) }
|
482
|
+
def sanitized_url(index_url)
|
483
|
+
index_url.sub(%r{//([^/@]+)@}, "//redacted@")
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|
488
|
+
end
|
data/lib/dependabot/uv/{update_checker/index_finder.rb → package/package_registry_finder.rb}
RENAMED
@@ -7,8 +7,9 @@ require "dependabot/errors"
|
|
7
7
|
|
8
8
|
module Dependabot
|
9
9
|
module Uv
|
10
|
-
|
11
|
-
class
|
10
|
+
module Package
|
11
|
+
class PackageRegistryFinder
|
12
|
+
extend T::Sig
|
12
13
|
PYPI_BASE_URL = "https://pypi.org/simple/"
|
13
14
|
ENVIRONMENT_VARIABLE_REGEX = /\$\{.+\}/
|
14
15
|
|
@@ -18,7 +19,8 @@ module Dependabot
|
|
18
19
|
@dependency = dependency
|
19
20
|
end
|
20
21
|
|
21
|
-
|
22
|
+
sig { returns(T::Array[String]) }
|
23
|
+
def registry_urls
|
22
24
|
extra_index_urls =
|
23
25
|
config_variable_index_urls[:extra] +
|
24
26
|
pipfile_index_urls[:extra] +
|
@@ -140,7 +142,7 @@ module Dependabot
|
|
140
142
|
end
|
141
143
|
|
142
144
|
def config_variable_index_urls
|
143
|
-
urls = { main: nil, extra: [] }
|
145
|
+
urls = { main: T.let(nil, T.nilable(String)), extra: [] }
|
144
146
|
|
145
147
|
index_url_creds = credentials
|
146
148
|
.select { |cred| cred["type"] == "python_index" }
|
@@ -158,7 +160,7 @@ module Dependabot
|
|
158
160
|
end
|
159
161
|
|
160
162
|
def clean_check_and_remove_environment_variables(url)
|
161
|
-
url = url.strip.
|
163
|
+
url = url.strip.sub(%r{/+$}, "") + "/"
|
162
164
|
|
163
165
|
return authed_base_url(url) unless url.match?(ENVIRONMENT_VARIABLE_REGEX)
|
164
166
|
|
@@ -190,7 +192,11 @@ module Dependabot
|
|
190
192
|
cred = credential_for(base_url)
|
191
193
|
return base_url unless cred
|
192
194
|
|
193
|
-
AuthedUrlBuilder.authed_url(credential: cred)
|
195
|
+
builder = AuthedUrlBuilder.authed_url(credential: cred)
|
196
|
+
|
197
|
+
return base_url unless builder
|
198
|
+
|
199
|
+
builder.gsub(%r{/*$}, "") + "/"
|
194
200
|
end
|
195
201
|
|
196
202
|
def credential_for(url)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strong
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require "cgi"
|
@@ -12,284 +12,30 @@ require "dependabot/update_checkers/version_filters"
|
|
12
12
|
require "dependabot/registry_client"
|
13
13
|
require "dependabot/uv/authed_url_builder"
|
14
14
|
require "dependabot/uv/name_normaliser"
|
15
|
+
require "dependabot/uv/package/package_registry_finder"
|
16
|
+
require "dependabot/uv/package/package_details_fetcher"
|
17
|
+
require "dependabot/package/package_latest_version_finder"
|
15
18
|
|
16
19
|
module Dependabot
|
17
20
|
module Uv
|
18
21
|
class UpdateChecker
|
19
|
-
class LatestVersionFinder
|
22
|
+
class LatestVersionFinder < Dependabot::Package::PackageLatestVersionFinder
|
20
23
|
extend T::Sig
|
21
24
|
|
22
|
-
require_relative "index_finder"
|
23
|
-
|
24
|
-
def initialize(dependency:, dependency_files:, credentials:,
|
25
|
-
ignored_versions:, raise_on_ignored: false,
|
26
|
-
security_advisories:)
|
27
|
-
@dependency = dependency
|
28
|
-
@dependency_files = dependency_files
|
29
|
-
@credentials = credentials
|
30
|
-
@ignored_versions = ignored_versions
|
31
|
-
@raise_on_ignored = raise_on_ignored
|
32
|
-
@security_advisories = security_advisories
|
33
|
-
end
|
34
|
-
|
35
|
-
def latest_version(python_version: nil)
|
36
|
-
@latest_version ||=
|
37
|
-
fetch_latest_version(python_version: python_version)
|
38
|
-
end
|
39
|
-
|
40
|
-
def latest_version_with_no_unlock(python_version: nil)
|
41
|
-
@latest_version_with_no_unlock ||=
|
42
|
-
fetch_latest_version_with_no_unlock(python_version: python_version)
|
43
|
-
end
|
44
|
-
|
45
|
-
def lowest_security_fix_version(python_version: nil)
|
46
|
-
@lowest_security_fix_version ||=
|
47
|
-
fetch_lowest_security_fix_version(python_version: python_version)
|
48
|
-
end
|
49
|
-
|
50
|
-
private
|
51
|
-
|
52
|
-
attr_reader :dependency
|
53
|
-
attr_reader :dependency_files
|
54
|
-
attr_reader :credentials
|
55
|
-
attr_reader :ignored_versions
|
56
|
-
attr_reader :security_advisories
|
57
|
-
|
58
|
-
def fetch_latest_version(python_version:)
|
59
|
-
versions = available_versions
|
60
|
-
versions = filter_yanked_versions(versions)
|
61
|
-
versions = filter_unsupported_versions(versions, python_version)
|
62
|
-
versions = filter_prerelease_versions(versions)
|
63
|
-
versions = filter_ignored_versions(versions)
|
64
|
-
versions.max
|
65
|
-
end
|
66
|
-
|
67
|
-
def fetch_latest_version_with_no_unlock(python_version:)
|
68
|
-
versions = available_versions
|
69
|
-
versions = filter_yanked_versions(versions)
|
70
|
-
versions = filter_unsupported_versions(versions, python_version)
|
71
|
-
versions = filter_prerelease_versions(versions)
|
72
|
-
versions = filter_ignored_versions(versions)
|
73
|
-
versions = filter_out_of_range_versions(versions)
|
74
|
-
versions.max
|
75
|
-
end
|
76
|
-
|
77
|
-
def fetch_lowest_security_fix_version(python_version:)
|
78
|
-
versions = available_versions
|
79
|
-
versions = filter_yanked_versions(versions)
|
80
|
-
versions = filter_unsupported_versions(versions, python_version)
|
81
|
-
versions = filter_prerelease_versions(versions)
|
82
|
-
versions = Dependabot::UpdateCheckers::VersionFilters.filter_vulnerable_versions(versions,
|
83
|
-
security_advisories)
|
84
|
-
versions = filter_ignored_versions(versions)
|
85
|
-
versions = filter_lower_versions(versions)
|
86
|
-
|
87
|
-
versions.min
|
88
|
-
end
|
89
|
-
|
90
|
-
sig { params(versions_array: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
|
91
|
-
def filter_yanked_versions(versions_array)
|
92
|
-
filtered = versions_array.reject { |details| details.fetch(:yanked) }
|
93
|
-
if versions_array.count > filtered.count
|
94
|
-
Dependabot.logger.info("Filtered out #{versions_array.count - filtered.count} yanked versions")
|
95
|
-
end
|
96
|
-
filtered
|
97
|
-
end
|
98
|
-
|
99
25
|
sig do
|
100
|
-
|
101
|
-
.returns(T::Array[T.untyped])
|
102
|
-
end
|
103
|
-
def filter_unsupported_versions(versions_array, python_version)
|
104
|
-
filtered = versions_array.filter_map do |details|
|
105
|
-
python_requirement = details.fetch(:python_requirement)
|
106
|
-
next details.fetch(:version) unless python_version
|
107
|
-
next details.fetch(:version) unless python_requirement
|
108
|
-
next unless python_requirement.satisfied_by?(python_version)
|
109
|
-
|
110
|
-
details.fetch(:version)
|
111
|
-
end
|
112
|
-
if versions_array.count > filtered.count
|
113
|
-
delta = versions_array.count - filtered.count
|
114
|
-
Dependabot.logger.info("Filtered out #{delta} unsupported Python #{python_version} versions")
|
115
|
-
end
|
116
|
-
filtered
|
117
|
-
end
|
118
|
-
|
119
|
-
sig { params(versions_array: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
|
120
|
-
def filter_prerelease_versions(versions_array)
|
121
|
-
return versions_array if wants_prerelease?
|
122
|
-
|
123
|
-
filtered = versions_array.reject(&:prerelease?)
|
124
|
-
|
125
|
-
if versions_array.count > filtered.count
|
126
|
-
Dependabot.logger.info("Filtered out #{versions_array.count - filtered.count} pre-release versions")
|
127
|
-
end
|
128
|
-
|
129
|
-
filtered
|
130
|
-
end
|
131
|
-
|
132
|
-
sig { params(versions_array: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
|
133
|
-
def filter_ignored_versions(versions_array)
|
134
|
-
filtered = versions_array
|
135
|
-
.reject { |v| ignore_requirements.any? { |r| r.satisfied_by?(v) } }
|
136
|
-
if @raise_on_ignored && filter_lower_versions(filtered).empty? && filter_lower_versions(versions_array).any?
|
137
|
-
raise Dependabot::AllVersionsIgnored
|
138
|
-
end
|
139
|
-
|
140
|
-
if versions_array.count > filtered.count
|
141
|
-
Dependabot.logger.info("Filtered out #{versions_array.count - filtered.count} ignored versions")
|
142
|
-
end
|
143
|
-
filtered
|
144
|
-
end
|
145
|
-
|
146
|
-
def filter_lower_versions(versions_array)
|
147
|
-
return versions_array unless dependency.numeric_version
|
148
|
-
|
149
|
-
versions_array.select { |version| version > dependency.numeric_version }
|
150
|
-
end
|
151
|
-
|
152
|
-
def filter_out_of_range_versions(versions_array)
|
153
|
-
reqs = dependency.requirements.filter_map do |r|
|
154
|
-
requirement_class.requirements_array(r.fetch(:requirement))
|
155
|
-
end
|
156
|
-
|
157
|
-
versions_array
|
158
|
-
.select { |v| reqs.all? { |r| r.any? { |o| o.satisfied_by?(v) } } }
|
159
|
-
end
|
160
|
-
|
161
|
-
def wants_prerelease?
|
162
|
-
return version_class.new(dependency.version).prerelease? if dependency.version
|
163
|
-
|
164
|
-
dependency.requirements.any? do |req|
|
165
|
-
reqs = (req.fetch(:requirement) || "").split(",").map(&:strip)
|
166
|
-
reqs.any? { |r| r.match?(/[A-Za-z]/) }
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
# See https://www.python.org/dev/peps/pep-0503/ for details of the
|
171
|
-
# Simple Repository API we use here.
|
172
|
-
def available_versions
|
173
|
-
@available_versions ||=
|
174
|
-
index_urls.flat_map do |index_url|
|
175
|
-
validate_index(index_url)
|
176
|
-
|
177
|
-
sanitized_url = index_url.gsub(%r{(?<=//).*(?=@)}, "redacted")
|
178
|
-
|
179
|
-
index_response = registry_response_for_dependency(index_url)
|
180
|
-
if index_response.status == 401 || index_response.status == 403
|
181
|
-
registry_index_response = registry_index_response(index_url)
|
182
|
-
|
183
|
-
if registry_index_response.status == 401 || registry_index_response.status == 403
|
184
|
-
raise PrivateSourceAuthenticationFailure, sanitized_url
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
version_links = []
|
189
|
-
index_response.body.scan(%r{<a\s.*?>.*?</a>}m) do
|
190
|
-
details = version_details_from_link(Regexp.last_match.to_s)
|
191
|
-
version_links << details if details
|
192
|
-
end
|
193
|
-
|
194
|
-
version_links.compact
|
195
|
-
rescue Excon::Error::Timeout, Excon::Error::Socket
|
196
|
-
raise if MAIN_PYPI_INDEXES.include?(index_url)
|
197
|
-
|
198
|
-
raise PrivateSourceTimedOut, sanitized_url
|
199
|
-
rescue URI::InvalidURIError
|
200
|
-
raise DependencyFileNotResolvable, "Invalid URL: #{sanitized_url}"
|
201
|
-
end
|
26
|
+
override.returns(T.nilable(Dependabot::Package::PackageDetails))
|
202
27
|
end
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
return unless filename&.match?(name_regex) || url&.match?(name_regex)
|
210
|
-
|
211
|
-
version = get_version_from_filename(filename)
|
212
|
-
return unless version_class.correct?(version)
|
213
|
-
|
214
|
-
{
|
215
|
-
version: version_class.new(version),
|
216
|
-
python_requirement: build_python_requirement_from_link(link),
|
217
|
-
yanked: link&.include?("data-yanked")
|
218
|
-
}
|
219
|
-
end
|
220
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
221
|
-
|
222
|
-
def get_version_from_filename(filename)
|
223
|
-
filename
|
224
|
-
.gsub(/#{name_regex}-/i, "")
|
225
|
-
.split(/-|\.tar\.|\.zip|\.whl/)
|
226
|
-
.first
|
227
|
-
end
|
228
|
-
|
229
|
-
def build_python_requirement_from_link(link)
|
230
|
-
req_string = Nokogiri::XML(link)
|
231
|
-
.at_css("a")
|
232
|
-
&.attribute("data-requires-python")
|
233
|
-
&.content
|
234
|
-
|
235
|
-
return unless req_string
|
236
|
-
|
237
|
-
requirement_class.new(CGI.unescapeHTML(req_string))
|
238
|
-
rescue Gem::Requirement::BadRequirementError
|
239
|
-
nil
|
28
|
+
def package_details
|
29
|
+
@package_details ||= Package::PackageDetailsFetcher.new(
|
30
|
+
dependency: dependency,
|
31
|
+
dependency_files: dependency_files,
|
32
|
+
credentials: credentials
|
33
|
+
).fetch
|
240
34
|
end
|
241
35
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
dependency_files: dependency_files,
|
246
|
-
credentials: credentials,
|
247
|
-
dependency: dependency
|
248
|
-
).index_urls
|
249
|
-
end
|
250
|
-
|
251
|
-
def registry_response_for_dependency(index_url)
|
252
|
-
Dependabot::RegistryClient.get(
|
253
|
-
url: index_url + normalised_name + "/",
|
254
|
-
headers: { "Accept" => "text/html" }
|
255
|
-
)
|
256
|
-
end
|
257
|
-
|
258
|
-
def registry_index_response(index_url)
|
259
|
-
Dependabot::RegistryClient.get(
|
260
|
-
url: index_url,
|
261
|
-
headers: { "Accept" => "text/html" }
|
262
|
-
)
|
263
|
-
end
|
264
|
-
|
265
|
-
def ignore_requirements
|
266
|
-
ignored_versions.flat_map { |req| requirement_class.requirements_array(req) }
|
267
|
-
end
|
268
|
-
|
269
|
-
def normalised_name
|
270
|
-
NameNormaliser.normalise(dependency.name)
|
271
|
-
end
|
272
|
-
|
273
|
-
def name_regex
|
274
|
-
parts = normalised_name.split(/[\s_.-]/).map { |n| Regexp.quote(n) }
|
275
|
-
/#{parts.join("[\s_.-]")}/i
|
276
|
-
end
|
277
|
-
|
278
|
-
def version_class
|
279
|
-
dependency.version_class
|
280
|
-
end
|
281
|
-
|
282
|
-
def requirement_class
|
283
|
-
dependency.requirement_class
|
284
|
-
end
|
285
|
-
|
286
|
-
def validate_index(index_url)
|
287
|
-
sanitized_url = index_url.gsub(%r{(?<=//).*(?=@)}, "redacted")
|
288
|
-
|
289
|
-
return if index_url&.match?(URI::DEFAULT_PARSER.regexp[:ABS_URI])
|
290
|
-
|
291
|
-
raise Dependabot::DependencyFileNotResolvable,
|
292
|
-
"Invalid URL: #{sanitized_url}"
|
36
|
+
sig { override.returns(T::Boolean) }
|
37
|
+
def cooldown_enabled?
|
38
|
+
Dependabot::Experiments.enabled?(:enable_cooldown_for_uv)
|
293
39
|
end
|
294
40
|
end
|
295
41
|
end
|
@@ -11,28 +11,29 @@ module Dependabot
|
|
11
11
|
class UpdateChecker
|
12
12
|
class PipVersionResolver
|
13
13
|
def initialize(dependency:, dependency_files:, credentials:,
|
14
|
-
ignored_versions:, raise_on_ignored: false,
|
14
|
+
ignored_versions:, update_cooldown: nil, raise_on_ignored: false,
|
15
15
|
security_advisories:)
|
16
|
-
@dependency
|
16
|
+
@dependency = dependency
|
17
17
|
@dependency_files = dependency_files
|
18
18
|
@credentials = credentials
|
19
19
|
@ignored_versions = ignored_versions
|
20
|
+
@update_cooldown = update_cooldown
|
20
21
|
@raise_on_ignored = raise_on_ignored
|
21
22
|
@security_advisories = security_advisories
|
22
23
|
end
|
23
24
|
|
24
25
|
def latest_resolvable_version
|
25
|
-
latest_version_finder.latest_version(
|
26
|
+
latest_version_finder.latest_version(language_version: language_version_manager.python_version)
|
26
27
|
end
|
27
28
|
|
28
29
|
def latest_resolvable_version_with_no_unlock
|
29
30
|
latest_version_finder
|
30
|
-
.latest_version_with_no_unlock(
|
31
|
+
.latest_version_with_no_unlock(language_version: language_version_manager.python_version)
|
31
32
|
end
|
32
33
|
|
33
34
|
def lowest_resolvable_security_fix_version
|
34
35
|
latest_version_finder
|
35
|
-
.lowest_security_fix_version(
|
36
|
+
.lowest_security_fix_version(language_version: language_version_manager.python_version)
|
36
37
|
end
|
37
38
|
|
38
39
|
private
|
@@ -50,6 +51,7 @@ module Dependabot
|
|
50
51
|
credentials: credentials,
|
51
52
|
ignored_versions: ignored_versions,
|
52
53
|
raise_on_ignored: @raise_on_ignored,
|
54
|
+
cooldown_options: @update_cooldown,
|
53
55
|
security_advisories: security_advisories
|
54
56
|
)
|
55
57
|
end
|
@@ -170,6 +170,7 @@ module Dependabot
|
|
170
170
|
credentials: credentials,
|
171
171
|
ignored_versions: ignored_versions,
|
172
172
|
raise_on_ignored: @raise_on_ignored,
|
173
|
+
update_cooldown: @update_cooldown,
|
173
174
|
security_advisories: security_advisories
|
174
175
|
)
|
175
176
|
end
|
@@ -241,6 +242,7 @@ module Dependabot
|
|
241
242
|
credentials: credentials,
|
242
243
|
ignored_versions: ignored_versions,
|
243
244
|
raise_on_ignored: @raise_on_ignored,
|
245
|
+
cooldown_options: @update_cooldown,
|
244
246
|
security_advisories: security_advisories
|
245
247
|
)
|
246
248
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dependabot-uv
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.301.
|
4
|
+
version: 0.301.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dependabot
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-03-
|
11
|
+
date: 2025-03-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dependabot-common
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.301.
|
19
|
+
version: 0.301.1
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.301.
|
26
|
+
version: 0.301.1
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: debug
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -266,13 +266,14 @@ files:
|
|
266
266
|
- lib/dependabot/uv/metadata_finder.rb
|
267
267
|
- lib/dependabot/uv/name_normaliser.rb
|
268
268
|
- lib/dependabot/uv/native_helpers.rb
|
269
|
+
- lib/dependabot/uv/package/package_details_fetcher.rb
|
270
|
+
- lib/dependabot/uv/package/package_registry_finder.rb
|
269
271
|
- lib/dependabot/uv/package_manager.rb
|
270
272
|
- lib/dependabot/uv/pipenv_runner.rb
|
271
273
|
- lib/dependabot/uv/requirement.rb
|
272
274
|
- lib/dependabot/uv/requirement_parser.rb
|
273
275
|
- lib/dependabot/uv/requirements_file_matcher.rb
|
274
276
|
- lib/dependabot/uv/update_checker.rb
|
275
|
-
- lib/dependabot/uv/update_checker/index_finder.rb
|
276
277
|
- lib/dependabot/uv/update_checker/latest_version_finder.rb
|
277
278
|
- lib/dependabot/uv/update_checker/lock_file_resolver.rb
|
278
279
|
- lib/dependabot/uv/update_checker/pip_compile_version_resolver.rb
|
@@ -284,7 +285,7 @@ licenses:
|
|
284
285
|
- MIT
|
285
286
|
metadata:
|
286
287
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
287
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.301.
|
288
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.301.1
|
288
289
|
post_install_message:
|
289
290
|
rdoc_options: []
|
290
291
|
require_paths:
|