dependabot-python 0.79.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/helpers/build +17 -0
- data/helpers/lib/__init__.py +0 -0
- data/helpers/lib/hasher.py +23 -0
- data/helpers/lib/parser.py +130 -0
- data/helpers/requirements.txt +9 -0
- data/helpers/run.py +18 -0
- data/lib/dependabot/python.rb +11 -0
- data/lib/dependabot/python/file_fetcher.rb +307 -0
- data/lib/dependabot/python/file_parser.rb +221 -0
- data/lib/dependabot/python/file_parser/pipfile_files_parser.rb +150 -0
- data/lib/dependabot/python/file_parser/poetry_files_parser.rb +139 -0
- data/lib/dependabot/python/file_parser/setup_file_parser.rb +158 -0
- data/lib/dependabot/python/file_updater.rb +149 -0
- data/lib/dependabot/python/file_updater/pip_compile_file_updater.rb +361 -0
- data/lib/dependabot/python/file_updater/pipfile_file_updater.rb +391 -0
- data/lib/dependabot/python/file_updater/pipfile_preparer.rb +123 -0
- data/lib/dependabot/python/file_updater/poetry_file_updater.rb +282 -0
- data/lib/dependabot/python/file_updater/pyproject_preparer.rb +103 -0
- data/lib/dependabot/python/file_updater/requirement_file_updater.rb +160 -0
- data/lib/dependabot/python/file_updater/requirement_replacer.rb +93 -0
- data/lib/dependabot/python/file_updater/setup_file_sanitizer.rb +89 -0
- data/lib/dependabot/python/metadata_finder.rb +122 -0
- data/lib/dependabot/python/native_helpers.rb +17 -0
- data/lib/dependabot/python/python_versions.rb +25 -0
- data/lib/dependabot/python/requirement.rb +129 -0
- data/lib/dependabot/python/requirement_parser.rb +38 -0
- data/lib/dependabot/python/update_checker.rb +229 -0
- data/lib/dependabot/python/update_checker/latest_version_finder.rb +250 -0
- data/lib/dependabot/python/update_checker/pip_compile_version_resolver.rb +379 -0
- data/lib/dependabot/python/update_checker/pipfile_version_resolver.rb +558 -0
- data/lib/dependabot/python/update_checker/poetry_version_resolver.rb +298 -0
- data/lib/dependabot/python/update_checker/requirements_updater.rb +365 -0
- data/lib/dependabot/python/version.rb +87 -0
- metadata +203 -0
@@ -0,0 +1,221 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "toml-rb"
|
4
|
+
|
5
|
+
require "dependabot/dependency"
|
6
|
+
require "dependabot/file_parsers"
|
7
|
+
require "dependabot/file_parsers/base"
|
8
|
+
require "dependabot/file_parsers/base/dependency_set"
|
9
|
+
require "dependabot/shared_helpers"
|
10
|
+
require "dependabot/python/requirement"
|
11
|
+
require "dependabot/errors"
|
12
|
+
require "dependabot/python/native_helpers"
|
13
|
+
|
14
|
+
module Dependabot
|
15
|
+
module Python
|
16
|
+
class FileParser < Dependabot::FileParsers::Base
|
17
|
+
require_relative "file_parser/pipfile_files_parser"
|
18
|
+
require_relative "file_parser/poetry_files_parser"
|
19
|
+
require_relative "file_parser/setup_file_parser"
|
20
|
+
|
21
|
+
POETRY_DEPENDENCY_TYPES =
|
22
|
+
%w(tool.poetry.dependencies tool.poetry.dev-dependencies).freeze
|
23
|
+
DEPENDENCY_GROUP_KEYS = [
|
24
|
+
{
|
25
|
+
pipfile: "packages",
|
26
|
+
lockfile: "default"
|
27
|
+
},
|
28
|
+
{
|
29
|
+
pipfile: "dev-packages",
|
30
|
+
lockfile: "develop"
|
31
|
+
}
|
32
|
+
].freeze
|
33
|
+
REQUIREMENT_FILE_EVALUATION_ERRORS = %w(
|
34
|
+
InstallationError RequirementsFileParseError InvalidMarker
|
35
|
+
InvalidRequirement
|
36
|
+
).freeze
|
37
|
+
|
38
|
+
def parse
|
39
|
+
dependency_set = DependencySet.new
|
40
|
+
|
41
|
+
dependency_set += pipenv_dependencies if pipfile
|
42
|
+
dependency_set += poetry_dependencies if using_poetry?
|
43
|
+
dependency_set += requirement_dependencies if requirement_files.any?
|
44
|
+
dependency_set += setup_file_dependencies if setup_file
|
45
|
+
|
46
|
+
dependency_set.dependencies
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def requirement_files
|
52
|
+
dependency_files.select { |f| f.name.end_with?(".txt", ".in") }
|
53
|
+
end
|
54
|
+
|
55
|
+
def pipenv_dependencies
|
56
|
+
@pipenv_dependencies ||=
|
57
|
+
PipfileFilesParser.
|
58
|
+
new(dependency_files: dependency_files).
|
59
|
+
dependency_set
|
60
|
+
end
|
61
|
+
|
62
|
+
def poetry_dependencies
|
63
|
+
@poetry_dependencies ||=
|
64
|
+
PoetryFilesParser.
|
65
|
+
new(dependency_files: dependency_files).
|
66
|
+
dependency_set
|
67
|
+
end
|
68
|
+
|
69
|
+
def requirement_dependencies
|
70
|
+
dependencies = DependencySet.new
|
71
|
+
parsed_requirement_files.each do |dep|
|
72
|
+
# This isn't ideal, but currently the FileUpdater won't update
|
73
|
+
# deps that appear in a requirements.txt and Pipfile / Pipfile.lock
|
74
|
+
# and *aren't* a straight lockfile for the Pipfile
|
75
|
+
next if included_in_pipenv_deps?(normalised_name(dep["name"]))
|
76
|
+
|
77
|
+
# If a requirement has a `<` or `<=` marker then updating it is
|
78
|
+
# probably blocked. Ignore it.
|
79
|
+
next if dep["markers"].include?("<")
|
80
|
+
|
81
|
+
requirements =
|
82
|
+
if lockfile_for_pip_compile_file?(dep["file"]) then []
|
83
|
+
else
|
84
|
+
[{
|
85
|
+
requirement: dep["requirement"],
|
86
|
+
file: Pathname.new(dep["file"]).cleanpath.to_path,
|
87
|
+
source: nil,
|
88
|
+
groups: []
|
89
|
+
}]
|
90
|
+
end
|
91
|
+
|
92
|
+
dependencies <<
|
93
|
+
Dependency.new(
|
94
|
+
name: normalised_name(dep["name"]),
|
95
|
+
version: dep["version"]&.include?("*") ? nil : dep["version"],
|
96
|
+
requirements: requirements,
|
97
|
+
package_manager: "pip"
|
98
|
+
)
|
99
|
+
end
|
100
|
+
dependencies
|
101
|
+
end
|
102
|
+
|
103
|
+
def included_in_pipenv_deps?(dep_name)
|
104
|
+
return false unless pipfile
|
105
|
+
|
106
|
+
pipenv_dependencies.dependencies.map(&:name).include?(dep_name)
|
107
|
+
end
|
108
|
+
|
109
|
+
def setup_file_dependencies
|
110
|
+
@setup_file_dependencies ||=
|
111
|
+
SetupFileParser.
|
112
|
+
new(dependency_files: dependency_files).
|
113
|
+
dependency_set
|
114
|
+
end
|
115
|
+
|
116
|
+
def lockfile_for_pip_compile_file?(filename)
|
117
|
+
return false unless pip_compile_files.any?
|
118
|
+
return false unless filename.end_with?(".txt")
|
119
|
+
|
120
|
+
basename = filename.gsub(/\.txt$/, "")
|
121
|
+
pip_compile_files.any? { |f| f.name == basename + ".in" }
|
122
|
+
end
|
123
|
+
|
124
|
+
def parsed_requirement_files
|
125
|
+
SharedHelpers.in_a_temporary_directory do
|
126
|
+
write_temporary_dependency_files
|
127
|
+
|
128
|
+
requirements = SharedHelpers.run_helper_subprocess(
|
129
|
+
command: "pyenv exec python #{NativeHelpers.python_helper_path}",
|
130
|
+
function: "parse_requirements",
|
131
|
+
args: [Dir.pwd]
|
132
|
+
)
|
133
|
+
|
134
|
+
check_requirements(requirements)
|
135
|
+
requirements
|
136
|
+
end
|
137
|
+
rescue SharedHelpers::HelperSubprocessFailed => error
|
138
|
+
evaluation_errors = REQUIREMENT_FILE_EVALUATION_ERRORS
|
139
|
+
raise unless error.message.start_with?(*evaluation_errors)
|
140
|
+
|
141
|
+
raise Dependabot::DependencyFileNotEvaluatable, error.message
|
142
|
+
end
|
143
|
+
|
144
|
+
def check_requirements(requirements)
|
145
|
+
requirements.each do |dep|
|
146
|
+
next unless dep["requirement"]
|
147
|
+
|
148
|
+
Python::Requirement.new(dep["requirement"].split(","))
|
149
|
+
rescue Gem::Requirement::BadRequirementError => error
|
150
|
+
raise Dependabot::DependencyFileNotEvaluatable, error.message
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def write_temporary_dependency_files
|
155
|
+
dependency_files.
|
156
|
+
reject { |f| f.name == ".python-version" }.
|
157
|
+
each do |file|
|
158
|
+
path = file.name
|
159
|
+
FileUtils.mkdir_p(Pathname.new(path).dirname)
|
160
|
+
File.write(path, file.content)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# See https://www.python.org/dev/peps/pep-0503/#normalized-names
|
165
|
+
def normalised_name(name)
|
166
|
+
name.downcase.gsub(/[-_.]+/, "-")
|
167
|
+
end
|
168
|
+
|
169
|
+
def check_required_files
|
170
|
+
filenames = dependency_files.map(&:name)
|
171
|
+
return if filenames.any? { |name| name.end_with?(".txt", ".in") }
|
172
|
+
return if pipfile
|
173
|
+
return if pyproject
|
174
|
+
return if setup_file
|
175
|
+
|
176
|
+
raise "No requirements.txt or setup.py!"
|
177
|
+
end
|
178
|
+
|
179
|
+
def pipfile
|
180
|
+
@pipfile ||= get_original_file("Pipfile")
|
181
|
+
end
|
182
|
+
|
183
|
+
def pipfile_lock
|
184
|
+
@pipfile_lock ||= get_original_file("Pipfile.lock")
|
185
|
+
end
|
186
|
+
|
187
|
+
def using_poetry?
|
188
|
+
return false unless pyproject
|
189
|
+
return true if poetry_lock || pyproject_lock
|
190
|
+
|
191
|
+
!TomlRB.parse(pyproject.content).dig("tool", "poetry").nil?
|
192
|
+
rescue TomlRB::ParseError
|
193
|
+
raise Dependabot::DependencyFileNotParseable, pyproject.path
|
194
|
+
end
|
195
|
+
|
196
|
+
def pyproject
|
197
|
+
@pyproject ||= get_original_file("pyproject.toml")
|
198
|
+
end
|
199
|
+
|
200
|
+
def pyproject_lock
|
201
|
+
@pyproject_lock ||= get_original_file("pyproject.lock")
|
202
|
+
end
|
203
|
+
|
204
|
+
def poetry_lock
|
205
|
+
@poetry_lock ||= get_original_file("poetry.lock")
|
206
|
+
end
|
207
|
+
|
208
|
+
def setup_file
|
209
|
+
@setup_file ||= get_original_file("setup.py")
|
210
|
+
end
|
211
|
+
|
212
|
+
def pip_compile_files
|
213
|
+
@pip_compile_files ||=
|
214
|
+
dependency_files.select { |f| f.name.end_with?(".in") }
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
Dependabot::FileParsers.
|
221
|
+
register("pip", Dependabot::Python::FileParser)
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "toml-rb"
|
4
|
+
|
5
|
+
require "dependabot/dependency"
|
6
|
+
require "dependabot/file_parsers/base/dependency_set"
|
7
|
+
require "dependabot/python/file_parser"
|
8
|
+
require "dependabot/errors"
|
9
|
+
|
10
|
+
module Dependabot
|
11
|
+
module Python
|
12
|
+
class FileParser
|
13
|
+
class PipfileFilesParser
|
14
|
+
DEPENDENCY_GROUP_KEYS = [
|
15
|
+
{
|
16
|
+
pipfile: "packages",
|
17
|
+
lockfile: "default"
|
18
|
+
},
|
19
|
+
{
|
20
|
+
pipfile: "dev-packages",
|
21
|
+
lockfile: "develop"
|
22
|
+
}
|
23
|
+
].freeze
|
24
|
+
|
25
|
+
def initialize(dependency_files:)
|
26
|
+
@dependency_files = dependency_files
|
27
|
+
end
|
28
|
+
|
29
|
+
def dependency_set
|
30
|
+
dependency_set = Dependabot::FileParsers::Base::DependencySet.new
|
31
|
+
|
32
|
+
dependency_set += pipfile_dependencies
|
33
|
+
dependency_set += pipfile_lock_dependencies
|
34
|
+
|
35
|
+
dependency_set
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :dependency_files
|
41
|
+
|
42
|
+
def pipfile_dependencies
|
43
|
+
dependencies = Dependabot::FileParsers::Base::DependencySet.new
|
44
|
+
|
45
|
+
DEPENDENCY_GROUP_KEYS.each do |keys|
|
46
|
+
next unless parsed_pipfile[keys[:pipfile]]
|
47
|
+
|
48
|
+
parsed_pipfile[keys[:pipfile]].map do |dep_name, req|
|
49
|
+
group = keys[:lockfile]
|
50
|
+
next unless req.is_a?(String) || req["version"]
|
51
|
+
next if pipfile_lock && !dependency_version(dep_name, req, group)
|
52
|
+
|
53
|
+
dependencies <<
|
54
|
+
Dependency.new(
|
55
|
+
name: normalised_name(dep_name),
|
56
|
+
version: dependency_version(dep_name, req, group),
|
57
|
+
requirements: [{
|
58
|
+
requirement: req.is_a?(String) ? req : req["version"],
|
59
|
+
file: pipfile.name,
|
60
|
+
source: nil,
|
61
|
+
groups: [group]
|
62
|
+
}],
|
63
|
+
package_manager: "pip"
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
dependencies
|
69
|
+
end
|
70
|
+
|
71
|
+
# Create a DependencySet where each element has no requirement. Any
|
72
|
+
# requirements will be added when combining the DependencySet with
|
73
|
+
# other DependencySets.
|
74
|
+
def pipfile_lock_dependencies
|
75
|
+
dependencies = Dependabot::FileParsers::Base::DependencySet.new
|
76
|
+
return dependencies unless pipfile_lock
|
77
|
+
|
78
|
+
DEPENDENCY_GROUP_KEYS.map { |h| h.fetch(:lockfile) }.each do |key|
|
79
|
+
next unless parsed_pipfile_lock[key]
|
80
|
+
|
81
|
+
parsed_pipfile_lock[key].each do |dep_name, details|
|
82
|
+
version = case details
|
83
|
+
when String then details
|
84
|
+
when Hash then details["version"]
|
85
|
+
end
|
86
|
+
next unless version
|
87
|
+
|
88
|
+
dependencies <<
|
89
|
+
Dependency.new(
|
90
|
+
name: dep_name,
|
91
|
+
version: version&.gsub(/^===?/, ""),
|
92
|
+
requirements: [],
|
93
|
+
package_manager: "pip"
|
94
|
+
)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
dependencies
|
99
|
+
end
|
100
|
+
|
101
|
+
def dependency_version(dep_name, requirement, group)
|
102
|
+
req = version_from_hash_or_string(requirement)
|
103
|
+
|
104
|
+
if pipfile_lock
|
105
|
+
details = parsed_pipfile_lock.
|
106
|
+
dig(group, normalised_name(dep_name))
|
107
|
+
|
108
|
+
version = version_from_hash_or_string(details)
|
109
|
+
version&.gsub(/^===?/, "")
|
110
|
+
elsif req.start_with?("==") && !req.include?("*")
|
111
|
+
req.strip.gsub(/^===?/, "")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def version_from_hash_or_string(obj)
|
116
|
+
case obj
|
117
|
+
when String then obj.strip
|
118
|
+
when Hash then obj["version"]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# See https://www.python.org/dev/peps/pep-0503/#normalized-names
|
123
|
+
def normalised_name(name)
|
124
|
+
name.downcase.gsub(/[-_.]+/, "-")
|
125
|
+
end
|
126
|
+
|
127
|
+
def parsed_pipfile
|
128
|
+
@parsed_pipfile ||= TomlRB.parse(pipfile.content)
|
129
|
+
rescue TomlRB::ParseError
|
130
|
+
raise Dependabot::DependencyFileNotParseable, pipfile.path
|
131
|
+
end
|
132
|
+
|
133
|
+
def parsed_pipfile_lock
|
134
|
+
@parsed_pipfile_lock ||= JSON.parse(pipfile_lock.content)
|
135
|
+
rescue JSON::ParserError
|
136
|
+
raise Dependabot::DependencyFileNotParseable, pipfile_lock.path
|
137
|
+
end
|
138
|
+
|
139
|
+
def pipfile
|
140
|
+
@pipfile ||= dependency_files.find { |f| f.name == "Pipfile" }
|
141
|
+
end
|
142
|
+
|
143
|
+
def pipfile_lock
|
144
|
+
@pipfile_lock ||=
|
145
|
+
dependency_files.find { |f| f.name == "Pipfile.lock" }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "toml-rb"
|
4
|
+
|
5
|
+
require "dependabot/dependency"
|
6
|
+
require "dependabot/file_parsers/base/dependency_set"
|
7
|
+
require "dependabot/python/file_parser"
|
8
|
+
require "dependabot/errors"
|
9
|
+
|
10
|
+
module Dependabot
|
11
|
+
module Python
|
12
|
+
class FileParser
|
13
|
+
class PoetryFilesParser
|
14
|
+
POETRY_DEPENDENCY_TYPES = %w(dependencies dev-dependencies).freeze
|
15
|
+
|
16
|
+
def initialize(dependency_files:)
|
17
|
+
@dependency_files = dependency_files
|
18
|
+
end
|
19
|
+
|
20
|
+
def dependency_set
|
21
|
+
dependency_set = Dependabot::FileParsers::Base::DependencySet.new
|
22
|
+
|
23
|
+
dependency_set += pyproject_dependencies
|
24
|
+
dependency_set += lockfile_dependencies if lockfile
|
25
|
+
|
26
|
+
dependency_set
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :dependency_files
|
32
|
+
|
33
|
+
def pyproject_dependencies
|
34
|
+
dependencies = Dependabot::FileParsers::Base::DependencySet.new
|
35
|
+
|
36
|
+
POETRY_DEPENDENCY_TYPES.each do |type|
|
37
|
+
deps_hash = parsed_pyproject.dig("tool", "poetry", type) || {}
|
38
|
+
|
39
|
+
deps_hash.each do |name, req|
|
40
|
+
next if normalised_name(name) == "python"
|
41
|
+
next if req.is_a?(Hash) && req.key?("git")
|
42
|
+
|
43
|
+
dependencies <<
|
44
|
+
Dependency.new(
|
45
|
+
name: normalised_name(name),
|
46
|
+
version: version_from_lockfile(name),
|
47
|
+
requirements: [{
|
48
|
+
requirement: req.is_a?(String) ? req : req["version"],
|
49
|
+
file: pyproject.name,
|
50
|
+
source: nil,
|
51
|
+
groups: [type]
|
52
|
+
}],
|
53
|
+
package_manager: "pip"
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
dependencies
|
59
|
+
end
|
60
|
+
|
61
|
+
# Create a DependencySet where each element has no requirement. Any
|
62
|
+
# requirements will be added when combining the DependencySet with
|
63
|
+
# other DependencySets.
|
64
|
+
def lockfile_dependencies
|
65
|
+
dependencies = Dependabot::FileParsers::Base::DependencySet.new
|
66
|
+
|
67
|
+
parsed_lockfile.fetch("package", []).each do |details|
|
68
|
+
next if details.dig("source", "type") == "git"
|
69
|
+
|
70
|
+
dependencies <<
|
71
|
+
Dependency.new(
|
72
|
+
name: details.fetch("name"),
|
73
|
+
version: details.fetch("version"),
|
74
|
+
requirements: [],
|
75
|
+
package_manager: "pip"
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
dependencies
|
80
|
+
end
|
81
|
+
|
82
|
+
def version_from_lockfile(dep_name)
|
83
|
+
return unless parsed_lockfile
|
84
|
+
|
85
|
+
parsed_lockfile.fetch("package", []).
|
86
|
+
find { |p| p.fetch("name") == normalised_name(dep_name) }&.
|
87
|
+
fetch("verison", nil)
|
88
|
+
end
|
89
|
+
|
90
|
+
# See https://www.python.org/dev/peps/pep-0503/#normalized-names
|
91
|
+
def normalised_name(name)
|
92
|
+
name.downcase.gsub(/[-_.]+/, "-")
|
93
|
+
end
|
94
|
+
|
95
|
+
def parsed_pyproject
|
96
|
+
@parsed_pyproject ||= TomlRB.parse(pyproject.content)
|
97
|
+
rescue TomlRB::ParseError
|
98
|
+
raise Dependabot::DependencyFileNotParseable, pyproject.path
|
99
|
+
end
|
100
|
+
|
101
|
+
def parsed_pyproject_lock
|
102
|
+
@parsed_pyproject_lock ||= TomlRB.parse(pyproject_lock.content)
|
103
|
+
rescue TomlRB::ParseError
|
104
|
+
raise Dependabot::DependencyFileNotParseable, pyproject_lock.path
|
105
|
+
end
|
106
|
+
|
107
|
+
def parsed_poetry_lock
|
108
|
+
@parsed_poetry_lock ||= TomlRB.parse(poetry_lock.content)
|
109
|
+
rescue TomlRB::ParseError
|
110
|
+
raise Dependabot::DependencyFileNotParseable, poetry_lock.path
|
111
|
+
end
|
112
|
+
|
113
|
+
def pyproject
|
114
|
+
@pyproject ||=
|
115
|
+
dependency_files.find { |f| f.name == "pyproject.toml" }
|
116
|
+
end
|
117
|
+
|
118
|
+
def lockfile
|
119
|
+
poetry_lock || pyproject_lock
|
120
|
+
end
|
121
|
+
|
122
|
+
def parsed_lockfile
|
123
|
+
return parsed_poetry_lock if poetry_lock
|
124
|
+
return parsed_pyproject_lock if pyproject_lock
|
125
|
+
end
|
126
|
+
|
127
|
+
def pyproject_lock
|
128
|
+
@pyproject_lock ||=
|
129
|
+
dependency_files.find { |f| f.name == "pyproject.lock" }
|
130
|
+
end
|
131
|
+
|
132
|
+
def poetry_lock
|
133
|
+
@poetry_lock ||=
|
134
|
+
dependency_files.find { |f| f.name == "poetry.lock" }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|