fix(livecheck/pypi): update to use json endpoint to query version

Signed-off-by: Rui Chen <rui@chenrui.dev>
This commit is contained in:
Rui Chen 2024-12-07 01:15:40 -05:00
parent 405cedad23
commit d49e01b82b
No known key found for this signature in database
GPG Key ID: 6577287BDCA70840
2 changed files with 40 additions and 32 deletions

View File

@ -1,19 +1,21 @@
# typed: strict # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "json"
require "utils/curl"
module Homebrew module Homebrew
module Livecheck module Livecheck
module Strategy module Strategy
# The {Pypi} strategy identifies versions of software at pypi.org by # The {Pypi} strategy identifies versions of software at pypi.org by
# checking project pages for archive files. # using the JSON API endpoint.
# #
# PyPI URLs have a standard format but the hexadecimal text between # PyPI URLs have a standard format:
# `/packages/` and the filename varies:
# #
# * `https://files.pythonhosted.org/packages/<hex>/<hex>/<long_hex>/example-1.2.3.tar.gz` # * `https://files.pythonhosted.org/packages/<hex>/<hex>/<long_hex>/example-1.2.3.tar.gz`
# #
# As such, the default regex only targets the filename at the end of the # This method uses the `info.version` field in the JSON response to
# URL. # determine the latest stable version.
# #
# @api public # @api public
class Pypi class Pypi
@ -44,10 +46,8 @@ module Homebrew
URL_MATCH_REGEX.match?(url) URL_MATCH_REGEX.match?(url)
end end
# Extracts information from a provided URL and uses it to generate # Extracts the package name from the provided URL and generates the
# various input values used by the strategy to check for new versions. # PyPI JSON API endpoint.
# Some of these values act as defaults and can be overridden in a
# `livecheck` block.
# #
# @param url [String] the URL used to generate values # @param url [String] the URL used to generate values
# @return [Hash] # @return [Hash]
@ -58,40 +58,48 @@ module Homebrew
match = File.basename(url).match(FILENAME_REGEX) match = File.basename(url).match(FILENAME_REGEX)
return values if match.blank? return values if match.blank?
# It's not technically necessary to have the `#files` fragment at the package_name = T.must(match[:package_name]).gsub(/[_-]/, "-")
# end of the URL but it makes the debug output a bit more useful. values[:url] = "https://pypi.org/project/#{package_name}/#files"
values[:url] = "https://pypi.org/project/#{T.must(match[:package_name]).gsub(/%20|_/, "-")}/#files" values[:regex] = %r{href=.*?/packages.*?/#{package_name}[._-]v?(\d+(?:\.\d+)*(?:[._-]post\d+)?)\.t}i
# Use `\.t` instead of specific tarball extensions (e.g. .tar.gz)
suffix = T.must(match[:suffix]).sub(Strategy::TARBALL_EXTENSION_REGEX, ".t")
regex_suffix = Regexp.escape(suffix).gsub("\\-", "-")
# Example regex: `%r{href=.*?/packages.*?/example[._-]v?(\d+(?:\.\d+)*(?:[._-]post\d+)?)\.t}i`
regex_name = Regexp.escape(T.must(match[:package_name])).gsub(/\\[_-]/, "[_-]")
values[:regex] =
%r{href=.*?/packages.*?/#{regex_name}[._-]v?(\d+(?:\.\d+)*(?:[._-]post\d+)?)#{regex_suffix}}i
values values
end end
# Generates a URL and regex (if one isn't provided) and passes them # Fetches the latest version of the package from the PyPI JSON API.
# to {PageMatch.find_versions} to identify versions in the content.
# #
# @param url [String] the URL of the content to check # @param url [String] the URL of the content to check
# @param regex [Regexp] a regex used for matching versions in content # @param regex [Regexp] a regex used for matching versions in content (optional)
# @return [Hash] # @return [Hash]
sig { sig {
params( params(
url: String, url: String,
regex: T.nilable(Regexp), regex: T.nilable(Regexp),
unused: T.untyped, _unused: T.untyped,
block: T.nilable(Proc), _block: T.nilable(Proc),
).returns(T::Hash[Symbol, T.untyped]) ).returns(T::Hash[Symbol, T.untyped])
} }
def self.find_versions(url:, regex: nil, **unused, &block) def self.find_versions(url:, regex: nil, **_unused, &_block)
generated = generate_input_values(url) match_data = { matches: {}, regex:, url: }
PageMatch.find_versions(url: generated[:url], regex: regex || generated[:regex], **unused, &block) generated = generate_input_values(url)
return match_data if generated.blank?
match_data[:url] = generated[:url]
# Parse JSON and get the latest version
begin
response = Utils::Curl.curl_output(generated[:url])
data = JSON.parse(response.stdout, symbolize_names: true)
latest_version = data.dig(:info, :version)
rescue => e
puts "Error fetching version from PyPI: #{e.message}"
return {}
end
# Return the version if found
return {} if latest_version.blank?
{ matches: { latest_version => Version.new(latest_version) } }
end end
end end
end end

View File

@ -11,7 +11,7 @@ RSpec.describe Homebrew::Livecheck::Strategy::Pypi do
let(:generated) do let(:generated) do
{ {
url: "https://pypi.org/project/example-package/#files", url: "https://pypi.org/project/example-package/#files",
regex: %r{href=.*?/packages.*?/example[_-]package[._-]v?(\d+(?:\.\d+)*(?:[._-]post\d+)?)\.t}i, regex: %r{href=.*?/packages.*?/example-package[._-]v?(\d+(?:\.\d+)*(?:[._-]post\d+)?)\.t}i,
} }
end end