haveapi-client 0.6.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9360cc54a28c352ca36a156d1cfdfc96eea32959
4
- data.tar.gz: c47ae6db07e500c6cf47f1836436349a9634e746
3
+ metadata.gz: fbf74153c675555c4efe868808662ecba7edab78
4
+ data.tar.gz: 9841b9182c4585edbb415cc7e9a09637c1b0491b
5
5
  SHA512:
6
- metadata.gz: 934b2293efe40af87bda6cef7a880ac8b4fdc793b8d675b8d8ee06e4dbe5d818b88b626ce7609ec5b43a228d7d7453e4cb74776f627ce84a34713b852af3c0e6
7
- data.tar.gz: e5b17a1f4d8f3367db75579008f0a4fefb6a1e5613b2f030b36951ad0076a2b1aa948a8737d70cbd6b45aae855881d2fda2fbcf91bedeacaa5428a377bc3bcf2
6
+ metadata.gz: 1dfa2473733340a6650dfa0b76ae94b224f916d4fb66c8c962b5bb28f6ba3d80d3f986562ef1cdfef837001cc3805214ab0b554150b7a9ed5925ce5873a5dc3a
7
+ data.tar.gz: 1bec8283d80a29b7a70fe9fac8f7b2b51bbdcce62c143a4a325f307d278dc098e1d764d4ae991e45a91c3a9e58f8fe9447247003232413311edf5e3197c008e4
data/CHANGELOG CHANGED
@@ -1,3 +1,16 @@
1
+ * Thu Nov 24 2016 - version 0.7.0
2
+ - Client.initialize takes two arguments: url and options
3
+ - Authenticate even for actions that do not require it
4
+ - New CLI options --[no-]block and --timeout
5
+ - CLI handles all interrupts
6
+ - Add shortcut method HaveAPI::Client.new
7
+ - Add method Action#meta
8
+ - Fix access to nil associations in ResourceInstance
9
+ - Handle 404 errors when fetching description
10
+ - Handle RestClient::ResourceNotFound
11
+ - Use DateTime.iso8601 to parse dates
12
+ - Do not show examples on action error, just a hint
13
+
1
14
  * Tue Oct 18 2016 - version 0.6.0
2
15
  - No changes
3
16
 
data/README.md CHANGED
@@ -40,6 +40,8 @@ Or install it yourself as:
40
40
  --utc Display Datetime parameters in UTC
41
41
  --localtime Display Datetime parameters in local timezone
42
42
  --date-format FORMAT Display Datetime in custom format
43
+ --[no-]block Toggle action blocking mode
44
+ --timeout SEC Fail when the action does not finish within the timeout
43
45
  -v, --[no-]verbose Run verbosely
44
46
  --client-version Show client version
45
47
  --protocol-version Show protocol version
@@ -6,7 +6,7 @@ require 'haveapi/client/version'
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = 'haveapi-client'
8
8
  spec.version = HaveAPI::Client::VERSION
9
- spec.date = '2016-10-18'
9
+ spec.date = '2016-11-24'
10
10
  spec.authors = ['Jakub Skokan']
11
11
  spec.email = ['jakub.skokan@vpsfree.cz']
12
12
  spec.summary =
@@ -27,4 +27,5 @@ Gem::Specification.new do |spec|
27
27
  spec.add_runtime_dependency 'rest-client', '~> 1.8.0'
28
28
  spec.add_runtime_dependency 'json', '~> 1.8.3'
29
29
  spec.add_runtime_dependency 'highline', '~> 1.7.8'
30
+ spec.add_runtime_dependency 'ruby-progressbar', '~> 1.7.5'
30
31
  end
data/lib/haveapi/cli.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  require_relative 'client'
2
2
 
3
3
  module HaveAPI
4
- module CLI ; end
4
+ module CLI
5
+ module Commands ; end
6
+ end
5
7
  end
6
8
 
7
9
  require_rel 'cli'
10
+ require_rel 'cli/commands'
@@ -0,0 +1,147 @@
1
+ require 'ruby-progressbar'
2
+
3
+ module HaveAPI::CLI
4
+ # This class can watch action's state and show it's progress with a progress bar.
5
+ #
6
+ # When interrupted, the user is asked whether he wishes to cancel the action,
7
+ # then it shows the progress of cancellation.
8
+ #
9
+ # Methods from this class may invoke `exit()` whenever appropriate.
10
+ class ActionState
11
+ # @param opts [Hash]
12
+ # @param action [HaveAPI::Client::Action]
13
+ # @param id [Integer]
14
+ def initialize(opts, client, id)
15
+ @opts = opts
16
+ @client = client
17
+ @id = id
18
+ end
19
+
20
+ # Block until the action is finished or timeout is reached. Progress is shown with
21
+ # a progress bar. Offers cancellation on interrupt.
22
+ #
23
+ # @param timeout [Float]
24
+ # @param cancel [Boolean] determines whether we're waiting for a cancel to finish
25
+ def wait_for_completion(id: nil, timeout: nil, cancel: false)
26
+ id ||= @id
27
+
28
+ if cancel
29
+ puts "Waiting for the action to cancel (hit Ctrl+C to skip)..."
30
+ else
31
+ puts "Waiting for the action to complete (hit Ctrl+C to skip)..."
32
+ end
33
+
34
+ last_status = false
35
+ can_cancel = false
36
+
37
+ begin
38
+ ret = HaveAPI::Client::Action.wait_for_completion(
39
+ @client,
40
+ id,
41
+ timeout: timeout,
42
+ ) do |state|
43
+ last_status = state.status
44
+ can_cancel = state.can_cancel?
45
+
46
+ update_progress(state, cancel)
47
+ end
48
+
49
+ rescue Interrupt
50
+ @pb && @pb.stop
51
+ puts
52
+
53
+ cancel_action(timeout: timeout) if can_cancel && !cancel && last_status
54
+
55
+ puts
56
+ print_help(id)
57
+ exit(false)
58
+ end
59
+
60
+ if ret
61
+ @pb && @pb.finish
62
+ else
63
+ @pb && @pb.stop
64
+ end
65
+
66
+ ret
67
+ end
68
+
69
+ # Ask the user if he wishes to cancel the action. If so, execute cancel
70
+ # and call self.wait_for_completion on the cancellation, if it is blocking
71
+ # operation.
72
+ def cancel_action(timeout: nil)
73
+ STDOUT.write("Do you wish to cancel the action? [y/N]: ")
74
+ STDOUT.flush
75
+
76
+ if STDIN.readline.strip.downcase == 'y'
77
+ begin
78
+ res = HaveAPI::Client::Action.cancel(@client, @id)
79
+
80
+ rescue HaveAPI::Client::ActionFailed => e
81
+ res = e.response
82
+ end
83
+
84
+ if res.is_a?(HaveAPI::Client::Response) && res.ok?
85
+ puts "Cancelled"
86
+ exit
87
+
88
+ elsif res
89
+ @pb.resume
90
+
91
+ wait_for_completion(
92
+ id: res,
93
+ timeout: timeout,
94
+ cancel: true,
95
+ )
96
+ exit
97
+ end
98
+
99
+ warn "Cancel failed: #{res.message}"
100
+ exit(false)
101
+ end
102
+ end
103
+
104
+ def print_help(id = nil)
105
+ id ||= @id
106
+
107
+ puts "Run"
108
+ puts " #{$0} action_state show #{id}"
109
+ puts "or"
110
+ puts " #{$0} action_state wait #{id}"
111
+ puts "to check the action's progress."
112
+ end
113
+
114
+ protected
115
+ def update_progress(state, cancel)
116
+ @pb ||= ProgressBar.create(
117
+ title: cancel ? 'Cancelling' : 'Executing',
118
+ total: state.progress.total,
119
+ format: if state.progress.total && state.progress.total > 0
120
+ "%t: [%B] %c/%C #{state.progress.unit}"
121
+ else
122
+ '%t: [%B]'
123
+ end,
124
+ starting_at: state.progress.current,
125
+ autofinish: false,
126
+ )
127
+
128
+ if state.status
129
+ @pb.title = cancel ? 'Cancelling' : 'Executing'
130
+
131
+ else
132
+ @pb.title = 'Failing'
133
+ end
134
+
135
+ if state.progress.total && state.progress.total > 0
136
+ @pb.progress = state.progress.current
137
+ @pb.total = state.progress.total
138
+ @pb.format("%t: [%B] %c/%C #{state.progress.unit}")
139
+
140
+ else
141
+ @pb.total = nil
142
+ @pb.format("%t: [%B] #{state.progress.unit}")
143
+ @pb.increment
144
+ end
145
+ end
146
+ end
147
+ end
@@ -11,6 +11,10 @@ module HaveAPI::CLI
11
11
 
12
12
  def run
13
13
  c = new
14
+
15
+ rescue Interrupt
16
+ warn "Interrupted"
17
+ exit(false)
14
18
  end
15
19
 
16
20
  def register_auth_method(name, klass)
@@ -89,7 +93,13 @@ module HaveAPI::CLI
89
93
  end
90
94
 
91
95
  if authenticate(action) && !action.unresolved_args?
92
- action.update_description(@api.describe_action(action))
96
+ begin
97
+ action.update_description(@api.describe_action(action))
98
+
99
+ rescue RestClient::ResourceNotFound => e
100
+ format_errors(action, 'Object not found', {})
101
+ exit(false)
102
+ end
93
103
  end
94
104
 
95
105
  @selected_params = @opts[:output] ? @opts[:output].split(',').uniq
@@ -115,6 +125,32 @@ module HaveAPI::CLI
115
125
  format_errors(action, ret[:message], ret[:errors])
116
126
  exit(false)
117
127
  end
128
+
129
+ if action.blocking?
130
+ res = HaveAPI::Client::Response.new(action, ret)
131
+
132
+ if res.meta[:action_state_id]
133
+ state = ActionState.new(
134
+ @opts,
135
+ HaveAPI::Client::Client.new(@api.url, communicator: @api, block: false),
136
+ res.meta[:action_state_id]
137
+ )
138
+
139
+ if @opts[:block]
140
+ puts
141
+ action_ret = state.wait_for_completion(timeout: @opts[:timeout])
142
+
143
+ if action_ret.nil?
144
+ warn "Timeout"
145
+ exit(false)
146
+ end
147
+
148
+ else
149
+ puts
150
+ state.print_help
151
+ end
152
+ end
153
+ end
118
154
  end
119
155
 
120
156
  def api_url
@@ -124,6 +160,7 @@ module HaveAPI::CLI
124
160
  def options
125
161
  options = {
126
162
  client: default_url,
163
+ block: true,
127
164
  verbose: false,
128
165
  }
129
166
 
@@ -208,6 +245,18 @@ module HaveAPI::CLI
208
245
  options[:date_format] = f
209
246
  end
210
247
 
248
+ opts.on('--[no-]block', 'Toggle action blocking mode') do |v|
249
+ options[:block] = v
250
+ end
251
+
252
+ opts.on(
253
+ '--timeout SEC',
254
+ Float,
255
+ 'Fail when the action does not finish within the timeout'
256
+ ) do |v|
257
+ options[:timeout] = v.to_f
258
+ end
259
+
211
260
  opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v|
212
261
  options[:verbose] = v
213
262
  end
@@ -549,8 +598,6 @@ module HaveAPI::CLI
549
598
  end
550
599
 
551
600
  def authenticate(action = nil)
552
- return false if !action.nil? && !action.auth?
553
-
554
601
  if @auth
555
602
  @auth.communicator = @api
556
603
  @auth.validate
@@ -606,14 +653,14 @@ module HaveAPI::CLI
606
653
  def format_errors(action, msg, errors)
607
654
  warn "Action failed: #{msg}"
608
655
 
609
- if errors.any?
656
+ if errors && errors.any?
610
657
  puts 'Errors:'
611
658
  errors.each do |param, e|
612
659
  puts "\t#{param}: #{e.join('; ')}"
613
660
  end
614
661
  end
615
662
 
616
- print_examples(action)
663
+ puts "\nUse --help to see available parameters and example usage."
617
664
  end
618
665
 
619
666
  def find_command(resource, action)
@@ -0,0 +1,28 @@
1
+ module HaveAPI::CLI::Commands
2
+ class ActionStateWait < HaveAPI::CLI::Command
3
+ cmd :action_state, :wait
4
+ args '<STATE ID>'
5
+ desc 'Block until the action is finished'
6
+
7
+ def exec(args)
8
+ if args.size < 1
9
+ warn "Provide argument STATE ID"
10
+ exit(false)
11
+ end
12
+
13
+ @api.set_opts(block: false)
14
+
15
+ state = HaveAPI::CLI::ActionState.new(
16
+ @global_opts,
17
+ @api,
18
+ args.first.to_i
19
+ )
20
+ ret = state.wait_for_completion(timeout: @global_opts[:timeout])
21
+
22
+ if ret.nil?
23
+ warn "Timeout"
24
+ exit(false)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -7,20 +7,27 @@ module HaveAPI::CLI
7
7
  # request
8
8
  out << "$ #{$0} #{action.resource_path.join('.')} #{action.name}"
9
9
 
10
- params = example[:request][action.namespace(:input).to_sym]
10
+ params = example[:request]
11
11
 
12
12
  if params
13
13
  out << ' --' unless params.empty?
14
14
 
15
15
  params.each do |k, v|
16
- out << ' ' << example_param(k, v, action.param_description(:input, k))
16
+ desc = action.param_description(:input, k)
17
+ next unless desc
18
+
19
+ out << ' ' << example_param(k, v, desc)
17
20
  end
18
21
  end
19
22
 
20
23
  out << "\n"
21
24
 
22
25
  # response
23
- cli.format_output(action, example[:response], out)
26
+ cli.format_output(
27
+ action,
28
+ {action.namespace(:output).to_sym => example[:response]},
29
+ out
30
+ )
24
31
  end
25
32
  end
26
33
 
@@ -2,7 +2,12 @@ require 'require_all'
2
2
  require 'date'
3
3
 
4
4
  module HaveAPI
5
- module Client ; end
5
+ module Client
6
+ # Shortcut to HaveAPI::Client::Client.new
7
+ def self.new(*args)
8
+ HaveAPI::Client::Client.new(*args)
9
+ end
10
+ end
6
11
  end
7
12
 
8
13
  require_rel 'client'
@@ -1,8 +1,10 @@
1
1
  module HaveAPI::Client
2
2
  class Action
3
+ attr_reader :client, :api, :name
3
4
  attr_accessor :resource_path
4
5
 
5
- def initialize(api, name, spec, args)
6
+ def initialize(client, api, name, spec, args)
7
+ @client = client
6
8
  @api = api
7
9
  @name = name
8
10
  @spec = spec
@@ -18,8 +20,7 @@ module HaveAPI::Client
18
20
  end
19
21
 
20
22
  ret = @api.call(self, params.to_api)
21
- @prepared_url = nil
22
- @prepared_help = nil
23
+ reset
23
24
  ret
24
25
  end
25
26
 
@@ -31,6 +32,10 @@ module HaveAPI::Client
31
32
  @spec[:auth]
32
33
  end
33
34
 
35
+ def blocking?
36
+ @spec[:blocking]
37
+ end
38
+
34
39
  def aliases(include_name = false)
35
40
  if include_name
36
41
  [@name] + @spec[:aliases]
@@ -83,6 +88,10 @@ module HaveAPI::Client
83
88
  @spec[dir][:parameters][name]
84
89
  end
85
90
 
91
+ def meta(scope)
92
+ @spec[:meta][scope]
93
+ end
94
+
86
95
  def url
87
96
  @spec[:url]
88
97
  end
@@ -117,9 +126,103 @@ module HaveAPI::Client
117
126
  @prepared_help = help
118
127
  end
119
128
 
129
+ def reset
130
+ @prepared_url = nil
131
+ @prepared_help = nil
132
+ end
133
+
120
134
  def update_description(spec)
121
135
  @spec = spec
122
136
  end
137
+
138
+ # Block until the action is completed or timeout occurs. If the block is given,
139
+ # it is regularly called with the action's state.
140
+ # @param interval [Float] how often should the action state be checked
141
+ # @param timeout [Integer] timeout in seconds
142
+ # @yieldparam state [ActionState]
143
+ # @return [Boolean] when the action is finished
144
+ # @return [nil] when timeout occurs
145
+ # @return [Response] if the action was cancelled and the cancel itself isn't blocking
146
+ # @return [Integer] id of cancellation if the action was cancelled, cancel is blocking
147
+ # and no cancel block is provided
148
+ def self.wait_for_completion(client, id, interval: 15, update_in: 3, timeout: nil)
149
+ res = client.action_state.show(id)
150
+ state = ActionState.new(res)
151
+
152
+ yield(state) if block_given?
153
+ return state.status if state.finished?
154
+
155
+ last = {}
156
+ t = Time.now if timeout
157
+
158
+ loop do
159
+ res = client.action_state.poll(
160
+ id,
161
+ timeout: interval,
162
+ update_in: update_in,
163
+ status: last[:status],
164
+ current: last[:current],
165
+ total: last[:total],
166
+ )
167
+
168
+ state = ActionState.new(res)
169
+
170
+ last[:status] = res.response[:status]
171
+ last[:current] = res.response[:current]
172
+ last[:total] = res.response[:total]
173
+
174
+ yield(state) if block_given?
175
+ break if state.finished?
176
+
177
+ if state.cancel?
178
+ state.stop
179
+ cancel_block = state.cancel_block
180
+
181
+ ret = cancel(client, id)
182
+
183
+ if ret.is_a?(Response)
184
+ # The cancel is not a blocking operation, return immediately
185
+ raise ActionFailed, ret unless ret.ok?
186
+ return ret
187
+ end
188
+
189
+ # Cancel is a blocking operation
190
+ if cancel_block
191
+ return wait_for_completion(
192
+ client,
193
+ ret,
194
+ interval: interval,
195
+ timeout: timeout,
196
+ update_in: update_in,
197
+ &cancel_block
198
+ )
199
+ end
200
+
201
+ return ret
202
+ end
203
+
204
+ return nil if (timeout && (Time.now - t) >= timeout) || state.stop?
205
+ end
206
+
207
+ state.status
208
+
209
+ rescue Interrupt => e
210
+ %i(show poll).each do |action|
211
+ client.action_state.actions[action].reset
212
+ end
213
+ raise e
214
+ end
215
+
216
+ def self.cancel(client, id)
217
+ res = client.action_state.cancel(id, meta: {block: false})
218
+
219
+ if res.ok? && res.action.blocking? && res.meta[:action_state_id]
220
+ res.meta[:action_state_id]
221
+
222
+ else
223
+ res
224
+ end
225
+ end
123
226
 
124
227
  private
125
228
  def apply_args(args)
@@ -0,0 +1,68 @@
1
+ module HaveAPI::Client
2
+ # Represents action's state as returned from API resource ActionState.Show/Poll.
3
+ class ActionState
4
+ Progress = Struct.new(:current, :total, :unit) do
5
+ def percent
6
+ 100.0 / total * current
7
+ end
8
+
9
+ def to_s
10
+ "#{current}/#{total} #{unit}"
11
+ end
12
+ end
13
+
14
+ attr_reader :progress
15
+
16
+ def initialize(response)
17
+ @data = response.response
18
+
19
+ @progress = Progress.new(@data[:current], @data[:total], @data[:unit])
20
+ end
21
+
22
+ def label
23
+ @data[:label]
24
+ end
25
+
26
+ def status
27
+ @data[:status] === true
28
+ end
29
+
30
+ def finished?
31
+ @data[:finished] === true
32
+ end
33
+
34
+ def can_cancel?
35
+ @data[:can_cancel] === true
36
+ end
37
+
38
+ # Stop monitoring the action's state and attempt to cancel it. The `block`
39
+ # is given to Action.wait_for_completion for the cancel operation. The block
40
+ # is used only if the cancel operation is blocking.
41
+ def cancel(&block)
42
+ unless can_cancel?
43
+ fail "action ##{@data[:id]} (#{label}) cannot be cancelled"
44
+ end
45
+
46
+ @cancel = true
47
+ @cancel_block = block
48
+ end
49
+
50
+ def cancel?
51
+ @cancel === true
52
+ end
53
+
54
+ def cancel_block
55
+ @cancel_block
56
+ end
57
+
58
+ # Stop monitoring the action's state, the call from Action.wait_for_completion
59
+ # will return.
60
+ def stop
61
+ @stop = true
62
+ end
63
+
64
+ def stop?
65
+ !@stop.nil? && @stop
66
+ end
67
+ end
68
+ end
@@ -36,7 +36,13 @@ module HaveAPI::Client::Authentication
36
36
 
37
37
  protected
38
38
  def request_token
39
- a = HaveAPI::Client::Action.new(@communicator, :request, @desc[:resources][:token][:actions][:request], [])
39
+ a = HaveAPI::Client::Action.new(
40
+ nil,
41
+ @communicator,
42
+ :request,
43
+ @desc[:resources][:token][:actions][:request],
44
+ []
45
+ )
40
46
  ret = a.execute({
41
47
  login: @opts[:user],
42
48
  password: @opts[:password],
@@ -8,16 +8,27 @@ class HaveAPI::Client::Client
8
8
  # The client by default uses the default version of the API.
9
9
  # API is asked for description only when needed or by calling #setup.
10
10
  # +identity+ is sent in each request to the API in User-Agent header.
11
- def initialize(url, v = nil, identity: 'haveapi-client', communicator: nil)
11
+ # @param url [String] API URL
12
+ # @param opts [Hash]
13
+ # @option opts [String] version
14
+ # @option opts [String] identity
15
+ # @option opts [HaveAPI::Client::Communicator] communicator
16
+ # @option opts [Boolean] block
17
+ # @option opts [Integer] block_interval
18
+ # @option opts [Integer] block_timeout
19
+ def initialize(url, opts = {})
12
20
  @setup = false
13
- @version = v
21
+ @opts = opts
22
+ @version = @opts[:version]
23
+ @opts[:identity] ||= 'haveapi-client'
24
+ @opts[:block] = true if @opts[:block].nil?
14
25
 
15
- if communicator
16
- @api = communicator
26
+ if @opts[:communicator]
27
+ @api = @opts[:communicator]
17
28
 
18
29
  else
19
- @api = HaveAPI::Client::Communicator.new(url, v)
20
- @api.identity = identity
30
+ @api = HaveAPI::Client::Communicator.new(url, @version)
31
+ @api.identity = @opts[:identity]
21
32
  end
22
33
  end
23
34
 
@@ -54,6 +65,22 @@ class HaveAPI::Client::Client
54
65
  @api.compatible?
55
66
  end
56
67
 
68
+ # return [Boolean] true if global blocking mode is enabled
69
+ def blocking?
70
+ @opts[:block]
71
+ end
72
+
73
+ # Override selected client options
74
+ # @param opts [Hash] options
75
+ def set_opts(opts)
76
+ @opts.update(opts)
77
+ end
78
+
79
+ # @return [Hash] client options
80
+ def opts(*keys)
81
+ keys.empty? ? @opts.clone : @opts.select { |k, _| keys.include?(k) }
82
+ end
83
+
57
84
  # Initialize the client if it is not yet initialized and call the resource
58
85
  # if it exists.
59
86
  def method_missing(symbol, *args)
@@ -97,7 +97,7 @@ module HaveAPI::Client
97
97
  end
98
98
 
99
99
  if a
100
- obj = Action.new(self, action, a, args)
100
+ obj = Action.new(nil, self, action, a, args)
101
101
  obj.resource_path = resources
102
102
  obj
103
103
  else
@@ -144,6 +144,9 @@ module HaveAPI::Client
144
144
  rescue RestClient::Forbidden
145
145
  return error('Access forbidden. Bad user name or password? Not authorized?')
146
146
 
147
+ rescue RestClient::ResourceNotFound => e
148
+ response = parse(e.http_body)
149
+
147
150
  rescue => e
148
151
  return error("Fatal API error: #{e.inspect}")
149
152
  end
@@ -1,3 +1,5 @@
1
+ require 'date'
2
+
1
3
  module HaveAPI::Client
2
4
  class Parameters::Typed
3
5
  module Boolean
@@ -52,7 +54,7 @@ module HaveAPI::Client
52
54
 
53
55
  elsif type == 'Datetime'
54
56
  begin
55
- Time.iso8601(raw)
57
+ DateTime.iso8601(raw).to_time
56
58
 
57
59
  rescue ArgumentError
58
60
  @errors << 'not in ISO 8601 format'
@@ -17,7 +17,7 @@ module HaveAPI::Client
17
17
  @description = description
18
18
 
19
19
  description[:actions].each do |name, desc|
20
- action = HaveAPI::Client::Action.new(@api, name, desc, [])
20
+ action = HaveAPI::Client::Action.new(@client, @api, name, desc, [])
21
21
  define_action(action)
22
22
  @actions[name] = action
23
23
  end
@@ -74,7 +74,8 @@ module HaveAPI::Client
74
74
  action.aliases(true).each do |name|
75
75
  next unless define_method?(action, name)
76
76
 
77
- define_singleton_method(name) do |*args|
77
+ define_singleton_method(name) do |*args, &block|
78
+ client_opts = @client.opts(:block, :block_interval, :block_timeout)
78
79
  all_args = @prepared_args + args
79
80
 
80
81
  if action.unresolved_args?
@@ -96,6 +97,14 @@ module HaveAPI::Client
96
97
  elsif all_args.last.is_a?(Hash)
97
98
  last = all_args.pop
98
99
 
100
+ if last.has_key?(:meta)
101
+ meta = last[:meta]
102
+
103
+ %i(block block_interval block_timeout).each do |p|
104
+ client_opts[p] = meta.delete(p) if meta.has_key?(p)
105
+ end
106
+ end
107
+
99
108
  all_args << default_action_input_params(action).update(last)
100
109
  end
101
110
 
@@ -103,7 +112,7 @@ module HaveAPI::Client
103
112
 
104
113
  raise ActionFailed.new(ret) unless ret.ok?
105
114
 
106
- case action.output_layout
115
+ return_value = case action.output_layout
107
116
  when :object
108
117
  ResourceInstance.new(@client, @api, self, action: action, response: ret)
109
118
 
@@ -116,6 +125,23 @@ module HaveAPI::Client
116
125
  else
117
126
  ret
118
127
  end
128
+
129
+ if action.blocking? && client_opts[:block]
130
+ wait_opts = {}
131
+
132
+ {
133
+ block_interval: :interval,
134
+ block_timeout: :timeout
135
+ }.each do |k, v|
136
+ wait_opts[v] = client_opts[k] if client_opts.has_key?(k)
137
+ end
138
+
139
+ ret.wait_for_completion(wait_opts) do |state|
140
+ block.call(return_value, state) if block
141
+ end
142
+ end
143
+
144
+ return_value
119
145
  end
120
146
  end
121
147
  end
@@ -22,11 +22,13 @@ module HaveAPI::Client
22
22
  if response.is_a?(Hash)
23
23
  @params = response
24
24
  @prepared_args = response[:_meta][:url_params]
25
+ @meta = response[:_meta] unless @meta
25
26
 
26
27
  else
27
28
  @response = response
28
29
  @params = response.response
29
30
  @prepared_args = response.meta[:url_params]
31
+ @meta = response.meta unless @meta
30
32
  end
31
33
 
32
34
  setup_from_clone(resource)
@@ -112,10 +114,13 @@ module HaveAPI::Client
112
114
  @resource_instances[name] = find_association(param, @params[name])
113
115
 
114
116
  # id reader
115
- ensure_method(:"#{name}_id") { @params[name][ param[:value_id].to_sym ] }
117
+ ensure_method(:"#{name}_id") do
118
+ @params[name] && @params[name][ param[:value_id].to_sym ]
119
+ end
116
120
 
117
121
  # id writer
118
122
  ensure_method(:"#{name}_id=") do |id|
123
+ @params[name] ||= {}
119
124
  @params[name][ param[:value_id].to_sym ] = id
120
125
 
121
126
  @resource_instances[name] = find_association(
@@ -139,6 +144,7 @@ module HaveAPI::Client
139
144
 
140
145
  # value writer
141
146
  ensure_method(:"#{name}=") do |obj|
147
+ @params[name] ||= {}
142
148
  @params[name][ param[:value_id].to_sym ] = obj.method(param[:value_id]).call
143
149
  @params[name][ param[:value_label].to_sym ] = obj.method(param[:value_label]).call
144
150
 
@@ -56,4 +56,15 @@ class HaveAPI::Client::Response
56
56
 
57
57
  @response[:response][@action.namespace(:output).to_sym].each
58
58
  end
59
+
60
+ # Block until the action is completed or timeout occurs. If the block is given,
61
+ # it is regularly called with the action's state.
62
+ #
63
+ # @see HaveAPI::Client::Action#wait_for_completion
64
+ def wait_for_completion(*args, &block)
65
+ id = meta[:action_state_id]
66
+ return nil unless id
67
+
68
+ HaveAPI::Client::Action.wait_for_completion(@action.client, id, *args, &block)
69
+ end
59
70
  end
@@ -1,6 +1,6 @@
1
1
  module HaveAPI
2
2
  module Client
3
- PROTOCOL_VERSION = '1.0'
4
- VERSION = '0.6.0'
3
+ PROTOCOL_VERSION = '1.1'
4
+ VERSION = '0.7.0'
5
5
  end
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: haveapi-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakub Skokan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-10-18 00:00:00.000000000 Z
11
+ date: 2016-11-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - ~>
109
109
  - !ruby/object:Gem::Version
110
110
  version: 1.7.8
111
+ - !ruby/object:Gem::Dependency
112
+ name: ruby-progressbar
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: 1.7.5
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ~>
123
+ - !ruby/object:Gem::Version
124
+ version: 1.7.5
111
125
  description: Ruby API and CLI for HaveAPI
112
126
  email:
113
127
  - jakub.skokan@vpsfree.cz
@@ -124,15 +138,18 @@ files:
124
138
  - bin/haveapi-cli
125
139
  - haveapi-client.gemspec
126
140
  - lib/haveapi/cli.rb
141
+ - lib/haveapi/cli/action_state.rb
127
142
  - lib/haveapi/cli/authentication/base.rb
128
143
  - lib/haveapi/cli/authentication/basic.rb
129
144
  - lib/haveapi/cli/authentication/token.rb
130
145
  - lib/haveapi/cli/cli.rb
131
146
  - lib/haveapi/cli/command.rb
147
+ - lib/haveapi/cli/commands/action_state_wait.rb
132
148
  - lib/haveapi/cli/example_formatter.rb
133
149
  - lib/haveapi/cli/output_formatter.rb
134
150
  - lib/haveapi/client.rb
135
151
  - lib/haveapi/client/action.rb
152
+ - lib/haveapi/client/action_state.rb
136
153
  - lib/haveapi/client/authentication/base.rb
137
154
  - lib/haveapi/client/authentication/basic.rb
138
155
  - lib/haveapi/client/authentication/noauth.rb
@@ -186,4 +203,3 @@ signing_key:
186
203
  specification_version: 4
187
204
  summary: Ruby API and CLI for HaveAPI
188
205
  test_files: []
189
- has_rdoc: