dependabot-python 0.298.0 → 0.299.0
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/python/package/package_details_fetcher.rb +485 -0
- data/lib/dependabot/python/{update_checker/index_finder.rb → package/package_registry_finder.rb} +4 -4
- data/lib/dependabot/python/update_checker/latest_version_finder.rb +13 -272
- data/lib/dependabot/python/update_checker/pip_version_resolver.rb +3 -3
- 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: d429e2e29dd9b40c0213abc4255d683c03153ebd78508ef1d01c8a67c0779606
|
4
|
+
data.tar.gz: 87e287bf9f902c64ab1d1c99c33262534b997e14b048df39215b6d9e30226e36
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9bdeb41bf8491bc81f7970ad48a57d0e7c792e54bb010d62adec1247da87b654db158e834351632aff89968a28e1bbfacb6f6981174d73c488dc474dd95070d0
|
7
|
+
data.tar.gz: 31e151c0043b9ad2f634b2833ac624676ffad90071b438833036fcd4eb426307c2622645eb9735b77b3b220a1974caf49f67f97908ce1223e2f429c54c0045ae
|
@@ -0,0 +1,485 @@
|
|
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/python/name_normaliser"
|
12
|
+
require "dependabot/package/package_release"
|
13
|
+
require "dependabot/package/package_details"
|
14
|
+
require "dependabot/python/package/package_registry_finder"
|
15
|
+
|
16
|
+
# Stores metadata for a package, including all its available versions
|
17
|
+
module Dependabot
|
18
|
+
module Python
|
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_python)
|
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::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
|
+
releases_json.each_with_object([]) do |(version, release_data_array), versions|
|
277
|
+
release_data = release_data_array.last
|
278
|
+
|
279
|
+
next unless release_data
|
280
|
+
|
281
|
+
release = format_version_release(version, release_data)
|
282
|
+
|
283
|
+
next unless release
|
284
|
+
|
285
|
+
versions << release
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
sig do
|
290
|
+
params(
|
291
|
+
version: String,
|
292
|
+
release_data: T::Hash[String, T.untyped]
|
293
|
+
)
|
294
|
+
.returns(T.nilable(Dependabot::Package::PackageRelease))
|
295
|
+
end
|
296
|
+
def format_version_release(version, release_data)
|
297
|
+
upload_time = release_data["upload_time"]
|
298
|
+
released_at = Time.parse(upload_time) if upload_time
|
299
|
+
yanked = release_data["yanked"] || false
|
300
|
+
yanked_reason = release_data["yanked_reason"]
|
301
|
+
downloads = release_data["downloads"] || -1
|
302
|
+
url = release_data["url"]
|
303
|
+
package_type = release_data["packagetype"]
|
304
|
+
language = package_language(
|
305
|
+
python_version: release_data["python_version"],
|
306
|
+
requires_python: release_data["requires_python"]
|
307
|
+
)
|
308
|
+
|
309
|
+
release = Dependabot::Package::PackageRelease.new(
|
310
|
+
version: Dependabot::Python::Version.new(version),
|
311
|
+
released_at: released_at,
|
312
|
+
yanked: yanked,
|
313
|
+
yanked_reason: yanked_reason,
|
314
|
+
downloads: downloads,
|
315
|
+
url: url,
|
316
|
+
package_type: package_type,
|
317
|
+
language: language
|
318
|
+
)
|
319
|
+
release
|
320
|
+
end
|
321
|
+
|
322
|
+
sig do
|
323
|
+
params(
|
324
|
+
python_version: T.nilable(String),
|
325
|
+
requires_python: T.nilable(String)
|
326
|
+
)
|
327
|
+
.returns(T.nilable(Dependabot::Package::PackageLanguage))
|
328
|
+
end
|
329
|
+
def package_language(python_version:, requires_python:)
|
330
|
+
# Extract language name and version
|
331
|
+
language_name, language_version = convert_language_version(python_version)
|
332
|
+
|
333
|
+
# Extract language requirement
|
334
|
+
language_requirement = build_python_requirement(requires_python)
|
335
|
+
|
336
|
+
return nil unless language_version || language_requirement
|
337
|
+
|
338
|
+
# Return a Language object with all details
|
339
|
+
Dependabot::Package::PackageLanguage.new(
|
340
|
+
name: language_name,
|
341
|
+
version: language_version,
|
342
|
+
requirement: language_requirement
|
343
|
+
)
|
344
|
+
end
|
345
|
+
|
346
|
+
sig { params(version: T.nilable(String)).returns([String, T.nilable(Dependabot::Version)]) }
|
347
|
+
def convert_language_version(version)
|
348
|
+
return ["python", nil] if version.nil? || version == "source"
|
349
|
+
|
350
|
+
# Extract numeric parts dynamically (e.g., "cp37" -> "3.7", "py38" -> "3.8")
|
351
|
+
extracted_version = version.scan(/\d+/).join(".")
|
352
|
+
|
353
|
+
# Detect the language implementation
|
354
|
+
language_name = if version.start_with?("cp")
|
355
|
+
"cpython" # CPython implementation
|
356
|
+
elsif version.start_with?("py")
|
357
|
+
"python" # General Python compatibility
|
358
|
+
else
|
359
|
+
"unknown" # Fallback for unknown cases
|
360
|
+
end
|
361
|
+
|
362
|
+
# Ensure extracted version is valid before converting
|
363
|
+
language_version =
|
364
|
+
extracted_version.match?(/^\d+(\.\d+)*$/) ? Dependabot::Version.new(extracted_version) : nil
|
365
|
+
|
366
|
+
Dependabot.logger.warn("Skipping invalid language_version: #{version.inspect}") if language_version.nil?
|
367
|
+
|
368
|
+
[language_name, language_version]
|
369
|
+
end
|
370
|
+
|
371
|
+
sig { returns(T::Array[String]) }
|
372
|
+
def registry_urls
|
373
|
+
@registry_urls ||=
|
374
|
+
Package::PackageRegistryFinder.new(
|
375
|
+
dependency_files: dependency_files,
|
376
|
+
credentials: credentials,
|
377
|
+
dependency: dependency
|
378
|
+
).registry_urls
|
379
|
+
end
|
380
|
+
|
381
|
+
sig { returns(String) }
|
382
|
+
def normalised_name
|
383
|
+
NameNormaliser.normalise(dependency.name)
|
384
|
+
end
|
385
|
+
|
386
|
+
sig { params(json_url: String).returns(Excon::Response) }
|
387
|
+
def registry_json_response_for_dependency(json_url)
|
388
|
+
Dependabot::RegistryClient.get(
|
389
|
+
url: "#{json_url.chomp('/')}/#{@dependency.name}/json",
|
390
|
+
headers: { "Accept" => APPLICATION_JSON }
|
391
|
+
)
|
392
|
+
end
|
393
|
+
|
394
|
+
sig { params(index_url: String).returns(Excon::Response) }
|
395
|
+
def registry_response_for_dependency(index_url)
|
396
|
+
Dependabot::RegistryClient.get(
|
397
|
+
url: index_url + normalised_name + "/",
|
398
|
+
headers: { "Accept" => APPLICATION_TEXT }
|
399
|
+
)
|
400
|
+
end
|
401
|
+
|
402
|
+
sig { params(index_url: String).returns(Excon::Response) }
|
403
|
+
def registry_index_response(index_url)
|
404
|
+
Dependabot::RegistryClient.get(
|
405
|
+
url: index_url,
|
406
|
+
headers: { "Accept" => APPLICATION_TEXT }
|
407
|
+
)
|
408
|
+
end
|
409
|
+
|
410
|
+
sig { params(filename: String).returns(T.nilable(String)) }
|
411
|
+
def get_version_from_filename(filename)
|
412
|
+
filename
|
413
|
+
.gsub(/#{name_regex}-/i, "")
|
414
|
+
.split(/-|\.tar\.|\.zip|\.whl/)
|
415
|
+
.first
|
416
|
+
end
|
417
|
+
|
418
|
+
sig do
|
419
|
+
params(req_string: T.nilable(String))
|
420
|
+
.returns(T.nilable(Dependabot::Requirement))
|
421
|
+
end
|
422
|
+
def build_python_requirement(req_string)
|
423
|
+
return nil unless req_string
|
424
|
+
|
425
|
+
requirement_class.new(CGI.unescapeHTML(req_string))
|
426
|
+
rescue Gem::Requirement::BadRequirementError
|
427
|
+
nil
|
428
|
+
end
|
429
|
+
|
430
|
+
sig { params(link: String).returns(T.nilable(String)) }
|
431
|
+
def requires_python_from_link(link)
|
432
|
+
raw_value = Nokogiri::XML(link)
|
433
|
+
.at_css("a")
|
434
|
+
&.attribute("data-requires-python")
|
435
|
+
&.content
|
436
|
+
|
437
|
+
return nil unless raw_value
|
438
|
+
|
439
|
+
CGI.unescapeHTML(raw_value) # Decodes HTML entities like >=3 → >=3
|
440
|
+
end
|
441
|
+
|
442
|
+
sig { returns(T.class_of(Dependabot::Version)) }
|
443
|
+
def version_class
|
444
|
+
dependency.version_class
|
445
|
+
end
|
446
|
+
|
447
|
+
sig { returns(T.class_of(Dependabot::Requirement)) }
|
448
|
+
def requirement_class
|
449
|
+
dependency.requirement_class
|
450
|
+
end
|
451
|
+
|
452
|
+
sig { params(index_url: String).returns(T::Hash[String, String]) }
|
453
|
+
def auth_headers_for(index_url)
|
454
|
+
credential = @credentials.find { |cred| cred["index-url"] == index_url }
|
455
|
+
return {} unless credential
|
456
|
+
|
457
|
+
{ "Authorization" => "Basic #{Base64.strict_encode64(
|
458
|
+
"#{credential[CREDENTIALS_USERNAME]}:#{credential[CREDENTIALS_PASSWORD]}"
|
459
|
+
)}" }
|
460
|
+
end
|
461
|
+
|
462
|
+
sig { returns(Regexp) }
|
463
|
+
def name_regex
|
464
|
+
parts = normalised_name.split(/[\s_.-]/).map { |n| Regexp.quote(n) }
|
465
|
+
/#{parts.join("[\s_.-]")}/i
|
466
|
+
end
|
467
|
+
|
468
|
+
sig { params(index_url: T.nilable(String)).returns(T::Boolean) }
|
469
|
+
def validate_index(index_url)
|
470
|
+
return false unless index_url
|
471
|
+
|
472
|
+
return true if index_url.match?(URI::DEFAULT_PARSER.regexp[:ABS_URI])
|
473
|
+
|
474
|
+
raise Dependabot::DependencyFileNotResolvable,
|
475
|
+
"Invalid URL: #{sanitized_url(index_url)}"
|
476
|
+
end
|
477
|
+
|
478
|
+
sig { params(index_url: String).returns(String) }
|
479
|
+
def sanitized_url(index_url)
|
480
|
+
index_url.sub(%r{//([^/@]+)@}, "//redacted@")
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
end
|
485
|
+
end
|
data/lib/dependabot/python/{update_checker/index_finder.rb → package/package_registry_finder.rb}
RENAMED
@@ -7,8 +7,8 @@ require "dependabot/errors"
|
|
7
7
|
|
8
8
|
module Dependabot
|
9
9
|
module Python
|
10
|
-
|
11
|
-
class
|
10
|
+
module Package
|
11
|
+
class PackageRegistryFinder
|
12
12
|
PYPI_BASE_URL = "https://pypi.org/simple/"
|
13
13
|
ENVIRONMENT_VARIABLE_REGEX = /\$\{.+\}/
|
14
14
|
|
@@ -18,7 +18,7 @@ module Dependabot
|
|
18
18
|
@dependency = dependency
|
19
19
|
end
|
20
20
|
|
21
|
-
def
|
21
|
+
def registry_urls
|
22
22
|
extra_index_urls =
|
23
23
|
config_variable_index_urls[:extra] +
|
24
24
|
pipfile_index_urls[:extra] +
|
@@ -158,7 +158,7 @@ module Dependabot
|
|
158
158
|
end
|
159
159
|
|
160
160
|
def clean_check_and_remove_environment_variables(url)
|
161
|
-
url = url.strip.
|
161
|
+
url = url.strip.sub(%r{/+$}, "") + "/"
|
162
162
|
|
163
163
|
return authed_base_url(url) unless url.match?(ENVIRONMENT_VARIABLE_REGEX)
|
164
164
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strong
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require "cgi"
|
@@ -12,284 +12,25 @@ require "dependabot/update_checkers/version_filters"
|
|
12
12
|
require "dependabot/registry_client"
|
13
13
|
require "dependabot/python/authed_url_builder"
|
14
14
|
require "dependabot/python/name_normaliser"
|
15
|
+
require "dependabot/python/package/package_registry_finder"
|
16
|
+
require "dependabot/python/package/package_details_fetcher"
|
17
|
+
require "dependabot/package/package_latest_version_finder"
|
15
18
|
|
16
19
|
module Dependabot
|
17
20
|
module Python
|
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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
202
|
-
end
|
203
|
-
|
204
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
205
|
-
def version_details_from_link(link)
|
206
|
-
doc = Nokogiri::XML(link)
|
207
|
-
filename = doc.at_css("a")&.content
|
208
|
-
url = doc.at_css("a")&.attributes&.fetch("href", nil)&.value
|
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
|
240
|
-
end
|
241
|
-
|
242
|
-
def index_urls
|
243
|
-
@index_urls ||=
|
244
|
-
IndexFinder.new(
|
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}"
|
26
|
+
override.returns(T.nilable(Dependabot::Package::PackageDetails))
|
27
|
+
end
|
28
|
+
def package_details
|
29
|
+
@package_details ||= Package::PackageDetailsFetcher.new(
|
30
|
+
dependency: dependency,
|
31
|
+
dependency_files: dependency_files,
|
32
|
+
credentials: credentials
|
33
|
+
).fetch
|
293
34
|
end
|
294
35
|
end
|
295
36
|
end
|
@@ -22,17 +22,17 @@ module Dependabot
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def latest_resolvable_version
|
25
|
-
latest_version_finder.latest_version(
|
25
|
+
latest_version_finder.latest_version(language_version: language_version_manager.python_version)
|
26
26
|
end
|
27
27
|
|
28
28
|
def latest_resolvable_version_with_no_unlock
|
29
29
|
latest_version_finder
|
30
|
-
.latest_version_with_no_unlock(
|
30
|
+
.latest_version_with_no_unlock(language_version: language_version_manager.python_version)
|
31
31
|
end
|
32
32
|
|
33
33
|
def lowest_resolvable_security_fix_version
|
34
34
|
latest_version_finder
|
35
|
-
.lowest_security_fix_version(
|
35
|
+
.lowest_security_fix_version(language_version: language_version_manager.python_version)
|
36
36
|
end
|
37
37
|
|
38
38
|
private
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dependabot-python
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.299.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dependabot
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-02-
|
11
|
+
date: 2025-02-27 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.
|
19
|
+
version: 0.299.0
|
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.
|
26
|
+
version: 0.299.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: debug
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -271,13 +271,14 @@ files:
|
|
271
271
|
- lib/dependabot/python/metadata_finder.rb
|
272
272
|
- lib/dependabot/python/name_normaliser.rb
|
273
273
|
- lib/dependabot/python/native_helpers.rb
|
274
|
+
- lib/dependabot/python/package/package_details_fetcher.rb
|
275
|
+
- lib/dependabot/python/package/package_registry_finder.rb
|
274
276
|
- lib/dependabot/python/package_manager.rb
|
275
277
|
- lib/dependabot/python/pip_compile_file_matcher.rb
|
276
278
|
- lib/dependabot/python/pipenv_runner.rb
|
277
279
|
- lib/dependabot/python/requirement.rb
|
278
280
|
- lib/dependabot/python/requirement_parser.rb
|
279
281
|
- lib/dependabot/python/update_checker.rb
|
280
|
-
- lib/dependabot/python/update_checker/index_finder.rb
|
281
282
|
- lib/dependabot/python/update_checker/latest_version_finder.rb
|
282
283
|
- lib/dependabot/python/update_checker/pip_compile_version_resolver.rb
|
283
284
|
- lib/dependabot/python/update_checker/pip_version_resolver.rb
|
@@ -290,7 +291,7 @@ licenses:
|
|
290
291
|
- MIT
|
291
292
|
metadata:
|
292
293
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
293
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
294
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.299.0
|
294
295
|
post_install_message:
|
295
296
|
rdoc_options: []
|
296
297
|
require_paths:
|