dratools 0.0.1 → 0.0.2
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/README.md +7 -7
- data/docs/design.md +21 -7
- data/docs/environment.md +21 -4
- data/docs/usage.md +17 -16
- data/lib/dratools/accession_resolver.rb +27 -0
- data/lib/dratools/accession_resource_type_classifier.rb +2 -2
- data/lib/dratools/command_line_interface.rb +1 -6
- data/lib/dratools/commands/meta_command.rb +2 -2
- data/lib/dratools/commands/runs_command.rb +1 -25
- data/lib/dratools/commands/size_command.rb +8 -7
- data/lib/dratools/commands/url_command.rb +8 -7
- data/lib/dratools/config.rb +14 -0
- data/lib/dratools/ddbj_record_fields.rb +1 -6
- data/lib/dratools/ddbj_resource_client.rb +102 -21
- data/lib/dratools/download_candidate_builder.rb +4 -31
- data/lib/dratools/external_command_runner.rb +95 -12
- data/lib/dratools/run_record_collector.rb +71 -25
- data/lib/dratools/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2890dc54bae2a8fe0c715daa8c778794d20292714d5d13d658ec538a0e95bb92
|
|
4
|
+
data.tar.gz: 409192a8111f7152f33c8ea586ac6db9cea775a1643151d70c4e5adce288ff66
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 17f69d78de301843fb5d43b53fa800528f6404d5e8974f9764e875c2e7489e0691e4b51a10e74638c3d353d7eab2ed9df52e12b12bd3158f42bf36a02a21738e
|
|
7
|
+
data.tar.gz: c49a56c178b3f55be5b980ad3c7ebd0d4d76df33c55a160b943148206380c1853a8b40ffa790529ee0893f0d8ed6f7b359ac30fbe0ee0ddd5e9535f155ac0728
|
data/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# dratools
|
|
2
2
|
|
|
3
3
|
[](https://github.com/kojix2/dratools/actions/workflows/ci.yml)
|
|
4
|
+
[](https://badge.fury.io/rb/dratools)
|
|
4
5
|
[](https://tokei.kojix2.net/github/kojix2/dratools)
|
|
6
|
+
[](https://doi.org/10.5281/zenodo.20967539)
|
|
5
7
|
|
|
6
8
|
[dratools](https://github.com/kojix2/dratools) は、日本国内の [DDBJ](https://www.ddbj.nig.ac.jp) からゲノムデータをダウンロードするためのツールです。
|
|
7
9
|
|
|
@@ -13,7 +15,7 @@ dratools は非公式のツールです。DDBJ や国立遺伝学研究所が提
|
|
|
13
15
|
|
|
14
16
|
必要なものは次のとおりです。
|
|
15
17
|
|
|
16
|
-
- [ruby](https://www.ruby-lang.org/)
|
|
18
|
+
- [ruby](https://www.ruby-lang.org/) 3.0 以上
|
|
17
19
|
- [curl](https://curl.se/) または [wget](https://www.gnu.org/software/wget/)
|
|
18
20
|
|
|
19
21
|
Rubyのgemとしてインストールできます。
|
|
@@ -80,7 +82,7 @@ printf 'DRR000001\nDRR000002\n' | dratools get -O ~/Downloads
|
|
|
80
82
|
dratools probe DRR000001 # URL の到達性だけ確認する
|
|
81
83
|
dratools url --json DRR000001 # URL 情報を JSON で表示する
|
|
82
84
|
dratools url --tsv DRR000001 # run/type/url/size/md5 を TAB 区切りで
|
|
83
|
-
dratools meta --json DRR000001 #
|
|
85
|
+
dratools meta --json DRR000001 # entry JSON を表示する
|
|
84
86
|
dratools runs PRJNA341783 | dratools get -O ~/Downloads # run 一覧をダウンロードへ渡す
|
|
85
87
|
dratools size --bytes PRJNA341783 # 合計サイズをバイト数で表示する
|
|
86
88
|
dratools size --per-run DRX000001 # 親 accession を run ごとに集計する
|
|
@@ -89,7 +91,7 @@ dratools get --skip-existing -O ~/Downloads DRR000001 # 既存ファイルは
|
|
|
89
91
|
|
|
90
92
|
詳しくは [使い方](docs/usage.md) をご覧ください。親 accession を扱うときの設定は [環境変数](docs/environment.md) にまとめています。
|
|
91
93
|
|
|
92
|
-
|
|
94
|
+
実際のファイル転送には通常 `curl` または `wget` を使います。`aria2c` も環境変数で明示した場合だけ使えます。ダウンロード中は進捗表示を端末にそのまま流します。同名のファイルが既にある場合は、サーバのファイルサイズと比べて再取得の要否を判断します。md5 が得られる場合は md5 で照合します。
|
|
93
95
|
|
|
94
96
|
ツールは全てコーディングエージェントによって実装されました。
|
|
95
97
|
|
|
@@ -102,11 +104,9 @@ dratools get --skip-existing -O ~/Downloads DRR000001 # 既存ファイルは
|
|
|
102
104
|
|
|
103
105
|
## お役立ちノート
|
|
104
106
|
|
|
105
|
-
|
|
106
|
-
日本国内のサーバーからゲノムデータをダウンロードすると作業が楽になります。
|
|
107
|
+
海外のサーバーからゲノムデータをダウンロードするのは大変です。日本国内のサーバーを使うと作業が楽になります。
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
NASの付属ソフトにURLを入力すると、何もしなくても自動でダウンロードが進むのでストレスが少ないです。
|
|
109
|
+
手間をかけずにダウンロードする方法として、NAS を使う方法があります。まず `dratools url` で URL の一覧を出します。次に NAS の付属ソフトの GUI 画面に、その URL をまとめて貼り付けます。あとは放置します。ダウンロードは自動で進みます。
|
|
110
110
|
|
|
111
111
|
## 開発
|
|
112
112
|
|
data/docs/design.md
CHANGED
|
@@ -12,24 +12,33 @@ accession から URL までの流れは次のとおりです。
|
|
|
12
12
|
|
|
13
13
|
```text
|
|
14
14
|
accession
|
|
15
|
-
-> DDBJ Search
|
|
15
|
+
-> DDBJ Search entry JSON (/search/api/entries/{type}/{id}.json)
|
|
16
16
|
-> sra-run record
|
|
17
|
-
->
|
|
17
|
+
-> distribution
|
|
18
18
|
-> https / ftp URL
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
`/resource/{type}/{id}.json` は DDBJ Search の後方互換入口です。現在は
|
|
22
|
+
`/search/entry/{type}/{id}.json` へリダイレクトされ、さらに nginx で
|
|
23
|
+
`/search/api/entries/{type}/{id}.json` へ rewrite されます。dratools は
|
|
24
|
+
リダイレクトを避けるため、正規の `/search/api/entries` endpoint を直接使います。
|
|
25
|
+
|
|
26
|
+
run 一覧だけが必要な場合は DBLinks endpoint を使います。多数の run record が必要な
|
|
27
|
+
場合は Bulk endpoint を使います。どちらも同じ DDBJ Search API サーバーの機能です。
|
|
28
|
+
|
|
21
29
|
## ダウンロード確認
|
|
22
30
|
|
|
23
31
|
ゲノムデータはサイズが大きいです。`probe` は完全なダウンロードを行いません。
|
|
24
32
|
|
|
25
33
|
- `curl` がある場合: `--range 0-0` と `--max-time` を使う
|
|
26
34
|
- `wget` がある場合: `--spider` と `--timeout` を使う
|
|
35
|
+
- `aria2c` がある場合: `--dry-run=true` と `--timeout` を使う
|
|
27
36
|
|
|
28
37
|
これは URL が使えるかどうかを短時間で確認するためです。完全な整合性の確認ではありません。
|
|
29
38
|
|
|
30
39
|
## サイズ確認
|
|
31
40
|
|
|
32
|
-
`size` は
|
|
41
|
+
`size` は entry JSON のサイズ情報を使いません。実レコードにはサイズや md5 が含まれないことが多いためです。代わりに、解決した URL に HTTP `HEAD` を送ります。`Content-Length` を合計します。
|
|
33
42
|
|
|
34
43
|
FASTQ はディレクトリ URL で返ることがあります。その場合はディレクトリ一覧を取得します。`*.fastq*` のリンクを取り出します。各ファイルに `HEAD` を送ります。取得できないものは失敗にしません。`unresolved` として数えます。
|
|
35
44
|
|
|
@@ -37,12 +46,17 @@ FASTQ はディレクトリ URL で返ることがあります。その場合は
|
|
|
37
46
|
|
|
38
47
|
## 実ダウンロード
|
|
39
48
|
|
|
40
|
-
実ダウンロードでは総時間の上限を設けません。数十 GB のファイルでは長時間かかるためです。代わりに、接続のタイムアウトと失速検知を `curl` / `wget` に渡します。
|
|
49
|
+
実ダウンロードでは総時間の上限を設けません。数十 GB のファイルでは長時間かかるためです。代わりに、接続のタイムアウトと失速検知を `curl` / `wget` / `aria2c` に渡します。
|
|
41
50
|
|
|
42
51
|
- `curl`: `--connect-timeout`, `--speed-limit`, `--speed-time`, `--retry`
|
|
43
52
|
- `wget`: `--connect-timeout`, `--read-timeout`, `--tries`, `--waitretry`
|
|
53
|
+
- `aria2c`: `--connect-timeout`, `--timeout`, `--lowest-speed-limit`, `--max-tries`, `--retry-wait`
|
|
54
|
+
|
|
55
|
+
`aria2c` は分割ダウンロードができますが、既定では `--split=1` と `--max-connection-per-server=1` で単一接続にします。公共アーカイブへの負荷を既定で増やさないためです。
|
|
56
|
+
|
|
57
|
+
`DRATOOLS_DOWNLOAD_RETRY_COUNT` はリトライ回数として扱います。`curl --retry` はリトライ回数ですが、`wget --tries` と `aria2c --max-tries` は総試行回数なので、外部コマンドには `DRATOOLS_DOWNLOAD_RETRY_COUNT + 1` を渡します。
|
|
44
58
|
|
|
45
|
-
ダウンロードは `system(*command)` で実行します。`Open3.capture3`
|
|
59
|
+
ダウンロードは `system(*command)` で実行します。`Open3.capture3` は使いません。外部コマンドの進捗を端末にそのまま表示するためです。失敗した場合は、コマンド行と終了ステータスを `CommandError` にします。
|
|
46
60
|
|
|
47
61
|
## チェックサムと既存ファイル
|
|
48
62
|
|
|
@@ -53,7 +67,7 @@ md5 が得られる候補では、ダウンロード後に `Digest::MD5.file`
|
|
|
53
67
|
1. `--force` があれば既存ファイルを使わず再取得する
|
|
54
68
|
2. `--skip-existing` があれば md5 を見ずに既存ファイルを使う
|
|
55
69
|
3. md5 があり、既存ファイルの md5 が一致すれば再取得せず `Skipped` にする
|
|
56
|
-
4. それ以外は `curl --continue-at
|
|
70
|
+
4. それ以外は `curl --continue-at -`, `wget --continue`, `aria2c --continue=true` でレジュームを試みる
|
|
57
71
|
|
|
58
72
|
md5 が無い候補では、既定では既存ファイルをスキップしません。SRA ファイルとしての検証は dratools の既定動作には含めません。
|
|
59
73
|
|
|
@@ -69,7 +83,7 @@ Ruby 側の依存は増やしません。次の標準ライブラリを使いま
|
|
|
69
83
|
- `digest/md5`
|
|
70
84
|
- `minitest`
|
|
71
85
|
|
|
72
|
-
|
|
86
|
+
実ファイルの転送だけは外部コマンドに任せます。自動選択では環境にある `curl` または `wget` を使います。`aria2c` は `DRATOOLS_DOWNLOAD_COMMAND=aria2c` で明示指定された場合だけ使います。
|
|
73
87
|
|
|
74
88
|
## 今後の候補
|
|
75
89
|
|
data/docs/environment.md
CHANGED
|
@@ -15,15 +15,20 @@
|
|
|
15
15
|
|
|
16
16
|
## ダウンロード開始と失速検知
|
|
17
17
|
|
|
18
|
-
`get` は大きいファイルを扱います。このため、総ダウンロード時間の上限を設けません。代わりに、接続タイムアウト、失速検知、リトライの設定を `curl` / `wget` に渡します。
|
|
18
|
+
`get` は大きいファイルを扱います。このため、総ダウンロード時間の上限を設けません。代わりに、接続タイムアウト、失速検知、リトライの設定を `curl` / `wget` / `aria2c` に渡します。
|
|
19
19
|
|
|
20
20
|
| 環境変数 | 既定値 | 役割 |
|
|
21
21
|
| --- | ---: | --- |
|
|
22
22
|
| `DRATOOLS_DOWNLOAD_CONNECT_TIMEOUT` | `30` | 接続確立のタイムアウト秒数 |
|
|
23
23
|
| `DRATOOLS_DOWNLOAD_STALL_TIMEOUT` | `60` | この秒数のあいだ転送速度が閾値を下回ると失速扱いにする |
|
|
24
|
-
| `DRATOOLS_DOWNLOAD_STALL_SPEED` | `1024` |
|
|
25
|
-
| `DRATOOLS_DOWNLOAD_RETRY_COUNT` | `3` | ダウンロード失敗時のリトライ回数。`0`
|
|
26
|
-
| `DRATOOLS_DOWNLOAD_RETRY_WAIT` | `5` | `wget` のリトライ待ち秒数 |
|
|
24
|
+
| `DRATOOLS_DOWNLOAD_STALL_SPEED` | `1024` | `curl` / `aria2c` の失速判定に使う最低転送速度。単位は bytes/sec |
|
|
25
|
+
| `DRATOOLS_DOWNLOAD_RETRY_COUNT` | `3` | ダウンロード失敗時のリトライ回数。`0` はリトライなし |
|
|
26
|
+
| `DRATOOLS_DOWNLOAD_RETRY_WAIT` | `5` | `wget` / `aria2c` のリトライ待ち秒数 |
|
|
27
|
+
| `DRATOOLS_DOWNLOAD_COMMAND` | 自動 | 実ダウンロードと `probe` に使う外部コマンド。`curl`, `wget`, `aria2c` のいずれか |
|
|
28
|
+
|
|
29
|
+
`DRATOOLS_DOWNLOAD_COMMAND` を指定しない場合は、`curl`, `wget` の順で PATH にあるものを使います。`aria2c` は自動探索しません。`aria2c` を使う場合は `DRATOOLS_DOWNLOAD_COMMAND=aria2c` を指定してください。指定した場合はそのコマンドだけを使います。見つからない場合は、別のコマンドへ自動では切り替えません。
|
|
30
|
+
|
|
31
|
+
`DRATOOLS_DOWNLOAD_RETRY_COUNT` は、初回の試行を含まない「リトライ回数」です。`curl` にはそのまま渡します。`wget` と `aria2c` は総試行回数を受け取るため、内部で `リトライ回数 + 1` に変換します。
|
|
27
32
|
|
|
28
33
|
## 例
|
|
29
34
|
|
|
@@ -62,6 +67,18 @@ DRATOOLS_DOWNLOAD_RETRY_COUNT=0 \
|
|
|
62
67
|
dratools get --no-verify -O ~/Downloads DRR000001
|
|
63
68
|
```
|
|
64
69
|
|
|
70
|
+
`wget` を明示してダウンロードする:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
DRATOOLS_DOWNLOAD_COMMAND=wget dratools get -O ~/Downloads DRR000001
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`aria2c` を明示してダウンロードする:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
DRATOOLS_DOWNLOAD_COMMAND=aria2c dratools get -O ~/Downloads DRR000001
|
|
80
|
+
```
|
|
81
|
+
|
|
65
82
|
## 注意
|
|
66
83
|
|
|
67
84
|
これらは上級の設定です。上限を大きくすると、DDBJ Search API へのリクエスト数が増えます。`size` の HTTP `HEAD` の回数も増えます。
|
data/docs/usage.md
CHANGED
|
@@ -35,7 +35,7 @@ bundle exec rake install
|
|
|
35
35
|
|
|
36
36
|
## メタ情報を表示する (`meta`)
|
|
37
37
|
|
|
38
|
-
`meta` は DDBJ Search の
|
|
38
|
+
`meta` は DDBJ Search の entry JSON を要約して表示します。
|
|
39
39
|
|
|
40
40
|
```sh
|
|
41
41
|
dratools meta DRR300000
|
|
@@ -158,7 +158,7 @@ dratools url --json DRR000001
|
|
|
158
158
|
|
|
159
159
|
## 接続確認 (`probe`)
|
|
160
160
|
|
|
161
|
-
`probe`
|
|
161
|
+
`probe` は接続確認だけを行います。ファイルをダウンロードしません。
|
|
162
162
|
|
|
163
163
|
```sh
|
|
164
164
|
dratools probe --timeout 5 DRR000001
|
|
@@ -190,29 +190,30 @@ direct run を多数持つ親 accession では、`tree` は各 run を個別取
|
|
|
190
190
|
dratools get -O ~/Downloads DRR000001
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
-
ダウンロード中は `curl
|
|
193
|
+
ダウンロード中は `curl`, `wget`, `aria2c` のいずれかの進捗が標準エラーに出ます。取得したファイルは `Downloaded<TAB>PATH` と表示します。既存ファイルを再利用した場合は `Skipped<TAB>PATH` と表示します。これらは標準エラーに出ます。最後に `dratools get: N downloaded, M skipped` のサマリを出します。状態とパスは TAB 区切りです。パスだけを取り出すには `cut -f2` を使います。
|
|
194
194
|
|
|
195
|
-
###
|
|
195
|
+
### 既存ファイルの扱い
|
|
196
196
|
|
|
197
|
-
md5
|
|
197
|
+
DDBJ のメタデータに md5 が含まれることは多くありません。ほとんどの場合、md5 は得られません。以下では、まず md5 が無い場合の動作を示します。
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
同名のファイルが既にある場合、`get` はサーバにファイルサイズを問い合わせます。それをローカルのファイルサイズと比べます。
|
|
200
200
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
- サイズが同じなら、再取得しません。`Skipped` と表示します。
|
|
202
|
+
- ローカルのほうが小さいなら、再取得します。途中で中断したファイルが対象です。
|
|
203
|
+
- ローカルのほうが大きいなら、エラーになります。別物の可能性があるためです。`--force` で上書きできます。
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
md5 が得られる場合は、サイズではなく md5 で判定します。既存ファイルの md5 が一致すれば `Skipped` と表示します。ダウンロード後にも md5 を照合します。
|
|
206
206
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
```sh
|
|
210
|
-
dratools get --force -O ~/Downloads DRR000001
|
|
211
|
-
```
|
|
207
|
+
### オプション
|
|
212
208
|
|
|
213
|
-
|
|
209
|
+
| オプション | 動作 |
|
|
210
|
+
| --- | --- |
|
|
211
|
+
| `--force` | 既存ファイルがあっても再取得します。 |
|
|
212
|
+
| `--skip-existing` | 同名のファイルがあれば、確認せずスキップします。サーバへの問い合わせを省きます。 |
|
|
213
|
+
| `--no-verify` | ダウンロード後の md5 照合を省きます。md5 が無い場合は照合しないので、効果はありません。 |
|
|
214
214
|
|
|
215
215
|
```sh
|
|
216
|
+
dratools get --force -O ~/Downloads DRR000001
|
|
216
217
|
dratools get --skip-existing -O ~/Downloads DRR000001
|
|
217
218
|
```
|
|
218
219
|
|
|
@@ -72,12 +72,39 @@ module Dratools
|
|
|
72
72
|
@client.fetch_resource_record(resource_type, accession)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
def direct_run_accessions_for(accession)
|
|
76
|
+
accession = accession.to_s.upcase
|
|
77
|
+
resource_type = resource_type_for(accession)
|
|
78
|
+
return [accession] if resource_type == DdbjRecordFields::SRA_RUN_RESOURCE_TYPE
|
|
79
|
+
|
|
80
|
+
@client.fetch_db_links(
|
|
81
|
+
resource_type,
|
|
82
|
+
accession,
|
|
83
|
+
target: DdbjRecordFields::SRA_RUN_RESOURCE_TYPE
|
|
84
|
+
).filter_map { |xref| xref_accession(xref) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def direct_run_count_for(accession)
|
|
88
|
+
accession = accession.to_s.upcase
|
|
89
|
+
resource_type = resource_type_for(accession)
|
|
90
|
+
return 1 if resource_type == DdbjRecordFields::SRA_RUN_RESOURCE_TYPE
|
|
91
|
+
|
|
92
|
+
counts = @client.fetch_db_link_counts([{ type: resource_type, id: accession }])
|
|
93
|
+
counts.fetch([resource_type, accession], {}).fetch(DdbjRecordFields::SRA_RUN_RESOURCE_TYPE, 0)
|
|
94
|
+
end
|
|
95
|
+
|
|
75
96
|
def resource_type_for(accession)
|
|
76
97
|
@resource_type_classifier.resource_type_for(accession)
|
|
77
98
|
end
|
|
78
99
|
|
|
79
100
|
private
|
|
80
101
|
|
|
102
|
+
def xref_accession(xref)
|
|
103
|
+
xref[DdbjRecordFields::IDENTIFIER_KEY] ||
|
|
104
|
+
xref[DdbjRecordFields::ID_KEY] ||
|
|
105
|
+
xref[DdbjRecordFields::ACCESSION_KEY]
|
|
106
|
+
end
|
|
107
|
+
|
|
81
108
|
def attach_downloads(node, file_type:)
|
|
82
109
|
if node.run? && node.record
|
|
83
110
|
downloads = @download_candidate_builder.build_from_run_record(node.record)
|
|
@@ -4,14 +4,14 @@ require_relative 'ddbj_record_fields'
|
|
|
4
4
|
require_relative 'errors'
|
|
5
5
|
|
|
6
6
|
module Dratools
|
|
7
|
-
# accession の接頭辞から DDBJ
|
|
7
|
+
# accession の接頭辞から DDBJ Search entry type を判定する。
|
|
8
8
|
class AccessionResourceTypeClassifier
|
|
9
9
|
RUN_PREFIXES = /\A[DES]RR\d+\z/
|
|
10
10
|
EXPERIMENT_PREFIXES = /\A[DES]RX\d+\z/
|
|
11
11
|
SAMPLE_PREFIXES = /\A[DES]RS\d+\z/
|
|
12
12
|
STUDY_PREFIXES = /\A[DES]RP\d+\z/
|
|
13
13
|
SUBMISSION_PREFIXES = /\A[DES]RA\d+\z/
|
|
14
|
-
BIOPROJECT_PREFIXES = /\APRJ
|
|
14
|
+
BIOPROJECT_PREFIXES = /\APRJ[DEN][A-Z]\d+\z/
|
|
15
15
|
BIOSAMPLE_PREFIXES = /\ASAM(?:D|N|EA|EG)?\d+\z/
|
|
16
16
|
|
|
17
17
|
TYPE_BY_ACCESSION = [
|
|
@@ -124,12 +124,7 @@ module Dratools
|
|
|
124
124
|
stream.puts format(' %-7<name>s %<summary>s', name: name, summary: summary)
|
|
125
125
|
end
|
|
126
126
|
stream.puts ''
|
|
127
|
-
stream.puts '
|
|
128
|
-
SUBCOMMAND_ALIASES.each do |alias_name, canonical|
|
|
129
|
-
stream.puts format(' %-7<a>s -> %<c>s', a: alias_name, c: canonical)
|
|
130
|
-
end
|
|
131
|
-
stream.puts ''
|
|
132
|
-
stream.puts "Run '#{COMMAND_NAME} <command> --help' for command options."
|
|
127
|
+
stream.puts "各コマンドのオプションは '#{COMMAND_NAME} <command> --help' で確認できます。"
|
|
133
128
|
stream.puts ''
|
|
134
129
|
stream.puts 'Examples:'
|
|
135
130
|
USAGE_EXAMPLES.each { |example| stream.puts " #{example}" }
|
|
@@ -6,7 +6,7 @@ require_relative 'base_command'
|
|
|
6
6
|
|
|
7
7
|
module Dratools
|
|
8
8
|
module Commands
|
|
9
|
-
# DDBJ
|
|
9
|
+
# DDBJ Search entry JSON のメタ情報を要約表示する。
|
|
10
10
|
class MetaCommand < BaseCommand
|
|
11
11
|
LABEL_WIDTH = 18
|
|
12
12
|
|
|
@@ -21,7 +21,7 @@ module Dratools
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def configure_parser(parser)
|
|
24
|
-
parser.on('--json', '生の
|
|
24
|
+
parser.on('--json', '生の entry JSON を整形して表示する') { @options[:json] = true }
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def usage_examples
|
|
@@ -6,8 +6,6 @@ module Dratools
|
|
|
6
6
|
module Commands
|
|
7
7
|
# accession を run accession のフラットな一覧に展開する。
|
|
8
8
|
class RunsCommand < BaseCommand
|
|
9
|
-
XREF_URL_PATTERN = %r{/(?:resource|search/entry)/sra-run/([^/?#.]+)}
|
|
10
|
-
|
|
11
9
|
private
|
|
12
10
|
|
|
13
11
|
def command_name
|
|
@@ -41,29 +39,7 @@ module Dratools
|
|
|
41
39
|
end
|
|
42
40
|
|
|
43
41
|
def direct_run_accessions_for(accession)
|
|
44
|
-
|
|
45
|
-
if record[DdbjRecordFields::TYPE_KEY] == DdbjRecordFields::SRA_RUN_RESOURCE_TYPE
|
|
46
|
-
return [record_accession(record)].compact
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
record.fetch(DdbjRecordFields::DB_XREFS_KEY, []).filter_map do |xref|
|
|
50
|
-
next unless xref[DdbjRecordFields::TYPE_KEY] == DdbjRecordFields::SRA_RUN_RESOURCE_TYPE
|
|
51
|
-
|
|
52
|
-
xref[DdbjRecordFields::IDENTIFIER_KEY] ||
|
|
53
|
-
xref[DdbjRecordFields::ID_KEY] ||
|
|
54
|
-
run_accession_from_url(xref[DdbjRecordFields::URL_KEY])
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def record_accession(record)
|
|
59
|
-
record[DdbjRecordFields::ACCESSION_KEY] ||
|
|
60
|
-
record[DdbjRecordFields::IDENTIFIER_KEY] ||
|
|
61
|
-
record[DdbjRecordFields::ID_KEY] ||
|
|
62
|
-
record[DdbjRecordFields::PRIMARY_ID_KEY]
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def run_accession_from_url(url)
|
|
66
|
-
url.to_s.match(XREF_URL_PATTERN)&.[](1)
|
|
42
|
+
@resolver.direct_run_accessions_for(accession)
|
|
67
43
|
end
|
|
68
44
|
end
|
|
69
45
|
end
|
|
@@ -140,19 +140,20 @@ module Dratools
|
|
|
140
140
|
|
|
141
141
|
def fetch_record_for_size(accession)
|
|
142
142
|
max_direct_runs = Config.size_max_direct_runs
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
ddbj_record
|
|
143
|
+
validate_direct_run_expansion_count!(accession, max_direct_runs)
|
|
144
|
+
@resolver.fetch_record_for(accession)
|
|
146
145
|
end
|
|
147
146
|
|
|
148
|
-
def
|
|
147
|
+
def validate_direct_run_expansion_count!(accession, max_direct_runs)
|
|
149
148
|
return unless max_direct_runs
|
|
150
149
|
|
|
151
|
-
direct_run_count =
|
|
152
|
-
xref[DdbjRecordFields::TYPE_KEY] == DdbjRecordFields::SRA_RUN_RESOURCE_TYPE
|
|
153
|
-
end
|
|
150
|
+
direct_run_count = @resolver.direct_run_count_for(accession)
|
|
154
151
|
return if direct_run_count <= max_direct_runs
|
|
155
152
|
|
|
153
|
+
raise_direct_run_limit_error(accession, direct_run_count, max_direct_runs)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def raise_direct_run_limit_error(accession, direct_run_count, max_direct_runs)
|
|
156
157
|
raise InvalidRecordError,
|
|
157
158
|
"#{accession.to_s.upcase} has #{direct_run_count} direct runs; " \
|
|
158
159
|
"size expands at most #{max_direct_runs} direct runs from one parent accession. " \
|
|
@@ -73,19 +73,20 @@ module Dratools
|
|
|
73
73
|
|
|
74
74
|
def fetch_record_for_url(accession)
|
|
75
75
|
max_direct_runs = Config.url_max_direct_runs
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
ddbj_record
|
|
76
|
+
validate_direct_run_expansion_count!(accession, max_direct_runs)
|
|
77
|
+
@resolver.fetch_record_for(accession)
|
|
79
78
|
end
|
|
80
79
|
|
|
81
|
-
def
|
|
80
|
+
def validate_direct_run_expansion_count!(accession, max_direct_runs)
|
|
82
81
|
return unless max_direct_runs
|
|
83
82
|
|
|
84
|
-
direct_run_count =
|
|
85
|
-
xref[DdbjRecordFields::TYPE_KEY] == DdbjRecordFields::SRA_RUN_RESOURCE_TYPE
|
|
86
|
-
end
|
|
83
|
+
direct_run_count = @resolver.direct_run_count_for(accession)
|
|
87
84
|
return if direct_run_count <= max_direct_runs
|
|
88
85
|
|
|
86
|
+
raise_direct_run_limit_error(accession, direct_run_count, max_direct_runs)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def raise_direct_run_limit_error(accession, direct_run_count, max_direct_runs)
|
|
89
90
|
raise InvalidRecordError,
|
|
90
91
|
"#{accession.to_s.upcase} has #{direct_run_count} direct runs; " \
|
|
91
92
|
"url expands at most #{max_direct_runs} direct runs from one parent accession. " \
|
data/lib/dratools/config.rb
CHANGED
|
@@ -14,6 +14,7 @@ module Dratools
|
|
|
14
14
|
DOWNLOAD_STALL_SPEED_ENV = 'DRATOOLS_DOWNLOAD_STALL_SPEED'
|
|
15
15
|
DOWNLOAD_RETRY_COUNT_ENV = 'DRATOOLS_DOWNLOAD_RETRY_COUNT'
|
|
16
16
|
DOWNLOAD_RETRY_WAIT_ENV = 'DRATOOLS_DOWNLOAD_RETRY_WAIT'
|
|
17
|
+
DOWNLOAD_COMMAND_ENV = 'DRATOOLS_DOWNLOAD_COMMAND'
|
|
17
18
|
|
|
18
19
|
DEFAULT_MAX_RECURSIVE_NON_RUN_XREFS = 100
|
|
19
20
|
DEFAULT_TREE_MAX_DIRECT_RUNS = 50
|
|
@@ -24,6 +25,7 @@ module Dratools
|
|
|
24
25
|
DEFAULT_DOWNLOAD_STALL_SPEED_BYTES_PER_SECOND = 1024
|
|
25
26
|
DEFAULT_DOWNLOAD_RETRY_COUNT = 3
|
|
26
27
|
DEFAULT_DOWNLOAD_RETRY_WAIT_SECONDS = 5
|
|
28
|
+
SUPPORTED_DOWNLOAD_COMMANDS = %w[curl wget aria2c].freeze
|
|
27
29
|
UNLIMITED_VALUE = 'unlimited'
|
|
28
30
|
|
|
29
31
|
module_function
|
|
@@ -70,6 +72,18 @@ module Dratools
|
|
|
70
72
|
positive_integer(DOWNLOAD_RETRY_WAIT_ENV, DEFAULT_DOWNLOAD_RETRY_WAIT_SECONDS)
|
|
71
73
|
end
|
|
72
74
|
|
|
75
|
+
def download_command
|
|
76
|
+
value = ENV.fetch(DOWNLOAD_COMMAND_ENV, '').strip
|
|
77
|
+
return nil if value.empty?
|
|
78
|
+
return value if SUPPORTED_DOWNLOAD_COMMANDS.include?(value)
|
|
79
|
+
|
|
80
|
+
invalid_environment_value!(
|
|
81
|
+
DOWNLOAD_COMMAND_ENV,
|
|
82
|
+
value,
|
|
83
|
+
SUPPORTED_DOWNLOAD_COMMANDS.join(' or ')
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
73
87
|
def positive_integer_or_unlimited(name, default)
|
|
74
88
|
value = ENV.fetch(name, '').strip
|
|
75
89
|
return default if value.empty?
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Dratools
|
|
4
|
-
# DDBJ Search
|
|
4
|
+
# DDBJ Search entry JSON で使う resource type とキー名をまとめる。
|
|
5
5
|
module DdbjRecordFields
|
|
6
6
|
SRA_RUN_RESOURCE_TYPE = 'sra-run'
|
|
7
7
|
SRA_EXPERIMENT_RESOURCE_TYPE = 'sra-experiment'
|
|
@@ -18,18 +18,13 @@ module Dratools
|
|
|
18
18
|
DB_XREFS_KEY = 'dbXrefs'
|
|
19
19
|
CHILD_BIOPROJECTS_KEY = 'childBioProjects'
|
|
20
20
|
TYPE_KEY = 'type'
|
|
21
|
-
URL_KEY = 'url'
|
|
22
|
-
FTP_URL_KEY = 'ftpUrl'
|
|
23
21
|
ID_KEY = 'id'
|
|
24
22
|
IDENTIFIER_KEY = 'identifier'
|
|
25
23
|
ACCESSION_KEY = 'accession'
|
|
26
24
|
PRIMARY_ID_KEY = 'primaryId'
|
|
27
|
-
DOWNLOAD_URL_KEY = 'downloadUrl'
|
|
28
25
|
DISTRIBUTION_KEY = 'distribution'
|
|
29
26
|
CONTENT_URL_KEY = 'contentUrl'
|
|
30
27
|
CONTENT_SIZE_KEY = 'contentSize'
|
|
31
|
-
SIZE_KEY = 'size'
|
|
32
|
-
FILE_SIZE_KEY = 'fileSize'
|
|
33
28
|
MD5_KEY = 'md5'
|
|
34
29
|
MD5_SUM_KEY = 'md5sum'
|
|
35
30
|
ENCODING_FORMAT_KEY = 'encodingFormat'
|
|
@@ -7,12 +7,17 @@ require 'uri'
|
|
|
7
7
|
|
|
8
8
|
require_relative 'errors'
|
|
9
9
|
require_relative 'version'
|
|
10
|
+
require_relative 'ddbj_record_fields'
|
|
10
11
|
|
|
11
12
|
module Dratools
|
|
12
|
-
# DDBJ
|
|
13
|
+
# DDBJ Search API を呼び出す薄い HTTP クライアント。
|
|
13
14
|
class DdbjResourceClient
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
DDBJ_SEARCH_API_BASE_URL = 'https://ddbj.nig.ac.jp/search/api'
|
|
16
|
+
ENTRIES_PATH = 'entries'
|
|
17
|
+
DBLINK_PATH = 'dblink'
|
|
18
|
+
ENTRY_RECORD_EXTENSION = '.json'
|
|
19
|
+
BULK_MAX_IDS = 1000
|
|
20
|
+
DBLINK_COUNTS_MAX_ITEMS = 100
|
|
16
21
|
HTTPS_SCHEME = 'https'
|
|
17
22
|
HTTP_LOCATION_HEADER = 'location'
|
|
18
23
|
USER_AGENT_HEADER = 'User-Agent'
|
|
@@ -20,7 +25,7 @@ module Dratools
|
|
|
20
25
|
DEFAULT_OPEN_TIMEOUT_SECONDS = 10
|
|
21
26
|
DEFAULT_READ_TIMEOUT_SECONDS = 30
|
|
22
27
|
|
|
23
|
-
def initialize(base_url:
|
|
28
|
+
def initialize(base_url: DDBJ_SEARCH_API_BASE_URL, open_timeout: DEFAULT_OPEN_TIMEOUT_SECONDS,
|
|
24
29
|
read_timeout: DEFAULT_READ_TIMEOUT_SECONDS)
|
|
25
30
|
@base_url = base_url.delete_suffix('/')
|
|
26
31
|
@open_timeout = open_timeout
|
|
@@ -28,32 +33,93 @@ module Dratools
|
|
|
28
33
|
end
|
|
29
34
|
|
|
30
35
|
def fetch_resource_record(type, accession)
|
|
31
|
-
fetch_json("#{@base_url}/#{type}/#{accession}#{
|
|
36
|
+
fetch_json("#{@base_url}/#{ENTRIES_PATH}/#{type}/#{accession}#{ENTRY_RECORD_EXTENSION}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def fetch_db_links(type, accession, target: nil)
|
|
40
|
+
request_uri = URI("#{@base_url}/#{DBLINK_PATH}/#{type}/#{accession}")
|
|
41
|
+
request_uri.query = URI.encode_www_form(target: target) if target
|
|
42
|
+
fetch_json(request_uri.to_s).fetch(DdbjRecordFields::DB_XREFS_KEY, [])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def fetch_resource_records_bulk(type, accessions, include_db_xrefs: false)
|
|
46
|
+
accessions.each_slice(BULK_MAX_IDS).with_object({}) do |chunk, records|
|
|
47
|
+
records.merge!(
|
|
48
|
+
fetch_resource_records_bulk_chunk(type, chunk, include_db_xrefs: include_db_xrefs)
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def fetch_db_link_counts(items)
|
|
54
|
+
items.each_slice(DBLINK_COUNTS_MAX_ITEMS).with_object({}) do |chunk, counts|
|
|
55
|
+
counts.merge!(fetch_db_link_counts_chunk(chunk))
|
|
56
|
+
end
|
|
32
57
|
end
|
|
33
58
|
|
|
34
59
|
private
|
|
35
60
|
|
|
61
|
+
def fetch_db_link_counts_chunk(items)
|
|
62
|
+
request_url = "#{@base_url}/#{DBLINK_PATH}/counts"
|
|
63
|
+
payload = post_json(request_url, items: items)
|
|
64
|
+
payload.fetch('items', []).to_h do |item|
|
|
65
|
+
[[item['type'], item[DdbjRecordFields::IDENTIFIER_KEY]], item.fetch('counts', {})]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def fetch_resource_records_bulk_chunk(type, accessions, include_db_xrefs:)
|
|
70
|
+
request_uri = URI("#{@base_url}/#{ENTRIES_PATH}/#{type}/bulk")
|
|
71
|
+
request_uri.query = URI.encode_www_form(includeDbXrefs: include_db_xrefs)
|
|
72
|
+
payload = post_json(request_uri.to_s, ids: accessions)
|
|
73
|
+
payload.fetch('entries', []).to_h do |record|
|
|
74
|
+
accession = record[DdbjRecordFields::IDENTIFIER_KEY] ||
|
|
75
|
+
record[DdbjRecordFields::ACCESSION_KEY] ||
|
|
76
|
+
record[DdbjRecordFields::ID_KEY] ||
|
|
77
|
+
record[DdbjRecordFields::PRIMARY_ID_KEY]
|
|
78
|
+
[accession, record]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
36
82
|
def fetch_json(request_url, redirects_remaining = DEFAULT_REDIRECT_LIMIT)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
83
|
+
with_network_errors(request_url) do
|
|
84
|
+
request_uri = URI(request_url)
|
|
85
|
+
response = get_http_response(request_uri)
|
|
86
|
+
|
|
87
|
+
case response
|
|
88
|
+
when Net::HTTPSuccess
|
|
89
|
+
parse_json_response(response, request_url)
|
|
90
|
+
when Net::HTTPRedirection
|
|
91
|
+
raise NetworkError, "too many redirects: #{request_url}" if redirects_remaining <= 0
|
|
92
|
+
|
|
93
|
+
location = response[HTTP_LOCATION_HEADER]
|
|
94
|
+
raise NetworkError, "redirect without location: #{request_url}" if location.to_s.empty?
|
|
95
|
+
|
|
96
|
+
fetch_json(URI.join(request_uri, location).to_s, redirects_remaining - 1)
|
|
97
|
+
when Net::HTTPNotFound
|
|
98
|
+
raise NotFoundError, "not found: #{request_url}"
|
|
99
|
+
else
|
|
100
|
+
raise NetworkError, "HTTP #{response.code}: #{request_url}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def post_json(request_url, payload)
|
|
106
|
+
with_network_errors(request_url) do
|
|
107
|
+
response = post_http_response(URI(request_url), payload)
|
|
108
|
+
return parse_json_response(response, request_url) if response.is_a?(Net::HTTPSuccess)
|
|
109
|
+
raise NotFoundError, "not found: #{request_url}" if response.is_a?(Net::HTTPNotFound)
|
|
110
|
+
|
|
53
111
|
raise NetworkError, "HTTP #{response.code}: #{request_url}"
|
|
54
112
|
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def parse_json_response(response, request_url)
|
|
116
|
+
JSON.parse(response.body)
|
|
55
117
|
rescue JSON::ParserError => error
|
|
56
118
|
raise NetworkError, "invalid JSON from #{request_url}: #{error.message}", cause: error
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def with_network_errors(request_url)
|
|
122
|
+
yield
|
|
57
123
|
rescue Timeout::Error, IOError, SocketError, SystemCallError => error
|
|
58
124
|
message = "failed to fetch #{request_url}: #{error.class}: #{error.message}"
|
|
59
125
|
raise NetworkError, message, cause: error
|
|
@@ -71,6 +137,21 @@ module Dratools
|
|
|
71
137
|
end
|
|
72
138
|
end
|
|
73
139
|
|
|
140
|
+
def post_http_response(request_uri, payload)
|
|
141
|
+
Net::HTTP.start(
|
|
142
|
+
request_uri.host,
|
|
143
|
+
request_uri.port,
|
|
144
|
+
use_ssl: request_uri.scheme == HTTPS_SCHEME,
|
|
145
|
+
open_timeout: @open_timeout,
|
|
146
|
+
read_timeout: @read_timeout
|
|
147
|
+
) do |http|
|
|
148
|
+
request = Net::HTTP::Post.new(request_uri.request_uri, USER_AGENT_HEADER => user_agent)
|
|
149
|
+
request['Content-Type'] = 'application/json'
|
|
150
|
+
request.body = JSON.generate(payload)
|
|
151
|
+
http.request(request)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
74
155
|
def user_agent
|
|
75
156
|
"#{NAME}/#{VERSION}"
|
|
76
157
|
end
|
|
@@ -4,12 +4,12 @@ require_relative 'ddbj_record_fields'
|
|
|
4
4
|
require_relative 'download_candidate'
|
|
5
5
|
|
|
6
6
|
module Dratools
|
|
7
|
-
# DDBJ run レコードの
|
|
7
|
+
# DDBJ run レコードの distribution から DownloadCandidate を作る。
|
|
8
8
|
class DownloadCandidateBuilder
|
|
9
9
|
def build_from_run_record(run_record)
|
|
10
10
|
run_accession = run_accession_from(run_record)
|
|
11
11
|
downloads = download_items_from(run_record).filter_map do |download_item|
|
|
12
|
-
|
|
12
|
+
build_from_distribution_item(run_accession, download_item)
|
|
13
13
|
end
|
|
14
14
|
downloads.uniq { |download| download_key(download) }
|
|
15
15
|
end
|
|
@@ -24,21 +24,12 @@ module Dratools
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def download_items_from(ddbj_record)
|
|
27
|
-
ddbj_record
|
|
28
|
-
ddbj_record.fetch(DdbjRecordFields::DISTRIBUTION_KEY, [])
|
|
27
|
+
Array(ddbj_record[DdbjRecordFields::DISTRIBUTION_KEY])
|
|
29
28
|
end
|
|
30
29
|
|
|
31
|
-
def
|
|
30
|
+
def build_from_distribution_item(run_accession, download_item)
|
|
32
31
|
return unless download_item.is_a?(Hash)
|
|
33
32
|
|
|
34
|
-
if download_item[DdbjRecordFields::CONTENT_URL_KEY]
|
|
35
|
-
build_from_distribution_item(run_accession, download_item)
|
|
36
|
-
else
|
|
37
|
-
build_from_download_url_item(run_accession, download_item)
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def build_from_distribution_item(run_accession, download_item)
|
|
42
33
|
file_type = file_type_from_distribution(download_item)
|
|
43
34
|
return unless file_type
|
|
44
35
|
|
|
@@ -52,28 +43,10 @@ module Dratools
|
|
|
52
43
|
)
|
|
53
44
|
end
|
|
54
45
|
|
|
55
|
-
def build_from_download_url_item(run_accession, download_item)
|
|
56
|
-
file_type = file_type_from_download_url(download_item)
|
|
57
|
-
return unless file_type
|
|
58
|
-
|
|
59
|
-
DownloadCandidate.new(
|
|
60
|
-
run_accession: run_accession,
|
|
61
|
-
type: file_type,
|
|
62
|
-
url: download_item[DdbjRecordFields::URL_KEY],
|
|
63
|
-
ftp_url: download_item[DdbjRecordFields::FTP_URL_KEY],
|
|
64
|
-
size: download_item[DdbjRecordFields::SIZE_KEY] || download_item[DdbjRecordFields::FILE_SIZE_KEY],
|
|
65
|
-
md5: download_item[DdbjRecordFields::MD5_KEY] || download_item[DdbjRecordFields::MD5_SUM_KEY]
|
|
66
|
-
)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
46
|
def file_type_from_distribution(download_item)
|
|
70
47
|
file_type_from(download_item[DdbjRecordFields::ENCODING_FORMAT_KEY])
|
|
71
48
|
end
|
|
72
49
|
|
|
73
|
-
def file_type_from_download_url(download_item)
|
|
74
|
-
file_type_from(download_item[DdbjRecordFields::TYPE_KEY])
|
|
75
|
-
end
|
|
76
|
-
|
|
77
50
|
def file_type_from(value)
|
|
78
51
|
case value.to_s.downcase
|
|
79
52
|
when DdbjRecordFields::FILE_TYPE_SRA
|
|
@@ -8,77 +8,146 @@ require_relative 'config'
|
|
|
8
8
|
require_relative 'errors'
|
|
9
9
|
|
|
10
10
|
module Dratools
|
|
11
|
-
# curl
|
|
11
|
+
# curl, wget, aria2c のいずれかを使って URL の確認とダウンロードを行うラッパー。
|
|
12
12
|
#
|
|
13
13
|
# probe は短時間・無出力で済ませ、download は外部コマンドの進捗を端末へ流す。
|
|
14
14
|
# 巨大ファイルを扱うため、download には総時間制限ではなく失速検知を使う。
|
|
15
15
|
class ExternalCommandRunner
|
|
16
16
|
CURL_COMMAND = 'curl'
|
|
17
17
|
WGET_COMMAND = 'wget'
|
|
18
|
-
|
|
18
|
+
ARIA2_COMMAND = 'aria2c'
|
|
19
|
+
AUTO_COMMANDS = [CURL_COMMAND, WGET_COMMAND].freeze
|
|
20
|
+
SUPPORTED_COMMANDS = [*AUTO_COMMANDS, ARIA2_COMMAND].freeze
|
|
21
|
+
|
|
19
22
|
COMMAND_NOT_FOUND_MESSAGE = 'curl または wget が見つかりません'
|
|
23
|
+
PREFERRED_COMMAND_NOT_FOUND_MESSAGE = '指定されたダウンロードコマンドが見つかりません'
|
|
24
|
+
UNSUPPORTED_COMMAND_MESSAGE = '未対応のダウンロードコマンドです'
|
|
20
25
|
DEFAULT_PROBE_TIMEOUT_SECONDS = 5
|
|
21
26
|
PROBE_BYTE_RANGE = '0-0'
|
|
22
27
|
SINGLE_ATTEMPT_COUNT = 1
|
|
23
28
|
|
|
29
|
+
# リダイレクト、HTTP エラー、静かな probe、範囲取得。
|
|
24
30
|
CURL_PROBE_OPTIONS = ['--location', '--fail', '--silent', '--show-error', '--range'].freeze
|
|
31
|
+
# probe 全体のタイムアウト。
|
|
25
32
|
CURL_TIMEOUT_OPTION = '--max-time'
|
|
26
33
|
CURL_CONNECT_TIMEOUT_OPTION = '--connect-timeout'
|
|
34
|
+
# 失速判定に使う最低転送速度。
|
|
27
35
|
CURL_SPEED_LIMIT_OPTION = '--speed-limit'
|
|
36
|
+
# 最低速度を下回ってよい秒数。
|
|
28
37
|
CURL_SPEED_TIME_OPTION = '--speed-time'
|
|
29
38
|
CURL_RETRY_OPTION = '--retry'
|
|
30
39
|
CURL_OUTPUT_OPTION = '--output'
|
|
40
|
+
# 部分ファイルがあれば続きから再開。
|
|
31
41
|
CURL_DOWNLOAD_OPTIONS = ['--location', '--fail', '--continue-at', '-'].freeze
|
|
32
42
|
|
|
43
|
+
# probe でファイルを保存しない。
|
|
33
44
|
WGET_PROBE_OPTIONS = ['--spider'].freeze
|
|
45
|
+
# probe 全体のタイムアウト。
|
|
34
46
|
WGET_TIMEOUT_OPTION = '--timeout'
|
|
35
47
|
WGET_CONNECT_TIMEOUT_OPTION = '--connect-timeout'
|
|
48
|
+
# wget で失速検知に近い意味で使う。
|
|
36
49
|
WGET_READ_TIMEOUT_OPTION = '--read-timeout'
|
|
50
|
+
# probe では 1 回だけにする。
|
|
37
51
|
WGET_TRIES_OPTION = '--tries'
|
|
38
52
|
WGET_WAITRETRY_OPTION = '--waitretry'
|
|
53
|
+
# 部分ファイルがあれば続きから再開。
|
|
39
54
|
WGET_CONTINUE_OPTION = '--continue'
|
|
40
55
|
WGET_OUTPUT_OPTION = '--output-document'
|
|
41
56
|
|
|
42
|
-
|
|
57
|
+
# probe で保存せず、通常出力も抑える。
|
|
58
|
+
ARIA2_PROBE_OPTIONS = ['--dry-run=true', '--quiet=true'].freeze
|
|
59
|
+
ARIA2_CONNECT_TIMEOUT_OPTION = '--connect-timeout'
|
|
60
|
+
# aria2c で失速検知に近い意味で使う。
|
|
61
|
+
ARIA2_TIMEOUT_OPTION = '--timeout'
|
|
62
|
+
# curl の --speed-limit に相当する最低転送速度。
|
|
63
|
+
ARIA2_LOWEST_SPEED_LIMIT_OPTION = '--lowest-speed-limit'
|
|
64
|
+
ARIA2_MAX_TRIES_OPTION = '--max-tries'
|
|
65
|
+
ARIA2_RETRY_WAIT_OPTION = '--retry-wait'
|
|
66
|
+
# 部分ファイルがあれば続きから再開。
|
|
67
|
+
ARIA2_CONTINUE_OPTION = '--continue=true'
|
|
68
|
+
# aria2c は保存先をディレクトリとファイル名に分ける。
|
|
69
|
+
ARIA2_DIR_OPTION = '--dir'
|
|
70
|
+
ARIA2_OUT_OPTION = '--out'
|
|
71
|
+
# 既定では並列取得しない。
|
|
72
|
+
ARIA2_SINGLE_CONNECTION_OPTIONS = ['--split=1', '--max-connection-per-server=1'].freeze
|
|
73
|
+
|
|
74
|
+
def initialize(preferred: Config.download_command)
|
|
43
75
|
@preferred = preferred
|
|
44
76
|
end
|
|
45
77
|
|
|
46
78
|
def available_command
|
|
47
|
-
candidates = [@preferred
|
|
79
|
+
candidates = @preferred ? [@preferred] : AUTO_COMMANDS
|
|
48
80
|
candidates.find { |command_name| executable_command?(command_name) }
|
|
49
81
|
end
|
|
50
82
|
|
|
51
83
|
def probe_url(url, timeout: DEFAULT_PROBE_TIMEOUT_SECONDS)
|
|
52
|
-
tool = available_command || raise(CommandError,
|
|
84
|
+
tool = available_command || raise(CommandError, command_not_found_message)
|
|
53
85
|
# 巨大ファイルを落とさないよう、短時間・最小範囲の確認に留める。
|
|
54
86
|
command =
|
|
55
|
-
|
|
87
|
+
case File.basename(tool)
|
|
88
|
+
when CURL_COMMAND
|
|
89
|
+
# 例: curl --location --fail --silent --show-error --range 0-0
|
|
90
|
+
# --max-time 5 --output /dev/null URL
|
|
56
91
|
[tool, *CURL_PROBE_OPTIONS, PROBE_BYTE_RANGE, CURL_TIMEOUT_OPTION, timeout.to_s,
|
|
57
92
|
CURL_OUTPUT_OPTION, null_device, url]
|
|
58
|
-
|
|
93
|
+
when WGET_COMMAND
|
|
94
|
+
# 例: wget --spider --timeout=5 --tries=1 URL
|
|
59
95
|
[tool, *WGET_PROBE_OPTIONS, "#{WGET_TIMEOUT_OPTION}=#{timeout}",
|
|
60
96
|
"#{WGET_TRIES_OPTION}=#{SINGLE_ATTEMPT_COUNT}", url]
|
|
97
|
+
when ARIA2_COMMAND
|
|
98
|
+
# 例: aria2c --dry-run=true --quiet=true --connect-timeout=5 --timeout=5 --max-tries=1 URL
|
|
99
|
+
[tool, *ARIA2_PROBE_OPTIONS,
|
|
100
|
+
"#{ARIA2_CONNECT_TIMEOUT_OPTION}=#{timeout}",
|
|
101
|
+
"#{ARIA2_TIMEOUT_OPTION}=#{timeout}",
|
|
102
|
+
"#{ARIA2_MAX_TRIES_OPTION}=#{SINGLE_ATTEMPT_COUNT}", url]
|
|
103
|
+
else
|
|
104
|
+
unsupported_command!(tool)
|
|
61
105
|
end
|
|
62
106
|
run_quietly(command)
|
|
63
107
|
end
|
|
64
108
|
|
|
65
109
|
def download_url(url, output_path)
|
|
66
|
-
tool = available_command || raise(CommandError,
|
|
110
|
+
tool = available_command || raise(CommandError, command_not_found_message)
|
|
67
111
|
command =
|
|
68
|
-
|
|
112
|
+
case File.basename(tool)
|
|
113
|
+
when CURL_COMMAND
|
|
114
|
+
# curl の低速検知は「指定秒数のあいだ指定速度を下回ったら失敗」。
|
|
115
|
+
# ネットワークが完全に切れず低速で固まるケースを、総時間制限なしで検出する。
|
|
116
|
+
# 例: curl --location --fail --continue-at - --connect-timeout 30
|
|
117
|
+
# --speed-limit 1024 --speed-time 60 --retry 3 --output OUT URL
|
|
69
118
|
[tool, *CURL_DOWNLOAD_OPTIONS,
|
|
70
119
|
CURL_CONNECT_TIMEOUT_OPTION, Config.download_connect_timeout_seconds.to_s,
|
|
71
120
|
CURL_SPEED_LIMIT_OPTION, Config.download_stall_speed_bytes_per_second.to_s,
|
|
72
121
|
CURL_SPEED_TIME_OPTION, Config.download_stall_timeout_seconds.to_s,
|
|
73
122
|
CURL_RETRY_OPTION, Config.download_retry_count.to_s,
|
|
74
123
|
CURL_OUTPUT_OPTION, output_path, url]
|
|
75
|
-
|
|
124
|
+
when WGET_COMMAND
|
|
125
|
+
# wget では --read-timeout を失速検知に近い意味で使う。
|
|
126
|
+
# --continue は部分ファイルの続きから再開し、--output-document は保存先を固定する。
|
|
127
|
+
# 例: wget --continue --connect-timeout=30 --read-timeout=60
|
|
128
|
+
# --tries=4 --waitretry=5 --output-document OUT URL
|
|
76
129
|
[tool, WGET_CONTINUE_OPTION,
|
|
77
130
|
"#{WGET_CONNECT_TIMEOUT_OPTION}=#{Config.download_connect_timeout_seconds}",
|
|
78
131
|
"#{WGET_READ_TIMEOUT_OPTION}=#{Config.download_stall_timeout_seconds}",
|
|
79
|
-
"#{WGET_TRIES_OPTION}=#{
|
|
132
|
+
"#{WGET_TRIES_OPTION}=#{download_attempt_count}",
|
|
80
133
|
"#{WGET_WAITRETRY_OPTION}=#{Config.download_retry_wait_seconds}",
|
|
81
134
|
WGET_OUTPUT_OPTION, output_path, url]
|
|
135
|
+
when ARIA2_COMMAND
|
|
136
|
+
# aria2c は保存先をディレクトリとファイル名に分けて指定する。
|
|
137
|
+
# --continue=true は部分ファイルがあれば続きから再開する。
|
|
138
|
+
# 例: aria2c --continue=true --split=1 --max-connection-per-server=1
|
|
139
|
+
# --connect-timeout=30 --timeout=60 --lowest-speed-limit=1024
|
|
140
|
+
# --max-tries=4 --retry-wait=5 --dir DIR --out FILE URL
|
|
141
|
+
[tool, ARIA2_CONTINUE_OPTION, *ARIA2_SINGLE_CONNECTION_OPTIONS,
|
|
142
|
+
"#{ARIA2_CONNECT_TIMEOUT_OPTION}=#{Config.download_connect_timeout_seconds}",
|
|
143
|
+
"#{ARIA2_TIMEOUT_OPTION}=#{Config.download_stall_timeout_seconds}",
|
|
144
|
+
"#{ARIA2_LOWEST_SPEED_LIMIT_OPTION}=#{Config.download_stall_speed_bytes_per_second}",
|
|
145
|
+
"#{ARIA2_MAX_TRIES_OPTION}=#{download_attempt_count}",
|
|
146
|
+
"#{ARIA2_RETRY_WAIT_OPTION}=#{Config.download_retry_wait_seconds}",
|
|
147
|
+
"#{ARIA2_DIR_OPTION}=#{File.dirname(output_path)}",
|
|
148
|
+
"#{ARIA2_OUT_OPTION}=#{File.basename(output_path)}", url]
|
|
149
|
+
else
|
|
150
|
+
unsupported_command!(tool)
|
|
82
151
|
end
|
|
83
152
|
run_streaming(command)
|
|
84
153
|
end
|
|
@@ -93,7 +162,7 @@ module Dratools
|
|
|
93
162
|
end
|
|
94
163
|
|
|
95
164
|
def run_streaming(command)
|
|
96
|
-
#
|
|
165
|
+
# 配列形式で渡すことでシェルを介さず、外部コマンドの stderr 進捗はそのまま見せる。
|
|
97
166
|
return true if system(*command)
|
|
98
167
|
|
|
99
168
|
status = $CHILD_STATUS
|
|
@@ -108,6 +177,20 @@ module Dratools
|
|
|
108
177
|
end
|
|
109
178
|
end
|
|
110
179
|
|
|
180
|
+
def command_not_found_message
|
|
181
|
+
return "#{PREFERRED_COMMAND_NOT_FOUND_MESSAGE}: #{@preferred}" if @preferred
|
|
182
|
+
|
|
183
|
+
COMMAND_NOT_FOUND_MESSAGE
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def download_attempt_count
|
|
187
|
+
Config.download_retry_count + 1
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def unsupported_command!(tool)
|
|
191
|
+
raise CommandError, "#{UNSUPPORTED_COMMAND_MESSAGE}: #{tool}"
|
|
192
|
+
end
|
|
193
|
+
|
|
111
194
|
def null_device
|
|
112
195
|
File::NULL
|
|
113
196
|
end
|
|
@@ -10,7 +10,6 @@ require_relative 'traversal_node'
|
|
|
10
10
|
module Dratools
|
|
11
11
|
# BioProject などの上位レコードから DDBJ sra-run レコードを集める。
|
|
12
12
|
class RunRecordCollector
|
|
13
|
-
XREF_URL_PATTERN = %r{/(?:resource|search/entry)/([^/]+)/([^/?#.]+)}
|
|
14
13
|
TRAVERSABLE_XREF_TYPES = [
|
|
15
14
|
DdbjRecordFields::SRA_RUN_RESOURCE_TYPE,
|
|
16
15
|
DdbjRecordFields::SRA_EXPERIMENT_RESOURCE_TYPE,
|
|
@@ -40,14 +39,26 @@ module Dratools
|
|
|
40
39
|
return node
|
|
41
40
|
end
|
|
42
41
|
|
|
43
|
-
direct_children =
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
direct_children = explore_run_xrefs(
|
|
43
|
+
run_xrefs,
|
|
44
|
+
seen_keys,
|
|
45
|
+
tolerant: tolerant,
|
|
46
|
+
direct_run_fetch_limit: direct_run_fetch_limit
|
|
47
|
+
)
|
|
46
48
|
if direct_children.any? { |child| child.run? || child.run_records.any? }
|
|
47
49
|
node.children.concat(direct_children)
|
|
48
50
|
return node
|
|
49
51
|
end
|
|
50
52
|
|
|
53
|
+
node.children.concat(
|
|
54
|
+
recursive_children(ddbj_record, xrefs, seen_keys, tolerant, direct_run_fetch_limit)
|
|
55
|
+
)
|
|
56
|
+
node
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def recursive_children(ddbj_record, xrefs, seen_keys, tolerant, direct_run_fetch_limit)
|
|
51
62
|
recursive_xrefs = xrefs.select { |xref| traversable_xref?(xref) }
|
|
52
63
|
validate_recursive_non_run_xref_count!(ddbj_record, recursive_xrefs)
|
|
53
64
|
db_xref_edges = explore_edges(
|
|
@@ -64,12 +75,9 @@ module Dratools
|
|
|
64
75
|
tolerant: tolerant,
|
|
65
76
|
direct_run_fetch_limit: direct_run_fetch_limit
|
|
66
77
|
)
|
|
67
|
-
|
|
68
|
-
node
|
|
78
|
+
db_xref_edges + child_edges
|
|
69
79
|
end
|
|
70
80
|
|
|
71
|
-
private
|
|
72
|
-
|
|
73
81
|
def lightweight_direct_run_nodes(run_xrefs, direct_run_fetch_limit)
|
|
74
82
|
return nil unless direct_run_fetch_limit && run_xrefs.length > direct_run_fetch_limit
|
|
75
83
|
|
|
@@ -111,6 +119,53 @@ module Dratools
|
|
|
111
119
|
end
|
|
112
120
|
end
|
|
113
121
|
|
|
122
|
+
def explore_run_xrefs(run_xrefs, seen_keys, tolerant:, direct_run_fetch_limit:)
|
|
123
|
+
fetchable_xrefs = unseen_fetchable_xrefs(run_xrefs, seen_keys)
|
|
124
|
+
return [] if fetchable_xrefs.empty?
|
|
125
|
+
|
|
126
|
+
accessions = fetchable_xrefs.map { |xref| xref_accession(xref) }
|
|
127
|
+
records = @client.fetch_resource_records_bulk(
|
|
128
|
+
DdbjRecordFields::SRA_RUN_RESOURCE_TYPE,
|
|
129
|
+
accessions,
|
|
130
|
+
include_db_xrefs: false
|
|
131
|
+
)
|
|
132
|
+
fetchable_xrefs.map do |xref|
|
|
133
|
+
accession = xref_accession(xref)
|
|
134
|
+
if (record = records[accession])
|
|
135
|
+
explore(
|
|
136
|
+
record,
|
|
137
|
+
seen_keys: seen_keys,
|
|
138
|
+
relation: TraversalNode::DB_XREF_RELATION,
|
|
139
|
+
tolerant: tolerant,
|
|
140
|
+
direct_run_fetch_limit: direct_run_fetch_limit
|
|
141
|
+
)
|
|
142
|
+
elsif tolerant
|
|
143
|
+
node_from_xref(
|
|
144
|
+
xref,
|
|
145
|
+
relation: TraversalNode::DB_XREF_RELATION,
|
|
146
|
+
error: "not found: #{accession}"
|
|
147
|
+
)
|
|
148
|
+
else
|
|
149
|
+
raise NotFoundError, "not found: sra-run/#{accession}"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def unseen_fetchable_xrefs(xrefs, seen_keys)
|
|
155
|
+
xrefs.each_with_object([]) do |xref, selected|
|
|
156
|
+
next unless traversable_xref?(xref)
|
|
157
|
+
|
|
158
|
+
accession = xref_accession(xref)
|
|
159
|
+
next if accession.empty?
|
|
160
|
+
|
|
161
|
+
reference_key = xref_key(xref)
|
|
162
|
+
next if reference_key.empty? || seen_keys.include?(reference_key)
|
|
163
|
+
|
|
164
|
+
seen_keys.add(reference_key)
|
|
165
|
+
selected << xref
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
114
169
|
def explore_xref(xref, relation, seen_keys, tolerant:, direct_run_fetch_limit:)
|
|
115
170
|
linked_record = fetch_xref_record(xref)
|
|
116
171
|
explore(
|
|
@@ -127,20 +182,17 @@ module Dratools
|
|
|
127
182
|
end
|
|
128
183
|
|
|
129
184
|
def xref_key(xref)
|
|
130
|
-
|
|
131
|
-
|
|
185
|
+
xref_accession(xref)
|
|
186
|
+
end
|
|
132
187
|
|
|
188
|
+
def xref_accession(xref)
|
|
133
189
|
(xref[DdbjRecordFields::ID_KEY] || xref[DdbjRecordFields::IDENTIFIER_KEY]).to_s
|
|
134
190
|
end
|
|
135
191
|
|
|
136
192
|
def fetch_xref_record(xref)
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
fetch_xref_by_identifier(xref)
|
|
141
|
-
else
|
|
142
|
-
raise InvalidRecordError, 'sra-run xref has no URL or id'
|
|
143
|
-
end
|
|
193
|
+
raise InvalidRecordError, 'xref has no identifier' if xref_accession(xref).empty?
|
|
194
|
+
|
|
195
|
+
fetch_xref_by_identifier(xref)
|
|
144
196
|
end
|
|
145
197
|
|
|
146
198
|
def fetch_xref_by_identifier(xref)
|
|
@@ -160,15 +212,13 @@ module Dratools
|
|
|
160
212
|
|
|
161
213
|
def run_record?(ddbj_record)
|
|
162
214
|
record_type = ddbj_record[DdbjRecordFields::TYPE_KEY]
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
ddbj_record[DdbjRecordFields::DOWNLOAD_URL_KEY].is_a?(Array)
|
|
215
|
+
record_type == DdbjRecordFields::SRA_RUN_RESOURCE_TYPE
|
|
166
216
|
end
|
|
167
217
|
|
|
168
218
|
def node_from_record(ddbj_record, relation:)
|
|
169
219
|
TraversalNode.new(
|
|
170
220
|
relation: relation,
|
|
171
|
-
type: ddbj_record[DdbjRecordFields::TYPE_KEY]
|
|
221
|
+
type: ddbj_record[DdbjRecordFields::TYPE_KEY],
|
|
172
222
|
accession: record_accession(ddbj_record),
|
|
173
223
|
object_type: ddbj_record['objectType'],
|
|
174
224
|
record: run_record?(ddbj_record) ? ddbj_record : nil
|
|
@@ -190,9 +240,5 @@ module Dratools
|
|
|
190
240
|
ddbj_record[DdbjRecordFields::ID_KEY] ||
|
|
191
241
|
ddbj_record[DdbjRecordFields::PRIMARY_ID_KEY]
|
|
192
242
|
end
|
|
193
|
-
|
|
194
|
-
def inferred_record_type(ddbj_record)
|
|
195
|
-
DdbjRecordFields::SRA_RUN_RESOURCE_TYPE if run_record?(ddbj_record)
|
|
196
|
-
end
|
|
197
243
|
end
|
|
198
244
|
end
|
data/lib/dratools/version.rb
CHANGED