gemstar 1.2 → 1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,182 @@
1
+ require "json"
2
+ require "open-uri"
3
+ require "time"
4
+ require "uri"
5
+
6
+ module Gemstar
7
+ class PyPIMetadata
8
+ def initialize(package_name)
9
+ @package_name = package_name
10
+ end
11
+
12
+ attr_reader :package_name
13
+
14
+ alias_method :gem_name, :package_name
15
+
16
+ def cache_key
17
+ "pypi-#{package_name}"
18
+ end
19
+
20
+ def meta(cache_only: false, force_refresh: false)
21
+ return @meta if !cache_only && defined?(@meta)
22
+
23
+ json = if cache_only
24
+ Cache.peek(cache_key)
25
+ else
26
+ url = "https://pypi.org/pypi/#{URI.encode_www_form_component(package_name)}/json"
27
+ Cache.fetch(cache_key, force: force_refresh) do
28
+ URI.parse(url).open(read_timeout: 8).read
29
+ end
30
+ end
31
+
32
+ parsed = begin
33
+ JSON.parse(json) if json
34
+ rescue
35
+ nil
36
+ end
37
+
38
+ normalized = normalize_meta(parsed)
39
+ @meta = normalized unless cache_only
40
+ normalized
41
+ end
42
+
43
+ def repo_uri(cache_only: false, force_refresh: false)
44
+ resolved_meta = meta(cache_only: cache_only, force_refresh: force_refresh)
45
+ return nil unless resolved_meta
46
+
47
+ return @repo_uri if !cache_only && defined?(@repo_uri)
48
+
49
+ uri = resolved_meta["source_code_uri"]
50
+ uri ||= resolved_meta["homepage_uri"] if resolved_meta["homepage_uri"].to_s.include?("github.com")
51
+ uri = normalize_repo_uri(uri)
52
+
53
+ @repo_uri = uri unless cache_only
54
+ uri
55
+ end
56
+
57
+ def changelog_sections(versions: nil, cache_only: false, force_refresh: false, use_github_cli: false)
58
+ requested_versions = Array(versions).compact
59
+ changelog = Gemstar::ChangeLog.new(self)
60
+ if requested_versions.empty?
61
+ changelog.sections(cache_only: cache_only, force_refresh: force_refresh)
62
+ else
63
+ changelog.sections_for_versions(requested_versions, cache_only: cache_only, force_refresh: force_refresh, use_github_cli: use_github_cli)
64
+ end
65
+ end
66
+
67
+ def registry_release_dates(cache_only: false, force_refresh: false)
68
+ parsed = raw_meta(cache_only: cache_only, force_refresh: force_refresh)
69
+ Array(parsed&.dig("releases")).each_with_object({}) do |(version, files), dates|
70
+ uploaded_at = Array(files).filter_map { |file| file["upload_time_iso_8601"] || file["upload_time"] }.min
71
+ next if version.to_s.empty? || uploaded_at.to_s.empty?
72
+
73
+ dates[version] = Time.parse(uploaded_at).utc.strftime("%b %-d, %Y")
74
+ end
75
+ rescue JSON::ParserError, ArgumentError
76
+ {}
77
+ end
78
+
79
+ def warm_cache(versions: nil)
80
+ meta
81
+ repo_uri
82
+ changelog_sections(versions: versions)
83
+ end
84
+
85
+ def discover_github_tag_sections?
86
+ true
87
+ end
88
+
89
+ def github_tag_candidates(version)
90
+ raw = version.to_s
91
+ candidates = [raw, (raw.start_with?("v") ? raw : "v#{raw}")]
92
+ candidates << "release_#{raw}"
93
+ candidates << "release_v#{raw}" unless raw.start_with?("v")
94
+
95
+ if raw.match?(/\A\d+\.\d\z/)
96
+ padded_minor = "#{raw}0"
97
+ candidates << padded_minor
98
+ candidates << "v#{padded_minor}"
99
+ candidates << "release_#{padded_minor}"
100
+ candidates << "release_v#{padded_minor}"
101
+ end
102
+
103
+ if raw.match?(/\A\d{4}\.\d{1,2}\.\d{1,2}\z/)
104
+ year, month, day = raw.split(".")
105
+ dotted_date = [year, month.rjust(2, "0"), day.rjust(2, "0")].join(".")
106
+ candidates << dotted_date
107
+ candidates << "v#{dotted_date}"
108
+ candidates << "release_#{dotted_date}"
109
+ candidates << "release_v#{dotted_date}"
110
+ end
111
+
112
+ candidates.uniq
113
+ end
114
+
115
+ def github_tag_matches?(_tag_name)
116
+ true
117
+ end
118
+
119
+ private
120
+
121
+ def raw_meta(cache_only: false, force_refresh: false)
122
+ json = if cache_only
123
+ Cache.peek(cache_key)
124
+ else
125
+ url = "https://pypi.org/pypi/#{URI.encode_www_form_component(package_name)}/json"
126
+ Cache.fetch(cache_key, force: force_refresh) do
127
+ URI.parse(url).open(read_timeout: 8).read
128
+ end
129
+ end
130
+
131
+ JSON.parse(json) if json
132
+ end
133
+
134
+ def normalize_meta(parsed)
135
+ return nil unless parsed.is_a?(Hash)
136
+
137
+ info = parsed["info"] || {}
138
+ project_urls = info["project_urls"] || {}
139
+ source_code_uri = project_url(project_urls, "Source", "Source Code", "Code", "Repository", "GitHub")
140
+ homepage_uri = info["home_page"].to_s.empty? ? nil : info["home_page"]
141
+ homepage_uri ||= project_url(project_urls, "Homepage", "Home", "homepage")
142
+ changelog_uri = project_url(project_urls, "Changelog", "Change Log", "Changes", "Release Notes", "Release notes", "History")
143
+
144
+ {
145
+ "name" => info["name"] || package_name,
146
+ "version" => info["version"],
147
+ "info" => info["summary"] || info["description"],
148
+ "homepage_uri" => homepage_uri,
149
+ "source_code_uri" => source_code_uri,
150
+ "project_uri" => info["package_url"] || "https://pypi.org/project/#{package_name}/",
151
+ "documentation_uri" => project_url(project_urls, "Documentation", "Docs", "documentation"),
152
+ "changelog_uri" => changelog_uri
153
+ }
154
+ end
155
+
156
+ def project_url(project_urls, *names)
157
+ normalized_urls = project_urls.transform_keys { |key| key.to_s.downcase.gsub(/[\s_-]+/, "") }
158
+ names.each do |name|
159
+ value = normalized_urls[name.to_s.downcase.gsub(/[\s_-]+/, "")]
160
+ return value unless value.to_s.empty?
161
+ end
162
+
163
+ nil
164
+ end
165
+
166
+ def normalize_repo_uri(uri)
167
+ value = uri.to_s
168
+ return "" if value.empty?
169
+
170
+ value = value.sub(/\Agit\+/, "")
171
+ value = value.sub(/\Agit:\/\//, "https://")
172
+ value = value.sub(/\Ahttp:\/\//, "https://")
173
+ value = value.gsub(/\.git\z/, "")
174
+
175
+ if value.include?("github.com")
176
+ value = value[%r{\Ahttps?://github\.com/[^/]+/[^/]+}] || value
177
+ end
178
+
179
+ value
180
+ end
181
+ end
182
+ end
@@ -97,6 +97,12 @@ module Gemstar
97
97
  end
98
98
  end
99
99
 
100
+ def correct_changelog_version(version, heading:)
101
+ overrides = package_metadata.dig("changelog", "version_overrides") || {}
102
+ release_date = heading.to_s[/\b\d{4}-\d{2}-\d{2}\b/]
103
+ overrides.fetch([version, release_date].compact.join("@"), version)
104
+ end
105
+
100
106
  def registry_release_dates(cache_only: false, force_refresh: false)
101
107
  cache_key = "rubygems-versions-#{gem_name}"
102
108
  json = if cache_only
@@ -145,7 +151,11 @@ module Gemstar
145
151
  override_branches = Array(override["branches"]).compact
146
152
  override_branches = [""] if override_branches.empty? && override["raw_base"]
147
153
  return {
148
- base: expand_metadata_template(override["raw_base"] || github_raw_base(repo_uri)),
154
+ base: expand_metadata_template(
155
+ override["raw_base"] || github_raw_base(repo_uri),
156
+ cache_only: cache_only,
157
+ force_refresh: force_refresh
158
+ ),
149
159
  paths: override_paths.empty? ? Gemstar::ChangeLog::DEFAULT_CHANGELOG_PATHS : override_paths,
150
160
  branches: override_branches.empty? ? RemoteRepository.new(github_raw_base(repo_uri)).find_main_branch(cache_only: cache_only, force_refresh: force_refresh) : override_branches
151
161
  }
@@ -171,8 +181,21 @@ module Gemstar
171
181
  repo_uri.sub("https://github.com", "https://raw.githubusercontent.com").chomp("/")
172
182
  end
173
183
 
174
- def expand_metadata_template(value)
175
- value.to_s.gsub("{gem_name}", gem_name)
184
+ def expand_metadata_template(value, cache_only: false, force_refresh: false)
185
+ version = metadata_template_version(cache_only: cache_only, force_refresh: force_refresh)
186
+ value.to_s.gsub("{gem_name}", gem_name).gsub("{version}", version)
187
+ end
188
+
189
+ def metadata_template_version(cache_only: false, force_refresh: false)
190
+ version = meta(cache_only: cache_only, force_refresh: force_refresh)&.dig("version").to_s
191
+ return version unless version.empty?
192
+
193
+ override_versions = package_metadata.dig("changelog", "version_overrides")
194
+ Array(override_versions&.values).compact.map(&:to_s).reject(&:empty?).max_by do |override_version|
195
+ Gem::Version.new(override_version)
196
+ rescue ArgumentError
197
+ Gem::Version.new("0")
198
+ end.to_s
176
199
  end
177
200
 
178
201
  def format_registry_release_date(datetime)
@@ -0,0 +1,114 @@
1
+ module Gemstar
2
+ class UvLockFile
3
+ attr_reader :specs
4
+ attr_reader :spec_sources
5
+
6
+ def initialize(path: nil, content: nil)
7
+ parsed = parse_content(content || File.read(path))
8
+ @specs = parsed[:specs]
9
+ @spec_sources = parsed[:spec_sources]
10
+ end
11
+
12
+ def source_for(name)
13
+ spec_sources[name]
14
+ end
15
+
16
+ private
17
+
18
+ def parse_content(content)
19
+ specs = {}
20
+ spec_sources = {}
21
+
22
+ package_blocks(content).each do |block|
23
+ name = scalar_value(block, "name")
24
+ version = scalar_value(block, "version")
25
+ source = source_for_block(block, name, version)
26
+ next if name.to_s.empty? || version.to_s.empty?
27
+ next if source[:type] == :virtual
28
+
29
+ specs[name] = version
30
+ spec_sources[name] = source
31
+ end
32
+
33
+ {
34
+ specs: specs,
35
+ spec_sources: spec_sources
36
+ }
37
+ end
38
+
39
+ def package_blocks(content)
40
+ content.to_s.split(/^\[\[package\]\]\s*$/).drop(1)
41
+ end
42
+
43
+ def scalar_value(block, key)
44
+ block[/^#{Regexp.escape(key)}\s*=\s*"([^"]*)"/, 1]
45
+ end
46
+
47
+ def source_for_block(block, name, version)
48
+ source = inline_table(block[/^source\s*=\s*\{([^}]*)\}/, 1])
49
+ package_url = "https://pypi.org/project/#{name}/"
50
+ registry_url = source["registry"]
51
+
52
+ if source.key?("virtual")
53
+ return {
54
+ type: :virtual,
55
+ path: source["virtual"]
56
+ }.compact
57
+ end
58
+
59
+ if source.key?("git")
60
+ return {
61
+ type: :git,
62
+ remote: source["git"],
63
+ revision: source["rev"] || source["commit"],
64
+ branch: source["branch"],
65
+ tag: source["tag"],
66
+ package_name: name,
67
+ package_version: version,
68
+ registry_url: package_url
69
+ }.compact
70
+ end
71
+
72
+ if source.key?("path") || source.key?("editable") || source.key?("directory")
73
+ return {
74
+ type: :path,
75
+ path: source["path"] || source["editable"] || source["directory"],
76
+ package_name: name,
77
+ package_version: version,
78
+ registry_url: package_url
79
+ }.compact
80
+ end
81
+
82
+ {
83
+ type: :pypi,
84
+ remote: registry_url,
85
+ distribution_url: distribution_url(block),
86
+ package_name: name,
87
+ package_version: version,
88
+ registry_url: package_url
89
+ }.compact
90
+ end
91
+
92
+ def distribution_url(block)
93
+ sdist = inline_table(block[/^sdist\s*=\s*\{([^}]*)\}/, 1])
94
+ return sdist["url"] unless sdist["url"].to_s.empty?
95
+
96
+ wheel = inline_table(block[/^\s*\{\s*url\s*=\s*"([^"]+)"/, 0])
97
+ wheel["url"]
98
+ end
99
+
100
+ def inline_table(content)
101
+ content.to_s.scan(/([\w-]+)\s*=\s*("[^"]*"|true|false|[^,\s}]+)/).each_with_object({}) do |(key, raw_value), values|
102
+ values[key] = parse_inline_value(raw_value)
103
+ end
104
+ end
105
+
106
+ def parse_inline_value(raw_value)
107
+ value = raw_value.to_s.strip
108
+ return true if value == "true"
109
+ return false if value == "false"
110
+
111
+ value.sub(/\A"/, "").sub(/"\z/, "")
112
+ end
113
+ end
114
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gemstar # :nodoc:
4
- VERSION = "1.2"
4
+ VERSION = "1.3"
5
5
  def self.debug?
6
6
  return @debug if defined?(@debug)
7
7
  @debug = ENV["GEMSTAR_DEBUG"] == "true"