optica-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []