togows 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +19 -0
- data/docs/user-guide.md +171 -0
- data/exe/togows +6 -0
- data/lib/togows/cli.rb +343 -0
- data/lib/togows/client.rb +65 -0
- data/lib/togows/color.rb +28 -0
- data/lib/togows/error.rb +16 -0
- data/lib/togows/help.rb +92 -0
- data/lib/togows/url_builder.rb +78 -0
- data/lib/togows/version.rb +5 -0
- data/lib/togows.rb +9 -0
- metadata +58 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3aa356b44bbb2bf65b5f57312ed7134c221480b1b0f21727310756f5f8f1aa05
|
|
4
|
+
data.tar.gz: 222d0166a0d845fae0dca15a94a178b9ff6f2f3ef0329331390f692f32e85c49
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 986782292f69d0fe7db251001108570a20e4502ad4e0bc0e4865443f233ea536c4f72d23f1c64176d04eee3b12f78a322748d153867ff0c6dd96a616ee9a8b86
|
|
7
|
+
data.tar.gz: 38199b730174c357333a5ae268529add28e289888ade3d384286c69f60d9eaec718661addb336e4a2f6c5cabad9df4f87b8257e830a24e0b18932bde4d1252af
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kojix2
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# togows
|
|
2
|
+
|
|
3
|
+
A small dependency-free Ruby command line client for the TogoWS REST API.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
gem install togows
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
togows databases
|
|
11
|
+
togows entry pubmed 20472643 authors --format json
|
|
12
|
+
togows search uniprot "FGF19" --limit 10
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
See the [user guide](docs/user-guide.md) for installation from source, command examples, exit status, and testing notes.
|
|
16
|
+
|
|
17
|
+
## License
|
|
18
|
+
|
|
19
|
+
MIT
|
data/docs/user-guide.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# User Guide
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
Install the released gem:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
gem install togows
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or build it from this repository:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
gem build togows.gemspec
|
|
15
|
+
gem install ./togows-*.gem
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
togows entry DATABASE ID[,ID...] [FIELD] [options]
|
|
22
|
+
togows search DATABASE QUERY [options]
|
|
23
|
+
togows convert SOURCE.FORMAT [FILE] [options]
|
|
24
|
+
togows databases [entry|search] [options]
|
|
25
|
+
togows ucsc [DATABASE] [TABLE] [QUERY] [options]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Run `togows help COMMAND` for command-specific examples:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
togows help entry
|
|
32
|
+
togows help search
|
|
33
|
+
togows help convert
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Discover Databases
|
|
37
|
+
|
|
38
|
+
TogoWS database names must be exact. Start here when you are unsure:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
togows databases
|
|
42
|
+
togows databases entry
|
|
43
|
+
togows databases search
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Entry
|
|
47
|
+
|
|
48
|
+
Fetch one entry:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
togows entry nucleotide NC_001138
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Fetch a field:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
togows entry pubmed 20472643 authors --format json
|
|
58
|
+
togows entry uniprot ACT_YEAST seq
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use comma-separated IDs for multiple entries:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
togows entry pubmed 20472643,20472644 title
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Search
|
|
68
|
+
|
|
69
|
+
Count search results:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
togows search pubmed "lung cancer" --count
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Fetch the first page of search results:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
togows search uniprot "FGF19" --limit 10
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Specify both offset and limit when you need a later page:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
togows search uniprot "lung cancer" --offset 11 --limit 10
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Convert
|
|
88
|
+
|
|
89
|
+
Convert a local file:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
{
|
|
93
|
+
printf 'LOCUS TEST 12 bp DNA linear UNA 01-JAN-2000\n'
|
|
94
|
+
printf 'FEATURES Location/Qualifiers\n'
|
|
95
|
+
printf 'ORIGIN\n'
|
|
96
|
+
printf ' 1 acgtacgtacgt\n'
|
|
97
|
+
printf '//\n'
|
|
98
|
+
} > input.gb
|
|
99
|
+
|
|
100
|
+
togows convert genbank.fasta input.gb
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Convert from stdin:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
{
|
|
107
|
+
printf 'LOCUS TEST 12 bp DNA linear UNA 01-JAN-2000\n'
|
|
108
|
+
printf 'FEATURES Location/Qualifiers\n'
|
|
109
|
+
printf 'ORIGIN\n'
|
|
110
|
+
printf ' 1 acgtacgtacgt\n'
|
|
111
|
+
printf '//\n'
|
|
112
|
+
} | togows convert genbank.fasta
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## UCSC
|
|
116
|
+
|
|
117
|
+
List UCSC genome databases or tables:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
togows ucsc
|
|
121
|
+
togows ucsc hg38
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Query a table:
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
togows ucsc hg38 refGene name2=UVSSA
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Useful Options
|
|
131
|
+
|
|
132
|
+
Print the constructed URL without making a request:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
togows entry pubmed 20472643 authors --format json --raw-url
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Page long output explicitly:
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
togows ucsc hg38 --pager
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Exit Status
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
0 success
|
|
148
|
+
2 usage or option error
|
|
149
|
+
4 HTTP client error from TogoWS
|
|
150
|
+
5 HTTP server error from TogoWS
|
|
151
|
+
6 network, socket, or timeout error
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Testing
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
bundle exec rake test
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Live tests are skipped by default. To make real requests to TogoWS:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
TOGOWS_LIVE_TEST=1 bundle exec rake test
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Release
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
gem build togows.gemspec
|
|
170
|
+
gem push togows-*.gem
|
|
171
|
+
```
|
data/exe/togows
ADDED
data/lib/togows/cli.rb
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "shellwords"
|
|
6
|
+
require "timeout"
|
|
7
|
+
|
|
8
|
+
module TogoWS
|
|
9
|
+
class CLI
|
|
10
|
+
DEFAULT_BASE_URL = "https://togows.org"
|
|
11
|
+
DEFAULT_TIMEOUT = 30
|
|
12
|
+
|
|
13
|
+
def initialize(argv, stdin, stdout, stderr)
|
|
14
|
+
@argv = argv.dup
|
|
15
|
+
@stdin = stdin
|
|
16
|
+
@stdout = stdout
|
|
17
|
+
@stderr = stderr
|
|
18
|
+
@out_color = Color.new(Color.enabled_for?(stdout))
|
|
19
|
+
@err_color = Color.new(Color.enabled_for?(stderr))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run
|
|
23
|
+
command = @argv.shift
|
|
24
|
+
return help_for(@argv.shift, 0) if ["help", "-h", "--help"].include?(command)
|
|
25
|
+
return help(nil, 0) if command.nil?
|
|
26
|
+
return version if ["version", "--version", "-v"].include?(command)
|
|
27
|
+
|
|
28
|
+
case command
|
|
29
|
+
when "entry" then run_entry
|
|
30
|
+
when "search" then run_search
|
|
31
|
+
when "convert" then run_convert
|
|
32
|
+
when "databases", "dbs" then run_databases
|
|
33
|
+
when "ucsc" then run_ucsc
|
|
34
|
+
else
|
|
35
|
+
error("unknown command: #{command}")
|
|
36
|
+
help(nil, 2)
|
|
37
|
+
end
|
|
38
|
+
rescue HTTPError => e
|
|
39
|
+
error("#{e.code} #{e.message_text}")
|
|
40
|
+
@stderr.puts e.body unless e.body.empty?
|
|
41
|
+
e.code >= 500 ? 5 : 4
|
|
42
|
+
rescue OptionParser::ParseError, Error => e
|
|
43
|
+
error(e.message)
|
|
44
|
+
2
|
|
45
|
+
rescue SystemCallError, SocketError, Timeout::Error => e
|
|
46
|
+
error("#{e.class}: #{e.message}")
|
|
47
|
+
6
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def common_options
|
|
53
|
+
{
|
|
54
|
+
base_url: DEFAULT_BASE_URL,
|
|
55
|
+
timeout: DEFAULT_TIMEOUT,
|
|
56
|
+
format: nil,
|
|
57
|
+
raw_url: false,
|
|
58
|
+
pager: false,
|
|
59
|
+
pretty: false
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def parser(options, banner)
|
|
64
|
+
OptionParser.new do |opts|
|
|
65
|
+
opts.banner = banner
|
|
66
|
+
opts.on("-f", "--format FORMAT", "Output format") { |v| options[:format] = v }
|
|
67
|
+
opts.on("--base-url URL", "Base URL") { |v| options[:base_url] = v }
|
|
68
|
+
opts.on("--timeout SEC", Integer, "Timeout seconds") { |v| options[:timeout] = v }
|
|
69
|
+
opts.on("--raw-url", "Print URL and exit") { options[:raw_url] = true }
|
|
70
|
+
opts.on("--pager", "Page output with PAGER when stdout is a terminal") { options[:pager] = true }
|
|
71
|
+
opts.on("--pretty", "Pretty-print JSON output") { options[:pretty] = true }
|
|
72
|
+
opts.on("-h", "--help", "Show help") do
|
|
73
|
+
@stdout.puts opts
|
|
74
|
+
throw :help
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def run_entry
|
|
80
|
+
options = common_options
|
|
81
|
+
catch(:help) do
|
|
82
|
+
parser(options, "Usage: togows entry DATABASE ID[,ID...] [FIELD] [options]").parse!(@argv)
|
|
83
|
+
database, *rest = @argv
|
|
84
|
+
return command_help(:entry, "entry requires DATABASE and ID", 2) if database.nil? || rest.empty?
|
|
85
|
+
|
|
86
|
+
ids, field = parse_entry_arguments(rest)
|
|
87
|
+
path = URLBuilder.entry(database, ids, field, options[:format])
|
|
88
|
+
output_request(path, options, :get)
|
|
89
|
+
end || 0
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def parse_entry_arguments(args)
|
|
93
|
+
raise Error, "entry accepts ID[,ID...] and optional FIELD; use commas for multiple IDs" if args.length > 2
|
|
94
|
+
|
|
95
|
+
ids = args.first.to_s.split(",")
|
|
96
|
+
field = args[1]
|
|
97
|
+
raise Error, "entry accepts comma-separated IDs, not space-separated IDs" if field && entry_id_like?(field)
|
|
98
|
+
|
|
99
|
+
[ids, field]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def entry_id_like?(value)
|
|
103
|
+
value.include?(",") || value.match?(/\A[A-Z0-9_]+(?:\.\d+)?\z/)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def run_search
|
|
107
|
+
options = common_options.merge(offset: nil, limit: nil, count: false)
|
|
108
|
+
catch(:help) do
|
|
109
|
+
p = parser(options, "Usage: togows search DATABASE QUERY [options]")
|
|
110
|
+
p.on("-o", "--offset N", Integer, "Result offset") { |v| options[:offset] = v }
|
|
111
|
+
p.on("-n", "--limit N", Integer, "Result limit") { |v| options[:limit] = v }
|
|
112
|
+
p.on("--count", "Count results") { options[:count] = true }
|
|
113
|
+
p.parse!(@argv)
|
|
114
|
+
database, query = @argv
|
|
115
|
+
return command_help(:search, "search requires DATABASE and QUERY", 2) if database.nil?
|
|
116
|
+
return command_help(:search, "search requires QUERY after DATABASE", 2) if query.nil?
|
|
117
|
+
|
|
118
|
+
validate_search_options(options)
|
|
119
|
+
options[:offset] = 1 if options[:limit] && !options[:offset]
|
|
120
|
+
|
|
121
|
+
path = URLBuilder.search(database, query, options[:offset], options[:limit], options[:count], options[:format])
|
|
122
|
+
output_request(path, options, :get)
|
|
123
|
+
end || 0
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def validate_search_options(options)
|
|
127
|
+
if options[:count] && (options[:offset] || options[:limit])
|
|
128
|
+
raise Error, "--count cannot be used with --offset or --limit"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
raise Error, "--offset and --limit must be used together" if options[:offset] && !options[:limit]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def run_convert
|
|
135
|
+
options = common_options.merge(to: nil)
|
|
136
|
+
catch(:help) do
|
|
137
|
+
p = parser(options, "Usage: togows convert SOURCE.FORMAT [FILE] [options]")
|
|
138
|
+
p.on("--to FORMAT", "Target format") { |v| options[:to] = v }
|
|
139
|
+
p.parse!(@argv)
|
|
140
|
+
source, file = @argv
|
|
141
|
+
return command_help(:convert, "convert requires SOURCE.FORMAT or SOURCE --to FORMAT", 2) if source.nil?
|
|
142
|
+
|
|
143
|
+
path = URLBuilder.convert(source, options[:to])
|
|
144
|
+
body = file ? File.read(file) : read_convert_stdin
|
|
145
|
+
output_request(path, options, :post, body)
|
|
146
|
+
end || 0
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def read_convert_stdin
|
|
150
|
+
raise Error, "convert requires FILE or piped stdin; stdin is a terminal" if @stdin.tty?
|
|
151
|
+
|
|
152
|
+
@stdin.read
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def run_ucsc
|
|
156
|
+
options = common_options.merge(offset: nil, limit: nil)
|
|
157
|
+
catch(:help) do
|
|
158
|
+
p = parser(options, "Usage: togows ucsc [DATABASE] [TABLE] [QUERY] [options]")
|
|
159
|
+
p.on("-o", "--offset N", Integer, "Result offset") { |v| options[:offset] = v }
|
|
160
|
+
p.on("-n", "--limit N", Integer, "Result limit") { |v| options[:limit] = v }
|
|
161
|
+
p.parse!(@argv)
|
|
162
|
+
raise Error, "--offset and --limit must be used together" if options[:offset] && !options[:limit]
|
|
163
|
+
raise Error, "--offset and --limit must be used together" if options[:limit] && !options[:offset]
|
|
164
|
+
|
|
165
|
+
database, table, *query_parts = @argv
|
|
166
|
+
query = query_parts.empty? ? nil : query_parts.join("/")
|
|
167
|
+
path = URLBuilder.ucsc(database, table, query, options[:offset], options[:limit], options[:format])
|
|
168
|
+
output_request(path, options, :get)
|
|
169
|
+
end || 0
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def run_databases
|
|
173
|
+
options = common_options
|
|
174
|
+
catch(:help) do
|
|
175
|
+
parser(options, "Usage: togows databases [entry|search] [options]").parse!(@argv)
|
|
176
|
+
kind = @argv.shift
|
|
177
|
+
raise Error, "databases accepts only one argument: entry or search" unless @argv.empty?
|
|
178
|
+
|
|
179
|
+
kinds = if kind.nil?
|
|
180
|
+
%w[entry search]
|
|
181
|
+
elsif %w[entry search].include?(kind)
|
|
182
|
+
[kind]
|
|
183
|
+
else
|
|
184
|
+
return command_help(:databases, "database list must be entry or search", 2)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
output_database_lists(kinds, options)
|
|
188
|
+
end || 0
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def output_request(path, options, method, body = nil)
|
|
192
|
+
if options[:raw_url]
|
|
193
|
+
@stdout.puts URLBuilder.full_url(options[:base_url], path)
|
|
194
|
+
return 0
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
client = Client.new(options[:base_url], options[:timeout])
|
|
198
|
+
response = with_status("Fetching...") do
|
|
199
|
+
(method == :post ? client.post(path, body) : client.get(path)).to_s
|
|
200
|
+
end
|
|
201
|
+
write_output(format_response(response, options), options)
|
|
202
|
+
0
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def output_database_lists(kinds, options)
|
|
206
|
+
if options[:raw_url]
|
|
207
|
+
kinds.each { |kind| @stdout.puts URLBuilder.full_url(options[:base_url], URLBuilder.databases(kind)) }
|
|
208
|
+
return 0
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
client = Client.new(options[:base_url], options[:timeout])
|
|
212
|
+
responses = with_status("Fetching...") do
|
|
213
|
+
kinds.map { |kind| [kind, client.get(URLBuilder.databases(kind))] }
|
|
214
|
+
end
|
|
215
|
+
responses.each_with_index do |(kind, body), index|
|
|
216
|
+
@stdout.puts if index.positive?
|
|
217
|
+
@stdout.puts "#{kind}:"
|
|
218
|
+
@stdout.write(body)
|
|
219
|
+
end
|
|
220
|
+
@stdout.write("\n")
|
|
221
|
+
0
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def format_response(response, options)
|
|
225
|
+
return response unless options[:pretty]
|
|
226
|
+
|
|
227
|
+
JSON.pretty_generate(JSON.parse(response))
|
|
228
|
+
rescue JSON::ParserError
|
|
229
|
+
response
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def version
|
|
233
|
+
@stdout.puts "togows #{VERSION}"
|
|
234
|
+
0
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def help(message, status)
|
|
238
|
+
@stderr.puts message if message
|
|
239
|
+
@stdout.puts color_help(<<~HELP)
|
|
240
|
+
Usage:
|
|
241
|
+
togows entry DATABASE ID[,ID...] [FIELD] [options]
|
|
242
|
+
togows search DATABASE QUERY [options]
|
|
243
|
+
togows convert SOURCE.FORMAT [FILE] [options]
|
|
244
|
+
togows databases [entry|search] [options]
|
|
245
|
+
togows ucsc [DATABASE] [TABLE] [QUERY] [options]
|
|
246
|
+
|
|
247
|
+
Commands:
|
|
248
|
+
entry Retrieve database entries
|
|
249
|
+
search Search database entries
|
|
250
|
+
convert Convert data from stdin or FILE
|
|
251
|
+
databases List databases supported by TogoWS
|
|
252
|
+
ucsc Access TogoWS UCSC API
|
|
253
|
+
|
|
254
|
+
Common options:
|
|
255
|
+
-f, --format FORMAT
|
|
256
|
+
--base-url URL
|
|
257
|
+
--timeout SEC
|
|
258
|
+
--raw-url
|
|
259
|
+
--pager
|
|
260
|
+
--pretty
|
|
261
|
+
-h, --help
|
|
262
|
+
HELP
|
|
263
|
+
status
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def help_for(command, status)
|
|
267
|
+
return help(nil, status) if command.nil?
|
|
268
|
+
|
|
269
|
+
case command
|
|
270
|
+
when "entry" then command_help(:entry, nil, status)
|
|
271
|
+
when "search" then command_help(:search, nil, status)
|
|
272
|
+
when "convert" then command_help(:convert, nil, status)
|
|
273
|
+
when "databases", "dbs" then command_help(:databases, nil, status)
|
|
274
|
+
when "ucsc" then command_help(:ucsc, nil, status)
|
|
275
|
+
else
|
|
276
|
+
error("unknown command: #{command}")
|
|
277
|
+
help(nil, 2)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def command_help(command, message, status)
|
|
282
|
+
error(message) if message
|
|
283
|
+
@stdout.puts color_help(Help::COMMANDS.fetch(command))
|
|
284
|
+
status
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def error(message)
|
|
288
|
+
@stderr.puts "#{@err_color.bold('togows:')} #{message}"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def write_output(response, options)
|
|
292
|
+
output = response.end_with?("\n") ? response : "#{response}\n"
|
|
293
|
+
return @stdout.write(output) unless options[:pager] && @stdout.respond_to?(:tty?) && @stdout.tty?
|
|
294
|
+
|
|
295
|
+
command = pager_command
|
|
296
|
+
return @stdout.write(output) if command.empty?
|
|
297
|
+
|
|
298
|
+
write_pager(command, output)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def pager_command
|
|
302
|
+
ENV.fetch("PAGER", "less -R").shellsplit
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def write_pager(command, output)
|
|
306
|
+
IO.popen(command, "w") { |pager| pager.write(output) }
|
|
307
|
+
rescue Errno::EPIPE
|
|
308
|
+
nil
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def with_status(message)
|
|
312
|
+
return yield unless @stderr.respond_to?(:tty?) && @stderr.tty?
|
|
313
|
+
|
|
314
|
+
@stderr.print "\r#{message}"
|
|
315
|
+
yield
|
|
316
|
+
ensure
|
|
317
|
+
clear_status(message) if @stderr.respond_to?(:tty?) && @stderr.tty?
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def clear_status(message)
|
|
321
|
+
@stderr.print "\r#{' ' * message.length}\r"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def color_help(text)
|
|
325
|
+
highlighted = text.gsub(/^(Usage:)/) { @out_color.bold(Regexp.last_match(1)) }
|
|
326
|
+
color_help_headings(highlighted)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def color_help_headings(text)
|
|
330
|
+
headings = [
|
|
331
|
+
"Next arguments:",
|
|
332
|
+
"Common search databases:",
|
|
333
|
+
"Search options:",
|
|
334
|
+
"Convert options:",
|
|
335
|
+
"Examples:",
|
|
336
|
+
"Commands:",
|
|
337
|
+
"Common options:"
|
|
338
|
+
]
|
|
339
|
+
pattern = /^(#{Regexp.union(headings)})/
|
|
340
|
+
text.gsub(pattern) { @out_color.bold(Regexp.last_match(1)) }
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module TogoWS
|
|
7
|
+
class Client
|
|
8
|
+
MAX_REDIRECTS = 5
|
|
9
|
+
USER_AGENT = "togows/#{VERSION}".freeze
|
|
10
|
+
|
|
11
|
+
def initialize(base_url, timeout)
|
|
12
|
+
@base_url = base_url
|
|
13
|
+
@timeout = timeout
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get(path)
|
|
17
|
+
uri = URI.parse(URLBuilder.full_url(@base_url, path))
|
|
18
|
+
request(uri) do |http, request_uri|
|
|
19
|
+
req = Net::HTTP::Get.new(request_uri)
|
|
20
|
+
apply_headers(req)
|
|
21
|
+
http.request(req)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def post(path, body)
|
|
26
|
+
uri = URI.parse(URLBuilder.full_url(@base_url, path))
|
|
27
|
+
request(uri) do |http, request_uri|
|
|
28
|
+
req = Net::HTTP::Post.new(request_uri)
|
|
29
|
+
apply_headers(req)
|
|
30
|
+
req["Content-Type"] = "text/plain"
|
|
31
|
+
req.body = body
|
|
32
|
+
http.request(req)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def request(uri, redirects = 0, &)
|
|
39
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
40
|
+
http.use_ssl = uri.scheme == "https"
|
|
41
|
+
http.open_timeout = @timeout
|
|
42
|
+
http.read_timeout = @timeout
|
|
43
|
+
|
|
44
|
+
response = http.start { |h| yield h, uri.request_uri }
|
|
45
|
+
if response.is_a?(Net::HTTPRedirection) && redirects < MAX_REDIRECTS
|
|
46
|
+
return request(redirect_uri(uri, response), redirects + 1, &)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
raise HTTPError.new(response.code, response.message, response.body) unless response.is_a?(Net::HTTPSuccess)
|
|
50
|
+
|
|
51
|
+
response.body
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def redirect_uri(uri, response)
|
|
55
|
+
location = response["location"]
|
|
56
|
+
raise HTTPError.new(response.code, response.message, response.body) if location.nil? || location.empty?
|
|
57
|
+
|
|
58
|
+
uri + location
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def apply_headers(request)
|
|
62
|
+
request["User-Agent"] = USER_AGENT
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/togows/color.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TogoWS
|
|
4
|
+
class Color
|
|
5
|
+
STYLES = {
|
|
6
|
+
bold: 1
|
|
7
|
+
}.freeze
|
|
8
|
+
|
|
9
|
+
def initialize(enabled)
|
|
10
|
+
@enabled = enabled
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.enabled_for?(io)
|
|
14
|
+
io.respond_to?(:tty?) && io.tty? && !ENV.key?("NO_COLOR")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def apply(text, *styles)
|
|
18
|
+
return text unless @enabled
|
|
19
|
+
|
|
20
|
+
codes = styles.map { |style| STYLES.fetch(style) }
|
|
21
|
+
"\e[#{codes.join(';')}m#{text}\e[0m"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def bold(text)
|
|
25
|
+
apply(text, :bold)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/togows/error.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TogoWS
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class HTTPError < Error
|
|
7
|
+
attr_reader :code, :message_text, :body
|
|
8
|
+
|
|
9
|
+
def initialize(code, message_text, body)
|
|
10
|
+
@code = code.to_i
|
|
11
|
+
@message_text = message_text
|
|
12
|
+
@body = body.to_s
|
|
13
|
+
super("#{code} #{message_text}")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/togows/help.rb
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TogoWS
|
|
4
|
+
module Help
|
|
5
|
+
COMMANDS = {
|
|
6
|
+
entry: <<~HELP,
|
|
7
|
+
Usage: togows entry DATABASE ID[,ID...] [FIELD] [options]
|
|
8
|
+
|
|
9
|
+
Next arguments:
|
|
10
|
+
DATABASE Database name such as pubmed, nucleotide, uniprot
|
|
11
|
+
ID[,ID...] One ID or comma-separated IDs
|
|
12
|
+
FIELD Optional field such as authors, title, seq, definition
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
togows entry pubmed 20472643 authors --format json
|
|
16
|
+
togows entry uniprot ACT_YEAST seq
|
|
17
|
+
togows entry nucleotide NC_001138
|
|
18
|
+
HELP
|
|
19
|
+
search: <<~HELP,
|
|
20
|
+
Usage: togows search DATABASE QUERY [options]
|
|
21
|
+
|
|
22
|
+
Next arguments:
|
|
23
|
+
DATABASE Search target. Run `togows databases search` to list choices.
|
|
24
|
+
QUERY Search text or database query
|
|
25
|
+
|
|
26
|
+
Common search databases:
|
|
27
|
+
pubmed Literature
|
|
28
|
+
uniprot Protein entries
|
|
29
|
+
nucleotide Nucleotide entries
|
|
30
|
+
protein Protein sequences
|
|
31
|
+
gene NCBI Gene
|
|
32
|
+
pathway KEGG pathways
|
|
33
|
+
|
|
34
|
+
Search options:
|
|
35
|
+
-o, --offset N
|
|
36
|
+
-n, --limit N Defaults to offset 1 when --offset is omitted
|
|
37
|
+
--count
|
|
38
|
+
--pager
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
togows databases search
|
|
42
|
+
togows search pubmed "lung cancer" --count
|
|
43
|
+
togows search uniprot "FGF19" --limit 10
|
|
44
|
+
togows search uniprot "lung cancer" --offset 1 --limit 10
|
|
45
|
+
HELP
|
|
46
|
+
convert: <<~HELP,
|
|
47
|
+
Usage: togows convert SOURCE.FORMAT [FILE] [options]
|
|
48
|
+
|
|
49
|
+
Next arguments:
|
|
50
|
+
SOURCE.FORMAT Conversion pair such as genbank.gff
|
|
51
|
+
FILE Optional input file; stdin is used when omitted
|
|
52
|
+
|
|
53
|
+
Convert options:
|
|
54
|
+
--to FORMAT
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
{
|
|
58
|
+
printf 'LOCUS TEST 12 bp DNA linear UNA 01-JAN-2000\n'
|
|
59
|
+
printf 'FEATURES Location/Qualifiers\n'
|
|
60
|
+
printf 'ORIGIN\n'
|
|
61
|
+
printf ' 1 acgtacgtacgt\n'
|
|
62
|
+
printf '//\n'
|
|
63
|
+
} | togows convert genbank.fasta
|
|
64
|
+
HELP
|
|
65
|
+
databases: <<~HELP,
|
|
66
|
+
Usage: togows databases [entry|search] [options]
|
|
67
|
+
|
|
68
|
+
Next arguments:
|
|
69
|
+
entry Databases usable with togows entry
|
|
70
|
+
search Databases usable with togows search
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
togows databases
|
|
74
|
+
togows databases entry
|
|
75
|
+
togows databases search
|
|
76
|
+
HELP
|
|
77
|
+
ucsc: <<~HELP
|
|
78
|
+
Usage: togows ucsc [DATABASE] [TABLE] [QUERY] [options]
|
|
79
|
+
|
|
80
|
+
Next arguments:
|
|
81
|
+
DATABASE Genome database such as hg38 or hg19
|
|
82
|
+
TABLE UCSC table such as refGene
|
|
83
|
+
QUERY Optional query fragment such as name2=UVSSA
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
togows ucsc
|
|
87
|
+
togows ucsc hg38
|
|
88
|
+
togows ucsc hg38 refGene name2=UVSSA
|
|
89
|
+
HELP
|
|
90
|
+
}.freeze
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TogoWS
|
|
6
|
+
module URLBuilder
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def entry(database, ids, field = nil, format = nil)
|
|
10
|
+
id_list = Array(ids).join(",")
|
|
11
|
+
parts = ["entry", escape_segment(database), escape_segment(id_list)]
|
|
12
|
+
parts << escape_path(field) if field && !field.empty?
|
|
13
|
+
with_format("/#{parts.join('/')}", format)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def search(database, query, offset = nil, limit = nil, count = false, format = nil)
|
|
17
|
+
parts = ["search", escape_segment(database), escape_segment(query)]
|
|
18
|
+
if count
|
|
19
|
+
parts << "count"
|
|
20
|
+
elsif offset && limit
|
|
21
|
+
parts << "#{offset},#{limit}"
|
|
22
|
+
end
|
|
23
|
+
with_format("/#{parts.join('/')}", format)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def convert(source, format = nil)
|
|
27
|
+
data_source, target_format = split_source_format(source, format)
|
|
28
|
+
"/convert/#{escape_segment(data_source)}.#{escape_segment(target_format)}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def databases(kind)
|
|
32
|
+
case kind
|
|
33
|
+
when "entry" then "/entry"
|
|
34
|
+
when "search" then "/search"
|
|
35
|
+
else
|
|
36
|
+
raise Error, "database list must be entry or search"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def ucsc(database = nil, table = nil, query = nil, offset = nil, limit = nil, format = nil)
|
|
41
|
+
parts = %w[api ucsc]
|
|
42
|
+
parts << escape_segment(database) if database && !database.empty?
|
|
43
|
+
parts << escape_segment(table) if table && !table.empty?
|
|
44
|
+
parts << escape_path(query) if query && !query.empty?
|
|
45
|
+
parts << "#{offset},#{limit}" if offset && limit
|
|
46
|
+
with_format("/#{parts.join('/')}", format)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def full_url(base_url, path)
|
|
50
|
+
base = base_url.to_s.sub(%r{/*\z}, "")
|
|
51
|
+
base + path
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def escape_segment(value)
|
|
55
|
+
URI.encode_www_form_component(value.to_s)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def escape_path(value)
|
|
59
|
+
value.to_s.split("/", -1).map { |part| escape_segment(part) }.join("/")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def with_format(path, format)
|
|
63
|
+
return path if format.nil? || format.empty?
|
|
64
|
+
|
|
65
|
+
"#{path}.#{escape_segment(format)}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def split_source_format(source, format)
|
|
69
|
+
if format && !format.empty?
|
|
70
|
+
[source, format]
|
|
71
|
+
elsif source.to_s.include?(".")
|
|
72
|
+
source.to_s.split(".", 2)
|
|
73
|
+
else
|
|
74
|
+
raise Error, "missing target format; use SOURCE.FORMAT or --to FORMAT"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/togows.rb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "togows/version"
|
|
4
|
+
require_relative "togows/color"
|
|
5
|
+
require_relative "togows/error"
|
|
6
|
+
require_relative "togows/help"
|
|
7
|
+
require_relative "togows/url_builder"
|
|
8
|
+
require_relative "togows/client"
|
|
9
|
+
require_relative "togows/cli"
|
metadata
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: togows
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- kojix2
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: togows provides a small Ruby command line interface for TogoWS entry,
|
|
13
|
+
search, convert, and UCSC API endpoints.
|
|
14
|
+
email:
|
|
15
|
+
- 2xijok@gmail.com
|
|
16
|
+
executables:
|
|
17
|
+
- togows
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE.txt
|
|
22
|
+
- README.md
|
|
23
|
+
- docs/user-guide.md
|
|
24
|
+
- exe/togows
|
|
25
|
+
- lib/togows.rb
|
|
26
|
+
- lib/togows/cli.rb
|
|
27
|
+
- lib/togows/client.rb
|
|
28
|
+
- lib/togows/color.rb
|
|
29
|
+
- lib/togows/error.rb
|
|
30
|
+
- lib/togows/help.rb
|
|
31
|
+
- lib/togows/url_builder.rb
|
|
32
|
+
- lib/togows/version.rb
|
|
33
|
+
homepage: https://github.com/kojix2/togows-cli
|
|
34
|
+
licenses:
|
|
35
|
+
- MIT
|
|
36
|
+
metadata:
|
|
37
|
+
homepage_uri: https://github.com/kojix2/togows-cli
|
|
38
|
+
source_code_uri: https://github.com/kojix2/togows-cli/tree/main
|
|
39
|
+
bug_tracker_uri: https://github.com/kojix2/togows-cli/issues
|
|
40
|
+
rubygems_mfa_required: 'true'
|
|
41
|
+
rdoc_options: []
|
|
42
|
+
require_paths:
|
|
43
|
+
- lib
|
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '3.1'
|
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
requirements: []
|
|
55
|
+
rubygems_version: 4.0.10
|
|
56
|
+
specification_version: 4
|
|
57
|
+
summary: Dependency-free command line client for the TogoWS REST API
|
|
58
|
+
test_files: []
|