optica-client 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2707b665c02b7e9fab50ed54997fcd7f6deaba82
4
+ data.tar.gz: 6a1e0236dd17a745341c62fb12723a325ad89a78
5
+ SHA512:
6
+ metadata.gz: f1011f391dd4695e3ebd5bc5833697d34e785d93b3510ff13fcd35c936731a4c9800b9a76cd4a2d7512620f2fbd8c2d9cedfe56a7bec06353b1fab1c69d5d759
7
+ data.tar.gz: 5be5f02624481514824262fd12fbde9d21287623996ee43d1f94d8526a60d0f3bb0f43653e516ed5e44bd90469203f367124f3f7e8314f73a0bb5f716d855522
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # optica-client
2
+
3
+ This `optica-client` is a CLI for Airbnb's [Optica](https://github.com/airbnb/optica) service.
4
+ It's command-line name is `optical`.
5
+
6
+ This is not an official Airbnb product.
7
+
8
+ ## Installation
9
+
10
+ Install it via Rubygems:
11
+
12
+ $ gem install optica-client
13
+
14
+ You'll want to configure the command-line client to use your Optica instance:
15
+
16
+ $ optical -H https://optica.example.com
17
+
18
+ ## Usage
19
+
20
+ ```text
21
+ Usage: optical [options] [FIELD=FILTER] [FIELD2=FILTER2...]
22
+
23
+ Fetch host information from Optica, and cache it for 15 minutes. Output the
24
+ fetched information as single-line JSON hashes, suitable for furhter processing with `jq`.
25
+
26
+ FIELD: any optica field; see your optica host for availible fields
27
+ FILTER: either a bare string, like "optica", or a regex string, like "/^(o|O)ptica?/"
28
+
29
+ Options:
30
+ -s, --select a,b,c Retrieve the given fields, in addition to the defaults
31
+ -a, --all Retrieve all fields (default is just role,id,hostname)
32
+ -v, --verbose Print debug information to STDERR
33
+ -p, --pretty[=true] Pretty-print JSON (default true when STDOUT is a TTY)
34
+ -r, --refresh Delete cache before performing request.
35
+ -h, --host URI Optica host (default "https://optica.d.musta.ch")
36
+ -H, --set-default-host URI Set the default optica host
37
+ ```
38
+
39
+ ## Development
40
+
41
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
42
+
43
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
44
+
45
+ ## Contributing
46
+
47
+ Bug reports and pull requests are welcome on GitHub at https://github.com/justjake/optica-client.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "optical"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "pry"
14
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/optical ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optica/client/cli'
4
+ exit(Optica::Client::CLI.run(ARGV))
@@ -0,0 +1,290 @@
1
+ require 'optparse'
2
+ require 'filecache'
3
+ require 'pathname'
4
+
5
+ require_relative './config'
6
+ require_relative './request'
7
+ require_relative './fetch_json'
8
+
9
+ module Optica
10
+ module Client
11
+ # Command-line interface
12
+ class CLI
13
+ # rate-limit when printing huge JSON to a tty
14
+ TTY_TIMEOUT = 0.00001
15
+
16
+ USER_PATH = Pathname.new('~/.optical').expand_path
17
+ CONFIG_PATH = USER_PATH.join('config.yml')
18
+ CACHE_ROOT = USER_PATH.join('cache').to_s
19
+
20
+ ERR_NOT_FOUND = 4
21
+ ERR_INVALID = 2
22
+
23
+ # 15 min
24
+ CACHE_MAX_AGE = 15 * 60
25
+
26
+ def self.run(argv)
27
+ new.run(argv)
28
+ end
29
+
30
+ attr_reader :fields, :verbose, :cache, :delete_cache
31
+
32
+ def initialize
33
+ @config = ::Optica::Client::Config.from_file(CONFIG_PATH)
34
+ @host = nil
35
+ @fields = []
36
+ @verbose = false
37
+ @pretty = data_pipe.tty?
38
+ @cache = ::FileCache.new(
39
+ "requests",
40
+ CACHE_ROOT,
41
+ CACHE_MAX_AGE,
42
+ 1
43
+ )
44
+ @delete_cache = false
45
+ end
46
+
47
+ # Options for the CLI
48
+ #
49
+ # @return [OptionParser]
50
+ def option_parser
51
+ @option_parser ||= OptionParser.new do |o|
52
+ o.banner = "Usage: optical [options] [FIELD=FILTER] [FIELD2=FILTER2...]"
53
+
54
+ o.separator ''
55
+ o.separator <<-EOS
56
+ Fetch host information from Optica, and cache it for 15 minutes. Output the
57
+ fetched information as single-line JSON hashes, suitable for furhter processing with `jq`.
58
+
59
+ FIELD: any optica field; see your optica host for availible fields
60
+ FILTER: either a bare string, like "optica", or a regex string, like "/^(o|O)ptica?/"
61
+ EOS
62
+
63
+ o.separator ''
64
+ o.separator 'Options:'
65
+
66
+ o.on(
67
+ '-s',
68
+ '--select a,b,c',
69
+ ::Array,
70
+ 'Retrieve the given fields, in addition to the defaults'
71
+ ) do |fs|
72
+ @fields.concat(fs)
73
+ end
74
+
75
+ o.on(
76
+ '-a',
77
+ '--all',
78
+ 'Retrieve all fields (default is just role,id,hostname)'
79
+ ) do |all|
80
+ @fields = nil if all
81
+ end
82
+
83
+ o.on(
84
+ '-v',
85
+ '--verbose',
86
+ 'Print debug information to STDERR'
87
+ ) do |v|
88
+ @verbose = v
89
+ end
90
+
91
+ o.on('-p', "--pretty[=#{data_pipe.tty?}]", "Pretty-print JSON (default true when STDOUT is a TTY)") do |p|
92
+ @pretty = p
93
+ end
94
+
95
+ o.on(
96
+ '-r',
97
+ '--refresh',
98
+ 'Delete cache before performing request.'
99
+ ) do |r|
100
+ @delete_cache = r
101
+ end
102
+
103
+ o.on(
104
+ '-h',
105
+ '--host URI',
106
+ "Optica host (default #{@config.default_host.inspect})"
107
+ ) do |host|
108
+ @host = host
109
+ end
110
+
111
+ o.on(
112
+ '-H URI',
113
+ '--set-default-host URI',
114
+ 'Set the default optica host'
115
+ ) do |h|
116
+ # TODO: move to #run
117
+ @host = h
118
+ @config.default_host = h
119
+ @config.write!
120
+ log "set default host to #{h}"
121
+ end
122
+
123
+ end
124
+ end
125
+
126
+ # Run the CLI
127
+ #
128
+ # @param [Array<String>] argv Command-line args
129
+ def run(argv)
130
+ args = option_parser.parse(argv)
131
+
132
+ begin
133
+ filters = args.map { |arg| parse_filter(arg) }.reduce({}, :merge)
134
+ rescue ArgumentError => err
135
+ ui_pipe.puts err.message
136
+ ui_pipe.puts ''
137
+ ui_pipe.puts option_parser
138
+ return ERR_INVALID
139
+ end
140
+
141
+ if host.nil?
142
+ ui_pipe.puts 'No host given.'
143
+ ui_pipe.puts 'Set the default with -H, or for the invocation with -h.'
144
+ ui_pipe.puts ''
145
+ ui_pipe.puts option_parser
146
+ return ERR_INVALID
147
+ end
148
+
149
+ manage_cache
150
+
151
+ req = ::Optica::Client::Request.new(host).where(filters)
152
+ if fields
153
+ req.select(*fields)
154
+ else
155
+ req.select_all
156
+ end
157
+
158
+ json = fetch(req)
159
+ output(json['nodes'])
160
+
161
+ if json['nodes'].any?
162
+ # happy!
163
+ return 0
164
+ else
165
+ # none found
166
+ return ERR_NOT_FOUND
167
+ end
168
+ end
169
+
170
+ def fetch(req)
171
+ log "URL: #{req.root}"
172
+ log "Filters: #{req.filters}"
173
+ log "Fields: #{req.select_all? ? '(all fields)' : req.fields.inspect}"
174
+ log "GET #{req.to_uri}"
175
+
176
+ key = req.to_uri.to_s
177
+ cache.get_or_set(key) do
178
+ fetch_with_progress_bar(req.to_uri)
179
+ end
180
+ end
181
+
182
+ def output(json)
183
+ log "got #{json.size} entries"
184
+
185
+ use_sleep = false
186
+ if json.size >= 1000 && data_pipe.tty?
187
+ log 'reducing output speed to allow Ctrl-C'
188
+ use_sleep = true
189
+ end
190
+
191
+ json.each do |_ip, node|
192
+ string = @pretty ? JSON.pretty_generate(node) : JSON.fast_generate(node)
193
+ data_pipe.puts string
194
+ # easier Ctrl-C in Tmux
195
+ sleep TTY_TIMEOUT if use_sleep
196
+ end
197
+ end
198
+
199
+ def manage_cache
200
+ # clean up expired stuff
201
+ cache.purge
202
+
203
+ if delete_cache
204
+ log "deleting cache dir #{CACHE_ROOT}"
205
+ FileUtils.rm_r(CACHE_ROOT)
206
+ end
207
+ end
208
+
209
+ def fetch_with_progress_bar(uri)
210
+ ::Optica::Client.fetch_json(uri) do |_chunk, ratio|
211
+ view = "Download #{progress_bar(ratio)}"
212
+
213
+ ui_pipe.print "\r#{view}"
214
+ ui_pipe.print "\n" if ratio == 1.0
215
+ end
216
+ end
217
+
218
+ # Returns a progress bar, as a string.
219
+ #
220
+ # Looks kinda like this:
221
+ # [>>>>> ] NN.NN%
222
+ #
223
+ # @return [String]
224
+ def progress_bar(ratio)
225
+ width = 40
226
+ chars = (width * ratio).to_i
227
+ start = '['
228
+ fin = ']'
229
+ done = '>' * chars
230
+ to_do = ' ' * (width - chars)
231
+ percent = format(' %.2f%', ratio * 100)
232
+ start + done + to_do + fin + percent
233
+ end
234
+
235
+ # Parse a command-line argument into a single Optica filter.
236
+ #
237
+ # Filter types:
238
+ # - exact string match: base case
239
+ # attribute=string
240
+ #
241
+ # - regex match: filter begins and ends with /
242
+ # attribute=/^[rR]egexp?/
243
+ #
244
+ # - array match: begins with [, ends with ]
245
+ # attribute=[one,two]
246
+ #
247
+ # @param string [String]
248
+ # @return [Hash<String, Any>] a filter hash
249
+ def parse_filter(string)
250
+ unless string.include?('=')
251
+ raise ArgumentError.new("Invalid filter: #{string.inspect}")
252
+ end
253
+
254
+ key, *values = string.split('=')
255
+ value = values.join('=')
256
+
257
+ # parse regex
258
+ if value[0] == '/' && value[-1] == '/'
259
+ return { key => /#{value[1...-1]}/ }
260
+ end
261
+
262
+ # parse array-like
263
+ if value[0] == '[' && value[-1] == ']'
264
+ return { key => value[1...-1].split(',') }
265
+ end
266
+
267
+ # just a string
268
+ { key => value }
269
+ end
270
+
271
+ def host
272
+ @host || @config.default_host
273
+ end
274
+
275
+ def log(msg)
276
+ if verbose
277
+ ui_pipe.puts(msg)
278
+ end
279
+ end
280
+
281
+ def data_pipe
282
+ $stdout
283
+ end
284
+
285
+ def ui_pipe
286
+ $stderr
287
+ end
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,45 @@
1
+ require 'yaml'
2
+
3
+ module Optica
4
+ module Client
5
+ # Handles storing and loading configurable settings for our CLI
6
+ class Config
7
+ attr_accessor :config_path
8
+ attr_accessor :default_host
9
+
10
+ def self.from_file(path)
11
+ new(config_path: path.to_s).reload
12
+ end
13
+
14
+ def initialize(hash = {})
15
+ set_all(hash)
16
+ end
17
+
18
+ def to_h
19
+ {
20
+ config_path: config_path.to_s,
21
+ default_host: default_host.to_s,
22
+ }
23
+ end
24
+
25
+ def write!
26
+ data = YAML.dump(to_h)
27
+ File.write(config_path, data)
28
+ end
29
+
30
+ def reload
31
+ return self unless File.exist?(config_path)
32
+
33
+ data = YAML.load(File.read(config_path)).merge(config_path: config_path)
34
+ set_all(data)
35
+ self
36
+ end
37
+
38
+ def set_all(data)
39
+ data.each do |key, val|
40
+ self.public_send("#{key}=", val)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,39 @@
1
+ require 'net/http'
2
+ require 'json'
3
+ require 'stringio'
4
+
5
+ module Optica
6
+ module Client
7
+ # Fetch the JSON data at the given URI. Blocks.
8
+ #
9
+ # As data is received, yeilds the data chunk (a string) and the completion
10
+ # ratio (a float).
11
+ #
12
+ # @return [Any] the parsed JSON
13
+ def self.fetch_json(uri)
14
+ io = StringIO.new
15
+
16
+ Net::HTTP.start(
17
+ uri.host,
18
+ uri.port,
19
+ use_ssl: uri.scheme == 'https'
20
+ ) do |http|
21
+ req = Net::HTTP::Get.new(uri)
22
+
23
+ http.request(req) do |res|
24
+ file_size = res['content-length'].to_i
25
+ downloaded = 0.0
26
+
27
+ res.read_body do |chunk|
28
+ io.write chunk
29
+ downloaded += chunk.size
30
+ ratio = downloaded / file_size
31
+ yield(chunk, ratio) if block_given?
32
+ end
33
+ end
34
+ end
35
+
36
+ JSON.parse(io.string)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,120 @@
1
+ require 'uri'
2
+
3
+ module Optica
4
+ module Client
5
+ # Request builds URIs to fetch data from Optica. There is no planned support
6
+ # for posting data to Optica.
7
+ #
8
+ # By default a Request will request the minimum number of fields from Optica
9
+ # to save on response size, using the /roles endpoint. Use the {#fields} or
10
+ # {#all_fields!} methods to fetch more fields.
11
+ #
12
+ # Build your request, then submit it using your HTTP client of choice using
13
+ # the {#to_uri} method.
14
+ class Request
15
+ # @param root_url [String] HTTP(S) URI root of your Optica instance.
16
+ # @param given_opts [Hash] options
17
+ # @option given_opts [Array<String>] :fields Additional fields to request.
18
+ # see also {#fields} and {#all_fields!}
19
+ # @option given_opts [Hash] :filters Filters. See {#where}.
20
+ # @option given_opts [Boolean] :all_fields (false) See {#all_fields!}
21
+ def initialize(root_url, given_opts = {})
22
+ opts = {
23
+ fields: [],
24
+ filters: {},
25
+ all_fields: false,
26
+ }.merge(given_opts)
27
+
28
+ @uri = URI(root_url)
29
+ @fields = opts[:fields]
30
+ @filters = opts[:filters]
31
+ @all_fields = opts[:all_fields]
32
+ end
33
+
34
+ def filters
35
+ @filters.dup
36
+ end
37
+
38
+ def fields
39
+ @fields.dup
40
+ end
41
+
42
+ # returns the fields that will be fetched using this request, via the
43
+ # /roles endpoint.
44
+ #
45
+ # @see https://github.com/airbnb/optica/blob/master/optica.rb#L15-L24
46
+
47
+ # Add more requested fields.
48
+ #
49
+ # @param fields [Array<String>]
50
+ # @return self
51
+ def select(*fields)
52
+ @fields.concat(fields).uniq!
53
+ self
54
+ end
55
+
56
+ # Request all fields. Overwrites any previously-requested fields.
57
+ #
58
+ # @return self
59
+ def select_all
60
+ @all_fields = true
61
+ self
62
+ end
63
+
64
+ # Add filters to this optica request
65
+ #
66
+ # Filters should be a hash of String => Constraint, where constraint is a
67
+ # - regex (regex match)
68
+ # - boolean (matches 1, true, or True)
69
+ # - string (exact match)
70
+ #
71
+ # @param filters [Hash]
72
+ # @return self
73
+ def where(filters)
74
+ @filters.merge!(filters)
75
+ self
76
+ end
77
+
78
+ # @return [URI]
79
+ def to_uri
80
+ uri = @uri.dup
81
+ uri.query = URI.encode_www_form(query_params)
82
+ uri.path = '/roles' unless @all_fields
83
+ uri
84
+ end
85
+
86
+ def root
87
+ @uri
88
+ end
89
+
90
+ def select_all?
91
+ @all_fields
92
+ end
93
+
94
+ private
95
+
96
+ # @return [String]
97
+ def query_params
98
+ params = {}
99
+ @filters.each do |key, value|
100
+ params[key.to_s] = filter_to_param(value)
101
+ end
102
+ if @fields.any? && !@all_fields
103
+ params['_extra_fields'] = @fields.join(',')
104
+ end
105
+ params
106
+ end
107
+
108
+ # @return [String]
109
+ def filter_to_param(value)
110
+ case value
111
+ when ::Regexp
112
+ value.source
113
+ else
114
+ literal = ::Regexp.escape(value.to_s)
115
+ "^#{literal}$"
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,5 @@
1
+ module Optica
2
+ module Client
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,12 @@
1
+ ##
2
+ # Optica::Client helps you efficiently query Optica, Airbnb's open-source host
3
+ # registration service.
4
+ #
5
+ # @see Optica::Client::Request
6
+
7
+ module Optica
8
+ module Client
9
+ require_relative './client/version'
10
+ require_relative './client/request'
11
+ end
12
+ end
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'optica/client/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "optica-client"
8
+ spec.version = Optica::Client::VERSION
9
+ spec.authors = ["Jake Teton-Landis"]
10
+ spec.email = ["jake.tl@airbnb.com"]
11
+
12
+ spec.summary = %q{An Optica client}
13
+ spec.description = %q{An efficient Optica CLI client and accompanying Ruby library}
14
+ spec.homepage = "https://github.com/justjake/optica-client"
15
+
16
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
18
+ if spec.respond_to?(:metadata)
19
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
20
+ end
21
+
22
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
23
+ f.match(%r{^(test|spec|features)/})
24
+ end
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_dependency "filecache", "~> 1.0.2"
30
+
31
+ spec.add_development_dependency "bundler", "~> 1.14"
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+
34
+ spec.add_development_dependency "pry"
35
+ spec.add_development_dependency "pry-byebug"
36
+ spec.add_development_dependency "pry-doc"
37
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: optica-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jake Teton-Landis
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-04-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: filecache
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.14'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.14'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry-doc
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: An efficient Optica CLI client and accompanying Ruby library
98
+ email:
99
+ - jake.tl@airbnb.com
100
+ executables:
101
+ - optical
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - .gitignore
106
+ - Gemfile
107
+ - README.md
108
+ - Rakefile
109
+ - bin/console
110
+ - bin/setup
111
+ - exe/optical
112
+ - lib/optica/client.rb
113
+ - lib/optica/client/cli.rb
114
+ - lib/optica/client/config.rb
115
+ - lib/optica/client/fetch_json.rb
116
+ - lib/optica/client/request.rb
117
+ - lib/optica/client/version.rb
118
+ - optica-client.gemspec
119
+ homepage: https://github.com/justjake/optica-client
120
+ licenses: []
121
+ metadata:
122
+ allowed_push_host: https://rubygems.org
123
+ post_install_message:
124
+ rdoc_options: []
125
+ require_paths:
126
+ - lib
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubyforge_project:
139
+ rubygems_version: 2.6.10
140
+ signing_key:
141
+ specification_version: 4
142
+ summary: An Optica client
143
+ test_files: []