miga-base 1.4.3.1 → 1.4.4.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 +4 -4
- data/lib/miga/remote_dataset/base.rb +16 -1
- data/lib/miga/remote_dataset/download.rb +147 -28
- data/lib/miga/remote_dataset.rb +11 -5
- data/lib/miga/version.rb +2 -2
- data/test/remote_dataset_test.rb +296 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b3befac28b6f332444a88067d1e04a8fe9f3d77e3800b6363be17fec49cee367
|
|
4
|
+
data.tar.gz: a68987776138c7752183a4eadc14fe42ca95eb971b721bf41830f16548bf859e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 44ed6636bbbd92e7d383e6b462e17c3dfc7da82535224b380fe8c05afd243ed9b3d86c81fc55c784973f1578377030f39288ab939cec5059100da19cdd314c8a
|
|
7
|
+
data.tar.gz: b5acca6e95002b45bdf18a8dab953ead9cb9d62c048602e26ef2c82981880d9794c96ba85b801440a0f9581e7ef3e2d43598456897bb3752f5cf024db4b08bf9
|
|
@@ -18,7 +18,7 @@ def uri_safe_join(*parts)
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
module MiGA::RemoteDataset::Base
|
|
21
|
-
@@_NCBI_DATASETS = 'https://api.ncbi.nlm.nih.gov/datasets/
|
|
21
|
+
@@_NCBI_DATASETS = 'https://api.ncbi.nlm.nih.gov/datasets/v2/'
|
|
22
22
|
@@_EUTILS = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/'
|
|
23
23
|
@@_EBI_API = 'https://www.ebi.ac.uk/Tools/'
|
|
24
24
|
@@_GTDB_API = 'https://gtdb-api.ecogenomic.org/'
|
|
@@ -173,6 +173,21 @@ module MiGA::RemoteDataset::Base
|
|
|
173
173
|
end
|
|
174
174
|
end
|
|
175
175
|
},
|
|
176
|
+
ncbi_datasets_report: {
|
|
177
|
+
dbs: { genome: { stage: :metadata, format: :json } },
|
|
178
|
+
uri: lambda do |opts|
|
|
179
|
+
uri_safe_join(
|
|
180
|
+
@@_NCBI_DATASETS, opts[:db], :accession, opts[:ids], :dataset_report
|
|
181
|
+
)
|
|
182
|
+
end,
|
|
183
|
+
method: :get,
|
|
184
|
+
headers: lambda do |opts|
|
|
185
|
+
{}.tap do |h|
|
|
186
|
+
h['Accept'] = 'application/json' if opts[:format] == :json
|
|
187
|
+
h['api-key'] = ENV['NCBI_API_KEY'] if ENV['NCBI_API_KEY']
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
},
|
|
176
191
|
ncbi_datasets: {
|
|
177
192
|
dbs: {
|
|
178
193
|
genome: {
|
|
@@ -19,9 +19,6 @@ class MiGA::RemoteDataset
|
|
|
19
19
|
getter = database_hash[:getter] || :download
|
|
20
20
|
action = database_hash[:method] || universe_hash[:method]
|
|
21
21
|
|
|
22
|
-
# Clean IDs
|
|
23
|
-
ids =
|
|
24
|
-
|
|
25
22
|
# Return options
|
|
26
23
|
{
|
|
27
24
|
universe: universe, db: db, ids: ids.is_a?(Array) ? ids : [ids],
|
|
@@ -60,11 +57,10 @@ class MiGA::RemoteDataset
|
|
|
60
57
|
begin
|
|
61
58
|
ncbi_asm_get(opts)
|
|
62
59
|
rescue
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
# narrowly, but for now it's safer to catch broadly here.
|
|
60
|
+
# Missing or unavailable assemblies are reported by `ncbi_asm_get` as
|
|
61
|
+
# MiGA::RemoteDataError (or its subclass MiGA::RemoteDataMissingError),
|
|
62
|
+
# but network errors and other transient failures can surface with
|
|
63
|
+
# different classes, so it's safer to catch broadly here.
|
|
68
64
|
|
|
69
65
|
opts_embl = opts.dup.merge(universe: :ebi, db: :embl)
|
|
70
66
|
download_get(opts_embl)
|
|
@@ -79,12 +75,25 @@ class MiGA::RemoteDataset
|
|
|
79
75
|
# +file+ (mandatory): String, assembly saved here
|
|
80
76
|
# +extra+: Hash, passed to download
|
|
81
77
|
# +format+: String, ignored
|
|
78
|
+
#
|
|
79
|
+
# Unversioned accessions (e.g., GCA_000175575) are first resolved to their
|
|
80
|
+
# current version (see +ncbi_asm_resolve_acc+), and the resolved accessions
|
|
81
|
+
# are registered in the +:ncbi_asm+ metadata entry of +obj+, if any.
|
|
82
|
+
#
|
|
83
|
+
# Raises MiGA::RemoteDataMissingError if the downloaded archive is invalid
|
|
84
|
+
# or doesn't contain any genomic FastA file
|
|
82
85
|
def ncbi_asm_get(opts)
|
|
83
86
|
require 'tempfile'
|
|
84
87
|
require 'zip'
|
|
85
88
|
|
|
89
|
+
ids = (opts[:ids].is_a?(Array) ? opts[:ids] : [opts[:ids]])
|
|
90
|
+
acc = ids.map { |id| ncbi_asm_resolve_acc(id) }
|
|
91
|
+
if acc != ids && opts[:obj].respond_to?(:metadata)
|
|
92
|
+
opts[:obj].metadata[:ncbi_asm] = acc.join(',')
|
|
93
|
+
end
|
|
94
|
+
|
|
86
95
|
zipped = download(
|
|
87
|
-
:ncbi_datasets_download, :genome,
|
|
96
|
+
:ncbi_datasets_download, :genome, acc,
|
|
88
97
|
:zip, nil, opts[:extra], opts[:obj]
|
|
89
98
|
)
|
|
90
99
|
zip_tmp = Tempfile.new(['asm', '.zip'], encoding: zipped.encoding.to_s)
|
|
@@ -92,22 +101,74 @@ class MiGA::RemoteDataset
|
|
|
92
101
|
zip_tmp.close
|
|
93
102
|
|
|
94
103
|
o = ''
|
|
104
|
+
found = false
|
|
95
105
|
ofh = opts[:file] ? File.open(opts[:file], 'w') : nil
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
begin
|
|
107
|
+
Zip::File.open(zip_tmp.path) do |zfh|
|
|
108
|
+
zfh.each do |entry|
|
|
109
|
+
if entry.file? && entry.name =~ /_genomic\.fna$/
|
|
110
|
+
DEBUG "Extracting: #{entry.name}"
|
|
111
|
+
found = true
|
|
112
|
+
entry.get_input_stream do |ifh|
|
|
113
|
+
cont = MiGA::MiGA.normalize_encoding(ifh.read).chomp + "\n"
|
|
114
|
+
ofh&.print(cont)
|
|
115
|
+
o += cont
|
|
116
|
+
end
|
|
104
117
|
end
|
|
105
118
|
end
|
|
106
119
|
end
|
|
120
|
+
rescue Zip::Error => e
|
|
121
|
+
raise MiGA::RemoteDataMissingError.new(
|
|
122
|
+
"NCBI Datasets returned an invalid archive for #{acc.join(',')} " \
|
|
123
|
+
"(#{zipped.bytesize} bytes): #{e.message}"
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
unless found
|
|
127
|
+
raise MiGA::RemoteDataMissingError.new(
|
|
128
|
+
"NCBI Datasets returned no genomic sequences for #{acc.join(',')}"
|
|
129
|
+
)
|
|
107
130
|
end
|
|
108
|
-
ofh&.close
|
|
109
|
-
File.unlink(zip_tmp.path)
|
|
110
131
|
o
|
|
132
|
+
ensure
|
|
133
|
+
ofh&.close
|
|
134
|
+
zip_tmp&.close!
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
##
|
|
138
|
+
# Resolve the NCBI Assembly Accession +acc+ to its current versioned
|
|
139
|
+
# accession using NCBI Datasets if it lacks a version (e.g., GCA_000175575
|
|
140
|
+
# is resolved to GCA_000175575.2). Any other accession is returned as is.
|
|
141
|
+
#
|
|
142
|
+
# Raises MiGA::RemoteDataMissingError if the accession is not found, or
|
|
143
|
+
# MiGA::RemoteDataError if the response from NCBI cannot be interpreted
|
|
144
|
+
def ncbi_asm_resolve_acc(acc)
|
|
145
|
+
return acc unless acc.to_s =~ /\AGC[AF]_\d{9}\z/
|
|
146
|
+
|
|
147
|
+
doc = download(:ncbi_datasets_report, :genome, acc, :json)
|
|
148
|
+
begin
|
|
149
|
+
report = MiGA::Json.parse(doc, contents: true)
|
|
150
|
+
rescue => e
|
|
151
|
+
raise MiGA::RemoteDataError.new(
|
|
152
|
+
"Cannot resolve NCBI Assembly Accession #{acc}, " \
|
|
153
|
+
"unexpected response from NCBI Datasets: #{e.message}"
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
unless report.is_a?(Hash) && (report.empty? || report.key?(:reports))
|
|
157
|
+
raise MiGA::RemoteDataError.new(
|
|
158
|
+
"Cannot resolve NCBI Assembly Accession #{acc}, " \
|
|
159
|
+
"unexpected response from NCBI Datasets: #{doc[0, 200]}"
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
entry = (report[:reports] || []).first || {}
|
|
164
|
+
out = entry[:current_accession] || entry[:accession]
|
|
165
|
+
if out.nil? || out.empty?
|
|
166
|
+
raise MiGA::RemoteDataMissingError.new(
|
|
167
|
+
"NCBI Assembly Accession not found: #{acc}"
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
DEBUG "Resolved NCBI Assembly Accession: #{acc} -> #{out}"
|
|
171
|
+
out
|
|
111
172
|
end
|
|
112
173
|
|
|
113
174
|
##
|
|
@@ -116,6 +177,7 @@ class MiGA::RemoteDataset
|
|
|
116
177
|
def ncbi_gb_get(opts)
|
|
117
178
|
# Simply use defaults, but ensure that the URL can be properly formed
|
|
118
179
|
o = download_rest(opts.merge(universe: :ncbi, db: :nuccore))
|
|
180
|
+
ncbi_efetch_check(o, opts[:ids])
|
|
119
181
|
return o unless o.strip.empty?
|
|
120
182
|
|
|
121
183
|
begin
|
|
@@ -137,9 +199,35 @@ class MiGA::RemoteDataset
|
|
|
137
199
|
end
|
|
138
200
|
ids.each_slice(20).map do |bunch|
|
|
139
201
|
download_rest(opts.merge(universe: :ncbi, db: :nuccore, ids: bunch))
|
|
202
|
+
.tap { |doc| ncbi_efetch_check(doc, bunch) }
|
|
140
203
|
end.join("\n")
|
|
141
204
|
end
|
|
142
205
|
|
|
206
|
+
##
|
|
207
|
+
# Check if +doc+ is an error message returned by NCBI efetch for +ids+
|
|
208
|
+
# (with HTTP status 200), and raise an exception if so. Unrecognized
|
|
209
|
+
# IDs raise MiGA::RemoteDataMissingError, and any other error raises
|
|
210
|
+
# MiGA::RemoteDataError
|
|
211
|
+
def ncbi_efetch_check(doc, ids)
|
|
212
|
+
return unless doc =~ /\A\s*Error:/
|
|
213
|
+
|
|
214
|
+
# NCBI sometimes spaces out the characters in the message, as in
|
|
215
|
+
# "F a i l e d t o u n d e r s t a n d i d"
|
|
216
|
+
msg = doc.strip.sub(/\AError:\s*/, '')
|
|
217
|
+
msg = msg.gsub(/(?<! ) (?! )/, '') if msg =~ /\A(\S ){3}/
|
|
218
|
+
msg = msg.gsub(/\s+/, ' ').strip
|
|
219
|
+
ids = ids.is_a?(Array) ? ids.join(',') : ids.to_s
|
|
220
|
+
|
|
221
|
+
if msg =~ /Failed to understand id|not found/i
|
|
222
|
+
raise MiGA::RemoteDataMissingError.new(
|
|
223
|
+
"NCBI nuccore entry not found: #{ids} (efetch: #{msg})"
|
|
224
|
+
)
|
|
225
|
+
end
|
|
226
|
+
raise MiGA::RemoteDataError.new(
|
|
227
|
+
"NCBI efetch failed for nuccore entry #{ids}: #{msg}"
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
|
|
143
231
|
##
|
|
144
232
|
# Download data using the GET method. Supported +opts+ (Hash) include:
|
|
145
233
|
# +universe+ (mandatory): Symbol
|
|
@@ -212,21 +300,52 @@ class MiGA::RemoteDataset
|
|
|
212
300
|
##
|
|
213
301
|
# Looks for the entry +id+ in +dbfrom+, and returns the linked
|
|
214
302
|
# identifier in +db+ (or nil).
|
|
303
|
+
#
|
|
304
|
+
# Raises MiGA::RemoteDataMissingError if NCBI doesn't recognize +id+ in
|
|
305
|
+
# +dbfrom+, or MiGA::RemoteDataError if the response cannot be interpreted
|
|
306
|
+
# even after one retry
|
|
215
307
|
def ncbi_map(id, dbfrom, db)
|
|
308
|
+
attempts ||= 0
|
|
216
309
|
doc = download(:ncbi_map, dbfrom, id, :json, nil, db: db)
|
|
217
310
|
return if doc.empty?
|
|
218
311
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
312
|
+
# NCBI may include raw control characters within strings (e.g., in ERROR
|
|
313
|
+
# messages), which is invalid JSON, but harmless to replace with spaces
|
|
314
|
+
tree = MiGA::Json.parse(doc.gsub(/[[:cntrl:]]/, ' '), contents: true)
|
|
315
|
+
linkset = tree.is_a?(Hash) ? (tree.dig(:linksets, 0) || {}) : {}
|
|
316
|
+
error = linkset[:ERROR] ||
|
|
317
|
+
(linkset[:linksetdbs] || []).map { |i| i[:ERROR] }.compact.first
|
|
318
|
+
if error
|
|
319
|
+
# Errors with an empty list of IDs indicate that +id+ is not recognized
|
|
320
|
+
# in +dbfrom+ (e.g., BLOB ID IS NOT IMPLEMENTED), so there's no point
|
|
321
|
+
# in retrying
|
|
322
|
+
if (linkset[:ids] || []).empty?
|
|
323
|
+
raise MiGA::RemoteDataMissingError.new(
|
|
324
|
+
"NCBI #{dbfrom} entry not found: #{id} " \
|
|
325
|
+
"(elink: #{error.to_s.squeeze(' ').strip})"
|
|
326
|
+
)
|
|
327
|
+
end
|
|
328
|
+
raise MiGA::RemoteDataError.new(
|
|
329
|
+
"NCBI elink failed for #{dbfrom} entry #{id}: " \
|
|
330
|
+
"#{error.to_s.squeeze(' ').strip}"
|
|
331
|
+
)
|
|
227
332
|
end
|
|
228
333
|
|
|
229
|
-
|
|
334
|
+
linkset.dig(:linksetdbs, 0, :links, 0)
|
|
335
|
+
rescue MiGA::RemoteDataMissingError
|
|
336
|
+
raise
|
|
337
|
+
rescue JSON::ParserError, MiGA::RemoteDataError => e
|
|
338
|
+
# Any other error in the response is possibly transient (e.g., busy
|
|
339
|
+
# servers), so retry once before giving up
|
|
340
|
+
if (attempts += 1) > 1
|
|
341
|
+
raise e if e.is_a?(MiGA::RemoteDataError)
|
|
342
|
+
raise MiGA::RemoteDataError.new(
|
|
343
|
+
"Unexpected response from NCBI elink for #{dbfrom} entry #{id}: #{e}"
|
|
344
|
+
)
|
|
345
|
+
end
|
|
346
|
+
sleep 5
|
|
347
|
+
DEBUG "RETRYING after: #{e}"
|
|
348
|
+
retry
|
|
230
349
|
end
|
|
231
350
|
|
|
232
351
|
##
|
data/lib/miga/remote_dataset.rb
CHANGED
|
@@ -368,11 +368,17 @@ class MiGA::RemoteDataset < MiGA::MiGA
|
|
|
368
368
|
def get_type_status_ncbi_nuccore(metadata)
|
|
369
369
|
return metadata if metadata[:ncbi_nuccore].nil?
|
|
370
370
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
371
|
+
begin
|
|
372
|
+
biosample =
|
|
373
|
+
self.class.ncbi_map(metadata[:ncbi_nuccore], :nuccore, :biosample)
|
|
374
|
+
return metadata if biosample.nil?
|
|
375
|
+
|
|
376
|
+
asm = self.class.ncbi_map(biosample, :biosample, :assembly)
|
|
377
|
+
rescue MiGA::RemoteDataMissingError => e
|
|
378
|
+
# Type status is best-effort, missing entries are reported on download
|
|
379
|
+
MiGA::MiGA.DEBUG "Cannot determine type status: #{e}"
|
|
380
|
+
return metadata
|
|
381
|
+
end
|
|
376
382
|
metadata[:ncbi_asm] = asm.to_s unless asm.nil?
|
|
377
383
|
get_type_status_ncbi_asm metadata
|
|
378
384
|
end
|
data/lib/miga/version.rb
CHANGED
|
@@ -12,7 +12,7 @@ module MiGA
|
|
|
12
12
|
# - String indicating release status:
|
|
13
13
|
# - rc* release candidate, not released as gem
|
|
14
14
|
# - [0-9]+ stable release, released as gem
|
|
15
|
-
VERSION = [1.4,
|
|
15
|
+
VERSION = [1.4, 4, 0].freeze
|
|
16
16
|
|
|
17
17
|
##
|
|
18
18
|
# Nickname for the current major.minor version.
|
|
@@ -20,7 +20,7 @@ module MiGA
|
|
|
20
20
|
|
|
21
21
|
##
|
|
22
22
|
# Date of the current gem relese.
|
|
23
|
-
VERSION_DATE = Date.new(2026,
|
|
23
|
+
VERSION_DATE = Date.new(2026, 11, 9)
|
|
24
24
|
|
|
25
25
|
##
|
|
26
26
|
# References of MiGA
|
data/test/remote_dataset_test.rb
CHANGED
|
@@ -215,6 +215,271 @@ class RemoteDatasetTest < Test::Unit::TestCase
|
|
|
215
215
|
assert_equal('}', p[-1])
|
|
216
216
|
end
|
|
217
217
|
|
|
218
|
+
def test_ncbi_datasets_report_request
|
|
219
|
+
# No remote access needed
|
|
220
|
+
rd = MiGA::RemoteDataset.new(
|
|
221
|
+
'GCA_000175575', :genome, :ncbi_datasets_report
|
|
222
|
+
)
|
|
223
|
+
u = rd.download_uri
|
|
224
|
+
h = rd.download_headers
|
|
225
|
+
|
|
226
|
+
assert(u.is_a? URI)
|
|
227
|
+
assert_equal('https', u.scheme)
|
|
228
|
+
assert_equal('dataset_report', File.basename(u.path))
|
|
229
|
+
assert_include(u.path, '/accession/GCA_000175575/')
|
|
230
|
+
|
|
231
|
+
assert(h.is_a? Hash)
|
|
232
|
+
assert_equal(1, h.size)
|
|
233
|
+
assert_equal('application/json', h['Accept'])
|
|
234
|
+
|
|
235
|
+
ENV['NCBI_API_KEY'] = 'Not-a-real-key'
|
|
236
|
+
h = rd.download_headers
|
|
237
|
+
ENV.delete('NCBI_API_KEY')
|
|
238
|
+
assert_equal(2, h.size)
|
|
239
|
+
assert_equal('Not-a-real-key', h['api-key'])
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def test_ncbi_asm_resolve_acc_offline
|
|
243
|
+
report = {
|
|
244
|
+
'GCA_000175575' =>
|
|
245
|
+
'{"reports":[{"accession":"GCA_000175575.2",' \
|
|
246
|
+
'"current_accession":"GCA_000175575.2"}]}',
|
|
247
|
+
'GCA_999999999' => '{}',
|
|
248
|
+
'GCA_888888888' => '{"error":"Too Many Requests","code":429}',
|
|
249
|
+
'GCA_777777777' => '<html>Service unavailable</html>'
|
|
250
|
+
}
|
|
251
|
+
calls = []
|
|
252
|
+
with_stubbed_download(lambda do |universe, _db, ids, *_rest|
|
|
253
|
+
assert_equal(:ncbi_datasets_report, universe)
|
|
254
|
+
calls << ids
|
|
255
|
+
report[ids]
|
|
256
|
+
end) do
|
|
257
|
+
rd = MiGA::RemoteDataset
|
|
258
|
+
assert_equal('GCA_000175575.2', rd.ncbi_asm_resolve_acc('GCA_000175575'))
|
|
259
|
+
assert_equal(['GCA_000175575'], calls)
|
|
260
|
+
|
|
261
|
+
# Versioned (or non-assembly) accessions are not resolved
|
|
262
|
+
assert_equal('GCF_000175575.2', rd.ncbi_asm_resolve_acc('GCF_000175575.2'))
|
|
263
|
+
assert_equal('M30502.1', rd.ncbi_asm_resolve_acc('M30502.1'))
|
|
264
|
+
assert_equal(1, calls.size)
|
|
265
|
+
|
|
266
|
+
# Missing accessions
|
|
267
|
+
assert_raise(MiGA::RemoteDataMissingError) do
|
|
268
|
+
rd.ncbi_asm_resolve_acc('GCA_999999999')
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Unexpected responses are not reported as missing data
|
|
272
|
+
%w[GCA_888888888 GCA_777777777].each do |acc|
|
|
273
|
+
err = assert_raise(MiGA::RemoteDataError) do
|
|
274
|
+
rd.ncbi_asm_resolve_acc(acc)
|
|
275
|
+
end
|
|
276
|
+
assert_not_instance_of(MiGA::RemoteDataMissingError, err)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def test_ncbi_asm_get_bad_zip_offline
|
|
282
|
+
require 'zip'
|
|
283
|
+
# A truncated archive like the one NCBI returns for unversioned accessions
|
|
284
|
+
truncated = "PK\x03\x04\x14\x00\x00\x00\x08\x00README.md".b
|
|
285
|
+
# A valid archive without any genomic FastA file
|
|
286
|
+
no_fna = Zip::OutputStream.write_buffer do |zos|
|
|
287
|
+
zos.put_next_entry('README.md')
|
|
288
|
+
zos.write('Nothing to see here')
|
|
289
|
+
end.string
|
|
290
|
+
|
|
291
|
+
[truncated, no_fna].each do |zipped|
|
|
292
|
+
with_stubbed_download(lambda do |universe, *_rest|
|
|
293
|
+
assert_equal(:ncbi_datasets_download, universe)
|
|
294
|
+
zipped
|
|
295
|
+
end) do
|
|
296
|
+
file = tmpfile('asm.fna')
|
|
297
|
+
err = assert_raise(MiGA::RemoteDataMissingError) do
|
|
298
|
+
MiGA::RemoteDataset.ncbi_asm_get(
|
|
299
|
+
ids: ['GCA_000175575.2'], file: file, extra: {}
|
|
300
|
+
)
|
|
301
|
+
end
|
|
302
|
+
assert_include(err.message, 'GCA_000175575.2')
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def test_ncbi_asm_get_offline
|
|
308
|
+
require 'zip'
|
|
309
|
+
zipped = Zip::OutputStream.write_buffer do |zos|
|
|
310
|
+
zos.put_next_entry(
|
|
311
|
+
'ncbi_dataset/data/GCA_000175575.2/GCA_000175575.2_ASM_genomic.fna'
|
|
312
|
+
)
|
|
313
|
+
zos.write(">ctg1\nACGT\n")
|
|
314
|
+
end.string
|
|
315
|
+
report =
|
|
316
|
+
'{"reports":[{"accession":"GCA_000175575.2",' \
|
|
317
|
+
'"current_accession":"GCA_000175575.2"}]}'
|
|
318
|
+
|
|
319
|
+
rd = MiGA::RemoteDataset.new('GCA_000175575', :assembly, :ncbi)
|
|
320
|
+
with_stubbed_download(lambda do |universe, _db, ids, *_rest|
|
|
321
|
+
case universe
|
|
322
|
+
when :ncbi_datasets_report
|
|
323
|
+
report
|
|
324
|
+
when :ncbi_datasets_download
|
|
325
|
+
assert_equal(['GCA_000175575.2'], ids)
|
|
326
|
+
zipped
|
|
327
|
+
end
|
|
328
|
+
end) do
|
|
329
|
+
file = tmpfile('asm.fna')
|
|
330
|
+
o = MiGA::RemoteDataset.ncbi_asm_get(
|
|
331
|
+
ids: rd.ids, file: file, extra: {}, obj: rd
|
|
332
|
+
)
|
|
333
|
+
assert_equal(">ctg1\nACGT\n", o)
|
|
334
|
+
assert_equal(">ctg1\nACGT\n", File.read(file))
|
|
335
|
+
assert_equal('GCA_000175575.2', rd.metadata[:ncbi_asm])
|
|
336
|
+
assert_equal('GCA_000175575', rd.metadata[:ncbi_assembly])
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def test_ncbi_asm_resolve_acc
|
|
341
|
+
declare_remote_access
|
|
342
|
+
rd = MiGA::RemoteDataset
|
|
343
|
+
assert_equal('GCA_000175575.2', rd.ncbi_asm_resolve_acc('GCA_000175575'))
|
|
344
|
+
assert_equal('GCF_000175575.2', rd.ncbi_asm_resolve_acc('GCF_000175575'))
|
|
345
|
+
assert_raise(MiGA::RemoteDataMissingError) do
|
|
346
|
+
rd.ncbi_asm_resolve_acc('GCA_999999999')
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def test_ncbi_asm_unversioned
|
|
351
|
+
declare_remote_access
|
|
352
|
+
rd = MiGA::RemoteDataset.new('GCA_000175575', :assembly, :ncbi)
|
|
353
|
+
ds = rd.save_to(project, 'unversioned')
|
|
354
|
+
assert_equal('GCA_000175575.2', ds.metadata[:ncbi_asm])
|
|
355
|
+
fna = ds.result(:assembly).file_path(:largecontigs)
|
|
356
|
+
assert_path_exist(fna)
|
|
357
|
+
assert_operator(File.size(fna), :>, 100_000)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Payload returned by NCBI elink for IDs not recognized in +dbfrom+, which
|
|
361
|
+
# includes raw newlines within strings (and is therefore invalid JSON)
|
|
362
|
+
ELINK_NOT_FOUND =
|
|
363
|
+
'{"header":{"type":"elink","version":"0.3"},"linksets":[{' \
|
|
364
|
+
'"dbfrom":"nuccore","ids":[],"linksetdbs":[{"dbto":"biosample",' \
|
|
365
|
+
'"linkname":"nuccore_biosample","ERROR":"NCBI C++ Exception:' + "\n" +
|
|
366
|
+
' Error: UNK_MODULE(CException::eInvalid) \"UNK_FILE\", line ' \
|
|
367
|
+
'18446744073709551615: UNK_FUNC --- ' + "\n" +
|
|
368
|
+
'"}],"ERROR":"BLOB ID IS NOT IMPLEMENTED"}]}'
|
|
369
|
+
|
|
370
|
+
def test_ncbi_map_offline
|
|
371
|
+
responses = {
|
|
372
|
+
# ID not recognized in nuccore
|
|
373
|
+
'GCA_039803745.1' => ELINK_NOT_FOUND,
|
|
374
|
+
# ID recognized, but the link failed (possibly transient)
|
|
375
|
+
'M30502.1' => ELINK_NOT_FOUND.sub('"ids":[]', '"ids":["1332355"]'),
|
|
376
|
+
# Unparseable response
|
|
377
|
+
'CP005986.1' => '<html>Service unavailable</html>',
|
|
378
|
+
# Normal responses, with and without links
|
|
379
|
+
'CP005986.2' =>
|
|
380
|
+
'{"header":{"type":"elink","version":"0.3"},"linksets":[{' \
|
|
381
|
+
'"dbfrom":"nuccore","ids":["640840007"],"linksetdbs":[{' \
|
|
382
|
+
'"dbto":"biosample","linkname":"nuccore_biosample",' \
|
|
383
|
+
'"links":["2469802"]}]}]}',
|
|
384
|
+
'M30502.2' =>
|
|
385
|
+
'{"header":{"type":"elink","version":"0.3"},"linksets":[{' \
|
|
386
|
+
'"dbfrom":"nuccore","ids":["1332355"]}]}'
|
|
387
|
+
}
|
|
388
|
+
calls = []
|
|
389
|
+
sleeps = 0
|
|
390
|
+
with_stubbed_class_method(:sleep, ->(*_args) { sleeps += 1 }) do
|
|
391
|
+
with_stubbed_download(lambda do |universe, db, ids, *_rest|
|
|
392
|
+
assert_equal(:ncbi_map, universe)
|
|
393
|
+
assert_equal(:nuccore, db)
|
|
394
|
+
calls << ids
|
|
395
|
+
responses[ids]
|
|
396
|
+
end) do
|
|
397
|
+
rd = MiGA::RemoteDataset
|
|
398
|
+
|
|
399
|
+
# Not found: no retries
|
|
400
|
+
err = assert_raise(MiGA::RemoteDataMissingError) do
|
|
401
|
+
rd.ncbi_map('GCA_039803745.1', :nuccore, :biosample)
|
|
402
|
+
end
|
|
403
|
+
assert_include(err.message, 'GCA_039803745.1')
|
|
404
|
+
assert_include(err.message, 'BLOB ID IS NOT IMPLEMENTED')
|
|
405
|
+
assert_equal(1, calls.size)
|
|
406
|
+
assert_equal(0, sleeps)
|
|
407
|
+
|
|
408
|
+
# Other errors: retry once and fail with a generic RemoteDataError
|
|
409
|
+
%w[M30502.1 CP005986.1].each do |id|
|
|
410
|
+
calls.clear
|
|
411
|
+
sleeps = 0
|
|
412
|
+
err = assert_raise(MiGA::RemoteDataError) do
|
|
413
|
+
rd.ncbi_map(id, :nuccore, :biosample)
|
|
414
|
+
end
|
|
415
|
+
assert_not_instance_of(MiGA::RemoteDataMissingError, err)
|
|
416
|
+
assert_include(err.message, id)
|
|
417
|
+
assert_equal([id, id], calls)
|
|
418
|
+
assert_equal(1, sleeps)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Normal responses
|
|
422
|
+
sleeps = 0
|
|
423
|
+
assert_equal('2469802', rd.ncbi_map('CP005986.2', :nuccore, :biosample))
|
|
424
|
+
assert_nil(rd.ncbi_map('M30502.2', :nuccore, :biosample))
|
|
425
|
+
assert_equal(0, sleeps)
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def test_type_status_nuccore_not_found_offline
|
|
431
|
+
rd = MiGA::RemoteDataset.new('GCA_039803745.1', :nuccore, :ncbi)
|
|
432
|
+
with_stubbed_download(->(*_args) { ELINK_NOT_FOUND }) do
|
|
433
|
+
md = { ncbi_nuccore: 'GCA_039803745.1' }
|
|
434
|
+
assert_equal(md, rd.get_type_status(md))
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def test_ncbi_gb_get_efetch_error_offline
|
|
439
|
+
responses = {
|
|
440
|
+
'GCA_039803745.1' =>
|
|
441
|
+
"Error: F a i l e d t o u n d e r s t a n d i d : " \
|
|
442
|
+
"G C A _ 0 3 9 8 0 3 7 4 5 . 1 \n\n\n",
|
|
443
|
+
'M30502.1' => "Error: Something else went wrong\n"
|
|
444
|
+
}
|
|
445
|
+
with_stubbed_class_method(:download_rest, ->(opts) {
|
|
446
|
+
responses[opts[:ids].first]
|
|
447
|
+
}) do
|
|
448
|
+
rd = MiGA::RemoteDataset
|
|
449
|
+
err = assert_raise(MiGA::RemoteDataMissingError) do
|
|
450
|
+
rd.ncbi_gb_get(ids: ['GCA_039803745.1'], file: tmpfile('a.fna'))
|
|
451
|
+
end
|
|
452
|
+
assert_equal(
|
|
453
|
+
'NCBI nuccore entry not found: GCA_039803745.1 ' \
|
|
454
|
+
'(efetch: Failed to understand id: GCA_039803745.1)',
|
|
455
|
+
err.message
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
err = assert_raise(MiGA::RemoteDataError) do
|
|
459
|
+
rd.ncbi_gb_get(ids: ['M30502.1'], file: tmpfile('b.fna'))
|
|
460
|
+
end
|
|
461
|
+
assert_not_instance_of(MiGA::RemoteDataMissingError, err)
|
|
462
|
+
assert_include(err.message, 'Something else went wrong')
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def test_ncbi_map_not_found
|
|
467
|
+
declare_remote_access
|
|
468
|
+
assert_raise(MiGA::RemoteDataMissingError) do
|
|
469
|
+
MiGA::RemoteDataset.ncbi_map('GCA_039803745.1', :nuccore, :biosample)
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def test_nuccore_not_found
|
|
474
|
+
declare_remote_access
|
|
475
|
+
rd = MiGA::RemoteDataset.new('GCA_039803745.1', :nuccore, :ncbi)
|
|
476
|
+
err = assert_raise(MiGA::RemoteDataMissingError) do
|
|
477
|
+
rd.save_to(project, 'wrong_db')
|
|
478
|
+
end
|
|
479
|
+
assert_include(err.message, 'GCA_039803745.1')
|
|
480
|
+
assert_nil(project.dataset('wrong_db'))
|
|
481
|
+
end
|
|
482
|
+
|
|
218
483
|
# This test is too expensive (too much time to run it!)
|
|
219
484
|
# def test_net_timeout
|
|
220
485
|
# declare_remote_access
|
|
@@ -222,4 +487,35 @@ class RemoteDatasetTest < Test::Unit::TestCase
|
|
|
222
487
|
# rd = MiGA::RemoteDataset.new(bad, :assembly, :web)
|
|
223
488
|
# assert_raise(Net::ReadTimeout) { rd.save_to(project, "bad") }
|
|
224
489
|
# end
|
|
490
|
+
|
|
491
|
+
private
|
|
492
|
+
|
|
493
|
+
##
|
|
494
|
+
# Temporarily replace MiGA::RemoteDataset.download with +fun+ while running
|
|
495
|
+
# the given block
|
|
496
|
+
def with_stubbed_download(fun, &blk)
|
|
497
|
+
with_stubbed_class_method(:download, fun, &blk)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
##
|
|
501
|
+
# Temporarily replace the class method +name+ of MiGA::RemoteDataset with
|
|
502
|
+
# +fun+ while running the given block. Methods inherited by the class (like
|
|
503
|
+
# +sleep+) can also be stubbed
|
|
504
|
+
def with_stubbed_class_method(name, fun)
|
|
505
|
+
klass = MiGA::RemoteDataset.singleton_class
|
|
506
|
+
orig = :"__orig_#{name}"
|
|
507
|
+
own =
|
|
508
|
+
klass.method_defined?(name, false) ||
|
|
509
|
+
klass.private_method_defined?(name, false)
|
|
510
|
+
klass.send(:alias_method, orig, name) if own
|
|
511
|
+
klass.send(:define_method, name) { |*args| fun.call(*args) }
|
|
512
|
+
yield
|
|
513
|
+
ensure
|
|
514
|
+
if own
|
|
515
|
+
klass.send(:alias_method, name, orig)
|
|
516
|
+
klass.send(:remove_method, orig)
|
|
517
|
+
else
|
|
518
|
+
klass.send(:remove_method, name)
|
|
519
|
+
end
|
|
520
|
+
end
|
|
225
521
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: miga-base
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.4.
|
|
4
|
+
version: 1.4.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Luis M. Rodriguez-R
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: daemons
|