cloud_powers 0.2.7.23 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.test.env.example +6 -6
- data/.travis.yml +1 -1
- data/README +190 -0
- data/cloud_powers.gemspec +4 -4
- data/lib/cloud_powers.rb +3 -13
- data/lib/cloud_powers/aws_resources.rb +21 -4
- data/lib/cloud_powers/creatable.rb +122 -0
- data/lib/cloud_powers/helpers.rb +58 -0
- data/lib/cloud_powers/helpers/lang_help.rb +288 -0
- data/lib/cloud_powers/helpers/logic_help.rb +152 -0
- data/lib/cloud_powers/helpers/path_help.rb +90 -0
- data/lib/cloud_powers/node.rb +69 -68
- data/lib/cloud_powers/node/instance.rb +52 -0
- data/lib/cloud_powers/resource.rb +44 -0
- data/lib/cloud_powers/storage.rb +27 -14
- data/lib/{stubs → cloud_powers/stubs}/aws_stubs.rb +37 -14
- data/lib/cloud_powers/synapse/broadcast.rb +117 -0
- data/lib/cloud_powers/synapse/broadcast/channel.rb +44 -0
- data/lib/cloud_powers/synapse/pipe.rb +211 -0
- data/lib/cloud_powers/synapse/pipe/stream.rb +41 -0
- data/lib/cloud_powers/synapse/queue.rb +357 -0
- data/lib/cloud_powers/synapse/queue/board.rb +61 -95
- data/lib/cloud_powers/synapse/queue/poller.rb +29 -0
- data/lib/cloud_powers/synapse/synapse.rb +10 -12
- data/lib/cloud_powers/synapse/web_soc.rb +13 -0
- data/lib/cloud_powers/synapse/web_soc/soc_client.rb +52 -0
- data/lib/cloud_powers/synapse/web_soc/soc_server.rb +48 -0
- data/lib/cloud_powers/version.rb +1 -1
- data/lib/cloud_powers/zenv.rb +13 -12
- metadata +24 -13
- data/lib/cloud_powers/context.rb +0 -275
- data/lib/cloud_powers/delegator.rb +0 -113
- data/lib/cloud_powers/helper.rb +0 -453
- data/lib/cloud_powers/synapse/websocket/websocclient.rb +0 -53
- data/lib/cloud_powers/synapse/websocket/websocserver.rb +0 -46
- data/lib/cloud_powers/workflow_factory.rb +0 -160
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'syslog/logger'
|
3
|
+
require 'cloud_powers/helpers/lang_help'
|
4
|
+
require 'cloud_powers/helpers/logic_help'
|
5
|
+
require 'cloud_powers/helpers/path_help'
|
6
|
+
|
7
|
+
module Smash
|
8
|
+
module CloudPowers
|
9
|
+
module Helpers
|
10
|
+
# methods to help change convert between different cases, like the
|
11
|
+
# <tt>from_json</tt> and <tt>to_camel</tt> and other help with Ruby
|
12
|
+
include Smash::CloudPowers::LangHelp
|
13
|
+
# methods to help awareness, dynamic code and other such fun
|
14
|
+
include Smash::CloudPowers::LogicHelp
|
15
|
+
# methods to help find locations of files and directories. This provides
|
16
|
+
# common locations for code to reference.
|
17
|
+
include Smash::CloudPowers::PathHelp
|
18
|
+
|
19
|
+
# creates a default logger
|
20
|
+
#
|
21
|
+
# Parameters
|
22
|
+
# * log_to +String+ (optional) - location to send logging information to; default is STDOUT
|
23
|
+
#
|
24
|
+
# Returns
|
25
|
+
# +Logger+
|
26
|
+
#
|
27
|
+
# Notes
|
28
|
+
# * TODO: at least make this have overridable defaults
|
29
|
+
def create_logger(log_to = STDOUT)
|
30
|
+
logger = Logger.new(log_to)
|
31
|
+
logger.datetime_format = '%Y-%m-%d %H:%M:%S'
|
32
|
+
logger
|
33
|
+
end
|
34
|
+
|
35
|
+
# Gets the path from the environment and sets @log_file using the path
|
36
|
+
#
|
37
|
+
# Returns
|
38
|
+
# @log_file +String+
|
39
|
+
#
|
40
|
+
# Notes
|
41
|
+
# * See +#zfind()+
|
42
|
+
def log_file
|
43
|
+
@log_file ||= zfind('LOG_FILE')
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns An instance of Logger, cached as @logger@log_file path <String>
|
47
|
+
#
|
48
|
+
# Returns
|
49
|
+
# +Logger+
|
50
|
+
#
|
51
|
+
# Notes
|
52
|
+
# * See +#create_logger+
|
53
|
+
def logger
|
54
|
+
@logger ||= create_logger
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,288 @@
|
|
1
|
+
module Smash
|
2
|
+
module CloudPowers
|
3
|
+
module LangHelp
|
4
|
+
|
5
|
+
# Allows you to modify all keys, including nested, with a block that you pass.
|
6
|
+
# If no block is passed, a copy is returned.
|
7
|
+
#
|
8
|
+
# Parameters
|
9
|
+
# * params +Hash+|+Array+ - hash to be modified
|
10
|
+
# * +block+ (optional) - a block to be used to modify each key should
|
11
|
+
# modify the key and return that value so it can be used in the copy
|
12
|
+
#
|
13
|
+
# Returns
|
14
|
+
# +Hash+|+Array+ - a copy of the given Array or Hash, with all Hash keys modified
|
15
|
+
#
|
16
|
+
# Example
|
17
|
+
# hash = { 'foo' => 'v1', 'bar' => { fleep: { 'florp' => 'yo' } } }
|
18
|
+
# modify_keys_with(hash) { |key| key.to_sym }
|
19
|
+
# # => { foo: 'v1', bar: { fleep: { florp: 'yo' } } }
|
20
|
+
#
|
21
|
+
# Notes
|
22
|
+
# * see `#modify_keys_with()` for handling first-level keys
|
23
|
+
# * see `#pass_the_buck()` for the way nested structures are handled
|
24
|
+
# * case for different types taken from _MultiXML_ (multi_xml.rb)
|
25
|
+
# * TODO: look at optimization
|
26
|
+
def deep_modify_keys_with(params)
|
27
|
+
case params
|
28
|
+
when Hash
|
29
|
+
params.inject({}) do |carry, (k, v)|
|
30
|
+
carry.tap do |h|
|
31
|
+
if block_given?
|
32
|
+
key = yield k
|
33
|
+
|
34
|
+
value = if v.kind_of?(Hash)
|
35
|
+
deep_modify_keys_with(v) { |new_key| Proc.new.call(new_key) }
|
36
|
+
else
|
37
|
+
v
|
38
|
+
end
|
39
|
+
|
40
|
+
h[key] = value
|
41
|
+
else
|
42
|
+
h[k] = v
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
when Array
|
47
|
+
params.map{ |value| symbolize_keys(value) }
|
48
|
+
else
|
49
|
+
params
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Search through a +Hash+ without knowing if the key is a +String+ or
|
54
|
+
# +Symbol+. A +String+ modification that _normalizes_ each value to compare
|
55
|
+
# is used that is case insensitive. It only leaves word characters, not
|
56
|
+
# including underscore. After the value is found, if it exists and can
|
57
|
+
# be found, the +Hash+, minus that value, is returns in an +Array+ with
|
58
|
+
# the element you were searching for
|
59
|
+
#
|
60
|
+
# Parameters
|
61
|
+
# * key +String+|+Symbol+ - the key you are searching for
|
62
|
+
# * hash +Hash+ - the +Hash+ to search through and return a modified copy
|
63
|
+
# from
|
64
|
+
def find_and_remove(key, hash)
|
65
|
+
candidate_keys = hash.select do |k,v|
|
66
|
+
to_pascal(key).casecmp(to_pascal(k)) == 0
|
67
|
+
end.keys
|
68
|
+
|
69
|
+
interesting_value = hash.delete(candidate_keys.first)
|
70
|
+
[interesting_value, hash]
|
71
|
+
end
|
72
|
+
|
73
|
+
# Join the message and backtrace into a String with line breaks
|
74
|
+
#
|
75
|
+
# Parameters
|
76
|
+
# * error +Exception+
|
77
|
+
#
|
78
|
+
# Returns
|
79
|
+
# +String+
|
80
|
+
def format_error_message(error)
|
81
|
+
begin
|
82
|
+
[error.message, error.backtrace.join("\n")].join("\n")
|
83
|
+
rescue Exception
|
84
|
+
# if the formatting won't work, return the original exception
|
85
|
+
error
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Change valid JSON into a hash
|
90
|
+
#
|
91
|
+
# Parameter
|
92
|
+
# * var +String+
|
93
|
+
#
|
94
|
+
# Returns
|
95
|
+
# +Hash+ or +nil+ - +nil+ is returned if the JSON is invalid
|
96
|
+
def from_json(var)
|
97
|
+
begin
|
98
|
+
JSON.parse(var)
|
99
|
+
rescue JSON::ParserError, TypeError
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Allows you to modify all first-level keys with a block that you pass.
|
105
|
+
# If no block is passed, a copy is returned.
|
106
|
+
#
|
107
|
+
# Parameters
|
108
|
+
# * params +Hash+|+Array+
|
109
|
+
# * block (optional) - should modify the key and return that value so it can be used in the copy
|
110
|
+
#
|
111
|
+
# Returns
|
112
|
+
# +Hash+|+Array+ - a copy of the given Array or Hash, with all Hash keys modified
|
113
|
+
#
|
114
|
+
# Example
|
115
|
+
# hash = { 'foo' => 'v1', 'bar' => { fleep: { 'florp' => 'yo' } } }
|
116
|
+
# modify_keys_with(hash) { |k| k.to_sym }
|
117
|
+
# # => { :foo => 'v1', :bar => { fleep: { 'florp' => 'yo' } } }
|
118
|
+
#
|
119
|
+
# Notes
|
120
|
+
# * see +#deep_modify_keys_with()+ for handling nested keys
|
121
|
+
# * case for different types taken from _MultiXML_ (multi_xml.rb)
|
122
|
+
def modify_keys_with(params)
|
123
|
+
params.inject({}) do |carry, (k, v)|
|
124
|
+
carry.tap do |h|
|
125
|
+
key = block_given? ? (yield k) : k
|
126
|
+
h[key] = v
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Change strings into camelCase
|
132
|
+
#
|
133
|
+
# Parameters
|
134
|
+
# * var +String+
|
135
|
+
#
|
136
|
+
# Returns
|
137
|
+
# +String+
|
138
|
+
def to_camel(var)
|
139
|
+
var = var.to_s unless var.kind_of? String
|
140
|
+
step_one = to_snake(var)
|
141
|
+
step_two = to_pascal(step_one)
|
142
|
+
step_two[0, 1].downcase + step_two[1..-1]
|
143
|
+
end
|
144
|
+
|
145
|
+
# Change strings into a hyphen delimited phrase
|
146
|
+
#
|
147
|
+
# Parameters
|
148
|
+
# * var +String+
|
149
|
+
#
|
150
|
+
# Returns
|
151
|
+
# +String+
|
152
|
+
def to_hyph(var)
|
153
|
+
var = var.to_s unless var.kind_of? String
|
154
|
+
|
155
|
+
var.gsub(/:{2}|\//, '-').
|
156
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
157
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
158
|
+
gsub(/\s+/, '-').
|
159
|
+
tr("_", "-").
|
160
|
+
gsub(/^\W/, '').
|
161
|
+
downcase
|
162
|
+
end
|
163
|
+
|
164
|
+
# Assure your arguments are a +Hash+
|
165
|
+
#
|
166
|
+
# Parameters
|
167
|
+
# * start_point +Object+ - Best to start with a +Hash+, then a 2-D Array,
|
168
|
+
# then something +Enumerable+ that is at least Ordered
|
169
|
+
#
|
170
|
+
# Returns
|
171
|
+
# +Hash+
|
172
|
+
#
|
173
|
+
# Notes
|
174
|
+
# * If a +Hash+ is given, a copy is returned
|
175
|
+
# * If an +Array+ is given,
|
176
|
+
# * And if the +Array+ is a properly formatted, 2-D +Array+, <tt>to_h</tt>
|
177
|
+
# is called
|
178
|
+
# * Else <tt>Hash[<default_key argument>, <the thing we're trying to turn into a hash>]</tt>
|
179
|
+
def to_basic_hash(start_point, default_key: 'key')
|
180
|
+
case start_point
|
181
|
+
when Hash
|
182
|
+
start_point
|
183
|
+
when Enumerable
|
184
|
+
two_dimensional_elements = start_point.select do |value|
|
185
|
+
value.respond_to? :each
|
186
|
+
end
|
187
|
+
|
188
|
+
if two_dimensional_elements.count - start_point.count
|
189
|
+
start_point.to_h
|
190
|
+
else
|
191
|
+
Hash[default_key, start_point]
|
192
|
+
end
|
193
|
+
else
|
194
|
+
Hash[default_key, start_point]
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Change strings into an i-var format
|
199
|
+
#
|
200
|
+
# Parameters
|
201
|
+
# * var +String+
|
202
|
+
#
|
203
|
+
# Returns
|
204
|
+
# +String+
|
205
|
+
def to_i_var(var)
|
206
|
+
var = var.to_s unless var.kind_of? String
|
207
|
+
/^\W*@\w+/ =~ var ? to_snake(var) : "@#{to_snake(var)}"
|
208
|
+
end
|
209
|
+
|
210
|
+
# Change strings into PascalCase
|
211
|
+
#
|
212
|
+
# Parameters
|
213
|
+
# * var +String+
|
214
|
+
#
|
215
|
+
# Returns
|
216
|
+
# +String+
|
217
|
+
def to_pascal(var)
|
218
|
+
var = var.to_s unless var.kind_of? String
|
219
|
+
var.gsub(/^(.{1})|\W.{1}|\_.{1}/) { |s| s.gsub(/[^a-z0-9]+/i, '').capitalize }
|
220
|
+
end
|
221
|
+
|
222
|
+
# Change strings into a ruby_file_name with extension
|
223
|
+
#
|
224
|
+
# Parameters
|
225
|
+
# * var +String+
|
226
|
+
#
|
227
|
+
# Returns
|
228
|
+
# +String+
|
229
|
+
#
|
230
|
+
# Notes
|
231
|
+
# * given_string.rb
|
232
|
+
# * includes ruby file extension
|
233
|
+
# * see #to_snake()
|
234
|
+
def to_ruby_file_name(name)
|
235
|
+
return name if /\w+\.rb$/ =~ name
|
236
|
+
"#{to_snake(name)}.rb"
|
237
|
+
end
|
238
|
+
|
239
|
+
# Change strings into PascalCase
|
240
|
+
#
|
241
|
+
# Parameters
|
242
|
+
# * var +String+
|
243
|
+
#
|
244
|
+
# Returns
|
245
|
+
# +String+
|
246
|
+
#
|
247
|
+
# Notes
|
248
|
+
# * given_string
|
249
|
+
# * will not have file extensions
|
250
|
+
# * see #to_ruby_file_name()
|
251
|
+
def to_snake(var)
|
252
|
+
var = var.to_s unless var.kind_of? String
|
253
|
+
|
254
|
+
var.gsub(/:{2}|\//, '_').
|
255
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
256
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
257
|
+
gsub(/\s+/, '_').
|
258
|
+
tr("-", "_").
|
259
|
+
downcase
|
260
|
+
end
|
261
|
+
|
262
|
+
# Predicate method to check if a String is parsable, as JSON
|
263
|
+
#
|
264
|
+
# Parameters
|
265
|
+
# * json +String+
|
266
|
+
#
|
267
|
+
# Returns
|
268
|
+
# +Boolean+
|
269
|
+
#
|
270
|
+
# Notes
|
271
|
+
# * See <tt>from_json</tt>
|
272
|
+
def valid_json?(json)
|
273
|
+
!!from_json(json)
|
274
|
+
end
|
275
|
+
|
276
|
+
# Predicate method to check if a String is a valid URL
|
277
|
+
#
|
278
|
+
# Parameters
|
279
|
+
# * url +String+
|
280
|
+
#
|
281
|
+
# Returns
|
282
|
+
# +Boolean+
|
283
|
+
def valid_url?(url)
|
284
|
+
url =~ /\A#{URI::regexp}\z/
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module Smash
|
2
|
+
module CloudPowers
|
3
|
+
module LogicHelp
|
4
|
+
# Sets an Array of instance variables, individually to a value that a
|
5
|
+
# user given block returns.
|
6
|
+
#
|
7
|
+
# Parameters
|
8
|
+
#
|
9
|
+
# * keys +Array+
|
10
|
+
# * * each object will be used as the name for the instance variable that
|
11
|
+
# your block returns
|
12
|
+
# +block+ (optional)
|
13
|
+
# * this block is called for each object in the Array and is used as the value
|
14
|
+
# for the instance variable that is being named and created for each key
|
15
|
+
# Returns +Array+ - each object will either be the result of
|
16
|
+
# <tt>#instance_variable_set(key, value)</tt> => +value+
|
17
|
+
# or instance_variable_get(key)
|
18
|
+
# Example
|
19
|
+
# keys = ['foo', 'bar', 'yo']
|
20
|
+
#
|
21
|
+
# attr_map!(keys) { |key| sleep 1; "#{key}:#{Time.now.to_i}" }
|
22
|
+
# # => ['foo:1475434058', 'bar:1475434059', 'yo:1475434060']
|
23
|
+
#
|
24
|
+
# puts @bar
|
25
|
+
# # => 'bar:1475434059'
|
26
|
+
def attr_map(attributes)
|
27
|
+
attributes = [attributes, nil] unless attributes.respond_to? :map
|
28
|
+
|
29
|
+
attributes.inject(self) do |this, (attribute, before_value)|
|
30
|
+
first_place, second_place = yield attribute, before_value if block_given?
|
31
|
+
|
32
|
+
results = if second_place.nil?
|
33
|
+
[attribute, first_place]
|
34
|
+
else
|
35
|
+
[first_place, second_place]
|
36
|
+
end
|
37
|
+
|
38
|
+
this.instance_variable_set(to_i_var(results.first), results.last)
|
39
|
+
this
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Does its best job at guessing where this method was called from, in terms
|
44
|
+
# of where it is located on the file system. It helps track down where a
|
45
|
+
# project root is etc.
|
46
|
+
#
|
47
|
+
# Returns
|
48
|
+
# +String+
|
49
|
+
#
|
50
|
+
# Notes
|
51
|
+
# * Uses +$0+ to figure out what the current file is
|
52
|
+
def called_from
|
53
|
+
File.expand_path(File.dirname($0))
|
54
|
+
end
|
55
|
+
|
56
|
+
# Create an <tt>attr_accessor</tt> feeling getter and setter for an instance
|
57
|
+
# variable. The method doesn't create a getter or setter if it is already
|
58
|
+
# defined.
|
59
|
+
#
|
60
|
+
# Parameters
|
61
|
+
# * base_name +String+ - the name, without the '@' symbol
|
62
|
+
# # ok
|
63
|
+
# add_instance_attr_accessor('my_variable_name', my_value)
|
64
|
+
# => <your_instance @my_variable_name=my_value, ...>
|
65
|
+
# # not ok
|
66
|
+
# add_instance_attr_accessor('@#!!)', my_value)
|
67
|
+
# => <your_instance> <i>no new instance variable found</i>
|
68
|
+
# * value +Object+ - the actual instance variable that matches the +base_name+
|
69
|
+
#
|
70
|
+
# Returns
|
71
|
+
# * the value of the instance variable that matches the +base_name+ (first) argument
|
72
|
+
#
|
73
|
+
# Notes
|
74
|
+
# * if a matching getter or setter method can be found, this method won't
|
75
|
+
# stomp on it. nothing happens, in that case
|
76
|
+
# * if an appropriately named instance variable can't be found, the getter
|
77
|
+
# method will return nil until you set it again.
|
78
|
+
# * <b>it is the responsibility of you and me to make sure our variable names
|
79
|
+
# are valid, i.e. proper Ruby instance variable names
|
80
|
+
def instance_attr_accessor(base_name)
|
81
|
+
i_var_name = to_i_var(base_name)
|
82
|
+
getter_signature = to_snake(base_name)
|
83
|
+
setter_signature = "#{getter_signature}="
|
84
|
+
|
85
|
+
unless respond_to? getter_signature
|
86
|
+
define_singleton_method(getter_signature) do
|
87
|
+
instance_variable_get(i_var_name)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
unless respond_to? setter_signature
|
92
|
+
define_singleton_method(setter_signature) do |argument|
|
93
|
+
instance_variable_set(i_var_name, argument)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Lets you retry a piece of logic with 1 second sleep in between attempts
|
99
|
+
# until another bit of logic does what it's supposed to, kind of like
|
100
|
+
# continuing to poll something and doing something when a package is ready
|
101
|
+
# to be taken and processed.
|
102
|
+
#
|
103
|
+
# Parameters
|
104
|
+
# * allowed_attempts +Number+|+Infinity(default)+ - The number of times
|
105
|
+
# the loop should be allowed to...well, loop, before a failed retry occurs.
|
106
|
+
# * &test +Block+ - A predicate method or block of code that is callable
|
107
|
+
# is used to test if the block being retried is successful yet.
|
108
|
+
# * []
|
109
|
+
#
|
110
|
+
# Example
|
111
|
+
# check_stuff = lambda { |params| return true }
|
112
|
+
# smart_retry(3, check_stuff(params)) { do_stuff_that_needs_to_be_checked }
|
113
|
+
def smart_retry(test, allowed_attempts = Float::INFINITY)
|
114
|
+
result = yield if block_given?
|
115
|
+
tries = 1
|
116
|
+
until test.call(result) || tries >= allowed_attempts
|
117
|
+
result = yield if block_given?
|
118
|
+
tries += 1
|
119
|
+
sleep 1
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# This method provides a default overrideable message body for things like
|
124
|
+
# basic status updates.
|
125
|
+
#
|
126
|
+
# Parameters
|
127
|
+
# * instanceId +Hash+
|
128
|
+
#
|
129
|
+
# Notes
|
130
|
+
# * camel casing is used on the keys because most other languages prefer
|
131
|
+
# that and it's not a huge problem in ruby. Besides, there's some other
|
132
|
+
# handy methods in this module to get you through those issues, like
|
133
|
+
# +#to_snake()+ and or +#modify_keys_with()+
|
134
|
+
def update_message_body(opts = {})
|
135
|
+
# TODO: Better implementation of merging message bodies and config needed
|
136
|
+
unless opts.kind_of? Hash
|
137
|
+
update = opts.to_s
|
138
|
+
opts = {}
|
139
|
+
opts[:extraInfo] = { message: update }
|
140
|
+
end
|
141
|
+
updated_extra_info = opts.delete(:extraInfo) || {}
|
142
|
+
|
143
|
+
{
|
144
|
+
instanceId: @instance_id || 'none-aquired',
|
145
|
+
type: 'status-update',
|
146
|
+
content: 'running',
|
147
|
+
extraInfo: updated_extra_info
|
148
|
+
}.merge(opts)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|