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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d7cbbb68900510599e8ae5879bc9dc36984b212c1e0f1bf23750a374da0665a
4
- data.tar.gz: 6afc9a2f6f5d4696ff8ef47669fa104fc1e580ad0719ee561ac59821715a579b
3
+ metadata.gz: d429e2e29dd9b40c0213abc4255d683c03153ebd78508ef1d01c8a67c0779606
4
+ data.tar.gz: 87e287bf9f902c64ab1d1c99c33262534b997e14b048df39215b6d9e30226e36
5
5
  SHA512:
6
- metadata.gz: f9848c2b7376c9fa036812f63f2f44ee598e0f8d7746cb931c7f91c2a7e387616e207286249b896473e82df893c57ce34fd511983f7c8b5b1f9ff7ab1a9efeba
7
- data.tar.gz: c5e08128f5f9eec6192e17c40cc8dc9f59e4c1ee5d3f8e7dbabb5cbba575500983a572d5eb3876833aa85b69848ae34d2ddfc1302aa86d89de2c5cd516aaab41
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 &gt;=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
@@ -7,8 +7,8 @@ require "dependabot/errors"
7
7
 
8
8
  module Dependabot
9
9
  module Python
10
- class UpdateChecker
11
- class IndexFinder
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 index_urls
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.gsub(%r{/*$}, "") + "/"
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: true
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
- params(versions_array: T::Array[T.untyped], python_version: T.nilable(T.any(String, Version)))
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
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(python_version: language_version_manager.python_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(python_version: language_version_manager.python_version)
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(python_version: language_version_manager.python_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.298.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-20 00:00:00.000000000 Z
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.298.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.298.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.298.0
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: