-
Notifications
You must be signed in to change notification settings - Fork 202
/
pypi.rb
156 lines (133 loc) · 5.58 KB
/
pypi.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# frozen_string_literal: true
module PackageManager
class Pypi < Base
HAS_VERSIONS = true
HAS_DEPENDENCIES = true
BIBLIOTHECARY_SUPPORT = true
SECURITY_PLANNED = true
URL = "https://pypi.org/"
COLOR = "#3572A5"
ENTIRE_PACKAGE_CAN_BE_DEPRECATED = true
# Adapted from https://peps.python.org/pep-0508/#names to include extras
PEP_508_NAME_REGEX = /[A-Z0-9][A-Z0-9._-]*[A-Z0-9]|[A-Z0-9]/i
PEP_508_NAME_WITH_EXTRAS_REGEX = /(^#{PEP_508_NAME_REGEX}\s*(?:\[#{PEP_508_NAME_REGEX}(?:,\s*#{PEP_508_NAME_REGEX})*\])?)/i
# This is unused but left here for possible future use and so we can quickly reference the set of valid
# environment markers.
PEP_508_ENVIRONMENT_MARKERS = %w[
python_version python_full_version os_name
sys_platform platform_release platform_system
platform_version platform_machine platform_python_implementation
implementation_name implementation_version extra
].freeze
def self.package_link(db_project, version = nil)
# NB PEP 503: "All URLs which respond with an HTML5 page MUST end with a / and the repository SHOULD redirect the URLs without a / to add a / to the end."
"https://pypi.org/project/#{db_project.name}/"
.then { |url| version.present? ? url + "#{version}/" : url }
end
def self.check_status_url(db_project)
# NB Pypa has maintained the original JSON API behavior of allowing no trailing slash in python/pypi-infra/pull/74
"https://pypi.org/pypi/#{db_project.name}/json"
end
def self.install_instructions(db_project, version = nil)
"pip install #{db_project.name}" + (version ? "==#{version}" : "")
end
def self.formatted_name
"PyPI"
end
def self.project_names
index = Nokogiri::HTML(get_raw("https://pypi.org/simple/"))
index.css("a").map(&:text)
end
def self.recent_names
u = "https://pypi.org/rss/updates.xml"
updated = SimpleRSS.parse(get_raw(u)).items.map(&:title)
u = "https://pypi.org/rss/packages.xml"
new_packages = SimpleRSS.parse(get_raw(u)).items.map(&:title)
(updated.map { |t| t.split.first } + new_packages.map { |t| t.split.first }).uniq
end
def self.project(name)
JsonApiProject.request(project_name: name)
end
def self.deprecation_info(db_project)
json_api_project = project(db_project.name)
{
is_deprecated: json_api_project.deprecated?,
message: json_api_project.deprecation_message,
}
end
# mapping eventually receives the return value of the project method.
# This happens in PackageManager::Base.
def self.mapping(json_api_project)
MappingBuilder.build_hash(
name: json_api_project.name,
description: json_api_project.description,
homepage: json_api_project.homepage,
keywords_array: json_api_project.keywords_array,
licenses: json_api_project.licenses,
repository_url: json_api_project.preferred_repository_url
)
end
# versions eventually receives the return value of the project method.
# This happens in PackageManager::Base.
def self.versions(json_api_project, _)
VersionProcessor.new(
project_releases: json_api_project.releases,
project_name: json_api_project.name,
known_versions: known_versions(json_api_project.name)
).execute
end
def self.one_version(json_api_project, version_number)
release = json_api_project.releases.find { |r| r.version_number == version_number }
return nil unless release.present?
{
number: version_number,
published_at: release.published_at,
original_license: json_api_project.license,
}
end
def self.known_versions(name)
Project
.find_by(platform: "Pypi", name: name)
&.versions
&.map { |v| v.slice(:number, :published_at, :original_license, :status).symbolize_keys }
&.index_by { |v| v[:number] } || {}
end
# Parses out the name, version requirement, and environment markers from a PEP508 dependency specification
# https://peps.python.org/pep-0508/
def self.parse_pep_508_dep_spec(dep)
name, requirement = dep.split(PEP_508_NAME_WITH_EXTRAS_REGEX, 2).last(2)
version, environment_markers = requirement.split(";").map(&:strip)
# remove whitespace from name
# remove parentheses surrounding version requirement
[name.remove(/\s/), version&.remove(/[()]/) || "", environment_markers || ""]
end
def self.dependencies(name, version, _mapped_project = nil)
api_response = get("https://pypi.org/pypi/#{name}/#{version}/json")
deps = api_response.dig("info", "requires_dist") || []
source_info = api_response.fetch("urls", [])
Rails.logger.warn("Pypi sdist (no deps): #{name}") unless source_info.any? { |rel| rel["packagetype"] == "bdist_wheel" }
deps.flat_map do |dep|
name, version, environment_markers = parse_pep_508_dep_spec(dep)
{
project_name: name,
requirements: version.presence || "*",
kind: environment_markers.presence || "runtime",
optional: environment_markers.present?,
platform: self.name.demodulize,
}
end
end
def self.project_find_names(project_name)
[
project_name,
project_name.gsub("-", "_"),
project_name.gsub("_", "-"),
]
end
# checks to see if the package exists on PyPI and the name matches the canonical name
def self.canonical_pypi_name?(name)
json_api_project = project(name)
json_api_project.present? && json_api_project.name == name
end
end
end