esse 0.3.4 → 0.3.5

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
  SHA256:
3
- metadata.gz: e0d63306d9028972cd58998169513b15c761f1ecd6fe67d2d6d04a6eb995cdae
4
- data.tar.gz: f4877f14109d22c40dbec51eef17edc86d60231d488929cc773749deeb8cb6b4
3
+ metadata.gz: b6c9d974dbe345e36fc5b8f286185735377309176016d9662900cec7510dd788
4
+ data.tar.gz: 01ad7d8d5ee1d849e0dc67cdf1c812c7c44aa686e8b71af5f85137c75e911842
5
5
  SHA512:
6
- metadata.gz: ee0b45292911de3db131af146d212e36f94ad8782021c89e20da74813135ebff6c586996bd33e355bb6af0e190db6a3be34eba1aebb4fa51f75abb4adf4cf1a1
7
- data.tar.gz: 70c69e8941c07ec18f3d14164ef211308ea72752bc22e1fa6e20dc0578d6c43aa549e681cca1177b148c6e37b9d2420f21f5a9ba9da4414a70a6814196a3d2e4
6
+ metadata.gz: b67855b65130b69443a88fe5f033c5437066f84b3ee377169c3ecb8f10c336b1f58e02751aa7fda5393eab308fbd022cc440465cfaa759a29fc943cdc85dc2b3
7
+ data.tar.gz: c509f2960292c11144a0092befd0fd8c074faa341b0d4ffb028eb9819561fbf28d5af07c388b7712dcc7e576214fa679c009cd54757d364ca967674de643dedd
@@ -95,6 +95,13 @@ module Esse
95
95
  end
96
96
  print_message(stats.join(', ') + '.')
97
97
  end
98
+
99
+ def elasticsearch_reindex(event)
100
+ print_message '[%<runtime>s] Reindex from %<from>s to %<to>s successfuly completed',
101
+ from: colorize(event[:request].dig(:body, :source, :index), :bold),
102
+ to: colorize(event[:request].dig(:body, :dest, :index), :bold),
103
+ runtime: formatted_runtime(event[:runtime])
104
+ end
98
105
  end
99
106
  end
100
107
  end
@@ -16,14 +16,18 @@ module Esse
16
16
  DESC
17
17
  option :suffix, type: :string, default: nil, aliases: '-s', desc: 'Suffix to append to index name'
18
18
  option :import, type: :boolean, default: true, desc: 'Import documents before point alias to the new index'
19
+ option :reindex, type: :boolean, default: false, desc: 'Use _reindex API to import documents from the old index to the new index'
19
20
  option :optimize, type: :boolean, default: true, desc: 'Optimize index before import documents by disabling refresh_interval and setting number_of_replicas to 0'
21
+ option :settings, type: :hash, default: nil, desc: 'List of settings to pass to the index class. Example: --settings=refresh_interval:1s,number_of_replicas:0'
20
22
  def reset(*index_classes)
21
23
  require_relative 'index/reset'
22
- Reset.new(indices: index_classes, **options.to_h.transform_keys(&:to_sym)).run
24
+ opts = HashUtils.deep_transform_keys(options.to_h, &:to_sym)
25
+ if opts[:import] && opts[:reindex]
26
+ raise ArgumentError, 'You cannot use --import and --reindex together'
27
+ end
28
+ Reset.new(indices: index_classes, **opts).run
23
29
  end
24
30
 
25
- # @TODO Add reindex task to create a new index and import documents from the old index using _reindex API
26
-
27
31
  desc 'create *INDEX_CLASSES', 'Creates indices for the given classes'
28
32
  long_desc <<-DESC
29
33
  Creates index and applies mapping and settings for the given classes.
@@ -33,9 +37,11 @@ module Esse
33
37
  DESC
34
38
  option :suffix, type: :string, default: nil, aliases: '-s', desc: 'Suffix to append to index name'
35
39
  option :alias, type: :boolean, default: false, aliases: '-a', desc: 'Update alias after create index'
40
+ option :settings, type: :hash, default: nil, desc: 'List of settings to pass to the index class. Example: --settings=index.refresh_interval:-1,index.number_of_replicas:0'
36
41
  def create(*index_classes)
37
42
  require_relative 'index/create'
38
- Create.new(indices: index_classes, **options.to_h.transform_keys(&:to_sym)).run
43
+ opts = HashUtils.deep_transform_keys(options.to_h, &:to_sym)
44
+ Create.new(indices: index_classes, **opts).run
39
45
  end
40
46
 
41
47
  desc 'delete *INDEX_CLASSES', 'Deletes indices for the given classes'
@@ -58,9 +64,11 @@ module Esse
58
64
  desc 'update_settings *INDEX_CLASS', 'Closes the index for read/write operations, updates the index settings, and open it again'
59
65
  option :suffix, type: :string, default: nil, aliases: '-s', desc: 'Suffix to append to index name'
60
66
  option :type, type: :string, default: nil, aliases: '-t', desc: 'Document Type to update mapping for'
67
+ option :settings, type: :hash, default: nil, desc: 'List of settings to pass to the index class. Example: --settings=index.refresh_interval:-1,index.number_of_replicas:0'
61
68
  def update_settings(*index_classes)
62
69
  require_relative 'index/update_settings'
63
- UpdateSettings.new(indices: index_classes, **options.to_h.transform_keys(&:to_sym)).run
70
+ opts = HashUtils.deep_transform_keys(options.to_h, &:to_sym)
71
+ UpdateSettings.new(indices: index_classes, **opts).run
64
72
  end
65
73
 
66
74
  desc 'update_mapping *INDEX_CLASS', 'Create or update a mapping'
@@ -89,8 +97,8 @@ module Esse
89
97
  option :suffix, type: :string, default: nil, aliases: '-s', desc: 'Suffix to append to index name'
90
98
  option :context, type: :hash, default: {}, required: true, desc: 'List of options to pass to the index class'
91
99
  option :repo, type: :string, default: nil, alias: '-r', desc: 'Repository to use for import'
92
- option :eager_include_document_attributes, type: :string, default: nil, desc: 'Comma separated list of lazy document attributes to include to the bulk index request'
93
- option :lazy_update_document_attributes, type: :string, default: nil, desc: 'Comma separated list of lazy document attributes to bulk update after the bulk index request'
100
+ option :eager_include_document_attributes, type: :string, default: nil, desc: 'Comma separated list of lazy document attributes to include to the bulk index request. Or pass `true` to include all lazy attributes'
101
+ option :lazy_update_document_attributes, type: :string, default: nil, desc: 'Comma separated list of lazy document attributes to bulk update after the bulk index request Or pass `true` to include all lazy attributes'
94
102
  def import(*index_classes)
95
103
  require_relative 'index/import'
96
104
  opts = HashUtils.deep_transform_keys(options.to_h, &:to_sym)
@@ -13,8 +13,10 @@ Esse.configure do |config|
13
13
 
14
14
  # Global index settings
15
15
  # cluster.settings = {
16
- # number_of_shards: 5,
17
- # number_of_replicas: 0,
16
+ # index: {
17
+ # number_of_shards: 5,
18
+ # number_of_replicas: 0,
19
+ # }
18
20
  # }
19
21
 
20
22
  # Global index mappings
data/lib/esse/config.rb CHANGED
@@ -10,8 +10,11 @@ module Esse
10
10
  # cluster.index_prefix = 'backend'
11
11
  # cluster.client = Elasticsearch::Client.new
12
12
  # cluster.settings = {
13
- # number_of_shards: 2,
14
- # number_of_replicas: 0
13
+ # index: {
14
+ # number_of_shards: 2,
15
+ # number_of_replicas: 1
16
+ # },
17
+ # analysis: { ... }
15
18
  # }
16
19
  # cluster.mappings = {
17
20
  # dynamic_templates: [...]
data/lib/esse/document.rb CHANGED
@@ -100,7 +100,10 @@ module Esse
100
100
  def inspect
101
101
  attributes = %i[id routing source].map do |attr|
102
102
  value = send(attr)
103
- "#{attr}: #{value.inspect}" if value
103
+ next unless value
104
+ "#{attr}: #{value.inspect}"
105
+ rescue
106
+ nil
104
107
  end.compact.join(', ')
105
108
  attributes << " mutations: #{@__mutations__.inspect}" if @__mutations__
106
109
  "#<#{self.class.name || 'Esse::Document'} #{attributes}>"
data/lib/esse/events.rb CHANGED
@@ -56,5 +56,7 @@ module Esse
56
56
  register_event 'elasticsearch.exist'
57
57
  register_event 'elasticsearch.count'
58
58
  register_event 'elasticsearch.get'
59
+ register_event 'elasticsearch.reindex'
60
+ register_event 'elasticsearch.update_by_query'
59
61
  end
60
62
  end
@@ -236,6 +236,20 @@ module Esse
236
236
  count
237
237
  end
238
238
 
239
+ # Update documents by query
240
+ #
241
+ # @param options [Hash] Hash of paramenters that will be passed along to elasticsearch request
242
+ # @option [String, nil] :suffix The index suffix. Defaults to the nil.
243
+ #
244
+ # @return [Hash] The elasticsearch response hash
245
+ def update_by_query(suffix: nil, **options)
246
+ definition = {
247
+ index: index_name(suffix: suffix),
248
+ }.merge(options)
249
+ cluster.may_update_type!(definition)
250
+ cluster.api.update_by_query(**definition)
251
+ end
252
+
239
253
  protected
240
254
 
241
255
  def document?(doc)
@@ -26,10 +26,10 @@ module Esse
26
26
  #
27
27
  # @see http://www.elasticsearch.org/blog/changing-mapping-with-zero-downtime/
28
28
  # @see Esse::Transport#create_index
29
- def create_index(suffix: nil, body: nil, **options)
29
+ def create_index(suffix: nil, body: nil, settings: nil, **options)
30
30
  options = CREATE_INDEX_RESERVED_KEYWORDS.merge(options)
31
31
  name = build_real_index_name(suffix)
32
- definition = body || [settings_hash, mappings_hash].reduce(&:merge)
32
+ definition = body || [settings_hash(settings: settings), mappings_hash].reduce(&:merge)
33
33
 
34
34
  if options.delete(:alias) && name != index_name
35
35
  definition[:aliases] = { index_name => {} }
@@ -48,34 +48,38 @@ module Esse
48
48
  # @return [Hash] the elasticsearch response
49
49
  #
50
50
  # @see https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-open-close.html
51
- def reset_index(suffix: index_suffix, optimize: true, import: true, reindex: false, **options)
51
+ def reset_index(suffix: index_suffix, settings: nil, optimize: true, import: true, reindex: false, refresh: nil, **options)
52
52
  cluster.throw_error_when_readonly!
53
53
 
54
54
  suffix ||= Esse.timestamp
55
55
  suffix = Esse.timestamp while index_exist?(suffix: suffix)
56
56
 
57
- if optimize
58
- definition = [settings_hash, mappings_hash].reduce(&:merge)
57
+ if optimize && import
58
+ definition = [settings_hash(settings: settings), mappings_hash].reduce(&:merge)
59
59
  number_of_replicas = definition.dig(Esse::SETTING_ROOT_KEY, :index, :number_of_replicas)
60
60
  refresh_interval = definition.dig(Esse::SETTING_ROOT_KEY, :index, :refresh_interval)
61
61
  new_number_of_replicas = ((definition[Esse::SETTING_ROOT_KEY] ||= {})[:index] ||= {})[:number_of_replicas] = 0
62
62
  new_refresh_interval = ((definition[Esse::SETTING_ROOT_KEY] ||= {})[:index] ||= {})[:refresh_interval] = '-1'
63
63
  create_index(**options, suffix: suffix, alias: false, body: definition)
64
64
  else
65
- create_index(**options, suffix: suffix, alias: false)
65
+ create_index(**options, suffix: suffix, alias: false, settings: settings)
66
66
  end
67
67
 
68
68
  if index_exist? && aliases.none?
69
69
  cluster.api.delete_index(index: index_name)
70
70
  end
71
71
  if import
72
- import(**options, suffix: suffix)
73
- elsif reindex && (_from = indices_pointing_to_alias).any?
74
- # @TODO: Reindex using the reindex API
72
+ import(**options, suffix: suffix, refresh: refresh)
73
+ elsif reindex && (source_indexes = indices_pointing_to_alias).any?
74
+ reindex_kwargs = reindex.is_a?(Hash) ? reindex : {}
75
+ reindex_kwargs[:wait_for_completion] = true unless reindex_kwargs.key?(:wait_for_completion)
76
+ source_indexes.each do |from|
77
+ cluster.api.reindex(**options, body: { source: { index: from }, dest: { index: index_name(suffix: suffix) } }, refresh: refresh)
78
+ end
75
79
  end
76
80
 
77
- if optimize && number_of_replicas != new_number_of_replicas || refresh_interval != new_refresh_interval
78
- update_settings(suffix: suffix)
81
+ if optimize && import && number_of_replicas != new_number_of_replicas || refresh_interval != new_refresh_interval
82
+ update_settings(suffix: suffix, settings: settings)
79
83
  refresh(suffix: suffix)
80
84
  end
81
85
 
@@ -152,16 +156,17 @@ module Esse
152
156
  #
153
157
  # @param :suffix [String, nil] :suffix The index suffix
154
158
  # @see Esse::Transport#update_settings
155
- def update_settings(suffix: nil, **options)
159
+ def update_settings(suffix: nil, settings: nil, **options)
156
160
  response = nil
157
161
 
158
- settings = HashUtils.deep_transform_keys(settings_hash.fetch(Esse::SETTING_ROOT_KEY), &:to_s)
162
+ settings = HashUtils.deep_transform_keys(settings_hash(settings: settings).fetch(Esse::SETTING_ROOT_KEY), &:to_sym)
159
163
  if options[:body]
160
- settings = settings.merge(HashUtils.deep_transform_keys(options.delete(:body), &:to_s))
164
+ body = HashUtils.deep_transform_keys(options.delete(:body), &:to_sym)
165
+ settings = HashUtils.deep_merge(settings, body)
161
166
  end
162
- settings.delete('number_of_shards') # Can't change number of shards for an index
163
- settings['index']&.delete('number_of_shards')
164
- analysis = settings.delete('analysis')
167
+ settings.delete(:number_of_shards) # Can't change number of shards for an index
168
+ settings[:index]&.delete(:number_of_shards)
169
+ analysis = settings.delete(:analysis)
165
170
 
166
171
  if settings.any?
167
172
  response = cluster.api.update_settings(index: index_name(suffix: suffix), body: settings, **options)
@@ -4,9 +4,28 @@ module Esse
4
4
  # https://github.com/elastic/elasticsearch-ruby/blob/master/elasticsearch-api/lib/elasticsearch/api/actions/indices/put_settings.rb
5
5
  class Index
6
6
  module ClassMethods
7
- def settings_hash
7
+ # Elasticsearch supports passing index.* related settings directly in the body of the request.
8
+ # We are moving it to the index key to make it more explicit and to be the source-of-truth when merging settings.
9
+ # So the settings `{ number_of_shards: 1 }` will be transformed to `{ index: { number_of_shards: 1 } }`
10
+ INDEX_SIMPLIFIED_SETTINGS = %i[
11
+ number_of_shards
12
+ number_of_replicas
13
+ refresh_interval
14
+ ].freeze
15
+
16
+ def settings_hash(settings: nil)
8
17
  hash = setting.body
9
- { Esse::SETTING_ROOT_KEY => (hash.key?(Esse::SETTING_ROOT_KEY) ? hash[Esse::SETTING_ROOT_KEY] : hash) }
18
+ values = (hash.key?(Esse::SETTING_ROOT_KEY) ? hash[Esse::SETTING_ROOT_KEY] : hash)
19
+ values = HashUtils.explode_keys(values)
20
+ if settings.is_a?(Hash)
21
+ values = HashUtils.deep_merge(values, HashUtils.explode_keys(settings))
22
+ end
23
+ INDEX_SIMPLIFIED_SETTINGS.each do |key|
24
+ next unless values.key?(key)
25
+
26
+ (values[:index] ||= {}).merge!(key => values.delete(key))
27
+ end
28
+ { Esse::SETTING_ROOT_KEY => values }
10
29
  end
11
30
 
12
31
  # Define /_settings definition by each index.
@@ -18,7 +37,7 @@ module Esse
18
37
  #
19
38
  # class UserIndex < Esse::Index
20
39
  # settings {
21
- # number_of_replicas: 4,
40
+ # index: { number_of_replicas: 4 }
22
41
  # }
23
42
  # end
24
43
  #
@@ -47,5 +47,15 @@ module Esse
47
47
  end
48
48
  end
49
49
  end
50
+
51
+ def explode_keys(hash, separator = '.')
52
+ hash.each_with_object({}) do |(key, value), result|
53
+ is_symbol = key.is_a?(Symbol)
54
+ keys = key.to_s.split(separator)
55
+ last_key = keys.pop
56
+ current = keys.reduce(result) { |memo, k| memo[is_symbol ? k.to_sym : k] ||= {} }
57
+ current[is_symbol ? last_key.to_sym : last_key] = value
58
+ end
59
+ end
50
60
  end
51
61
  end
@@ -185,6 +185,83 @@ module Esse
185
185
  payload[:response] = coerce_exception { client.indices.put_settings(**opts) }
186
186
  end
187
187
  end
188
+
189
+ # Allows to copy documents from one index to another, optionally filtering the source
190
+ # documents by a query, changing the destination index settings, or fetching the
191
+ # documents from a remote cluster.
192
+ #
193
+ # @option arguments [Boolean] :refresh Should the affected indexes be refreshed?
194
+ # @option arguments [Time] :timeout Time each individual bulk request should wait for shards that are unavailable.
195
+ # @option arguments [String] :wait_for_active_shards Sets the number of shard copies that must be active before proceeding with the reindex operation. Defaults to 1, meaning the primary shard only. Set to `all` for all shard copies, otherwise set to any non-negative value less than or equal to the total number of copies for the shard (number of replicas + 1)
196
+ # @option arguments [Boolean] :wait_for_completion Should the request should block until the reindex is complete.
197
+ # @option arguments [Number] :requests_per_second The throttle to set on this request in sub-requests per second. -1 means no throttle.
198
+ # @option arguments [Time] :scroll Control how long to keep the search context alive
199
+ # @option arguments [Number|string] :slices The number of slices this task should be divided into. Defaults to 1, meaning the task isn't sliced into subtasks. Can be set to `auto`.
200
+ # @option arguments [Number] :max_docs Maximum number of documents to process (default: all documents)
201
+ # @option arguments [Hash] :headers Custom HTTP headers
202
+ # @option arguments [Hash] :body The search definition using the Query DSL and the prototype for the index request. (*Required*)
203
+ #
204
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html
205
+ def reindex(body:, **options)
206
+ throw_error_when_readonly!
207
+
208
+ Esse::Events.instrument('elasticsearch.reindex') do |payload|
209
+ payload[:request] = opts = options.merge(body: body)
210
+ payload[:response] = coerce_exception { client.reindex(**opts) }
211
+ end
212
+ end
213
+
214
+ # Performs an update on every document in the index without changing the source,
215
+ # for example to pick up a mapping change.
216
+ #
217
+ # @option arguments [List] :index A comma-separated list of index names to search; use `_all` or empty string to perform the operation on all indices (*Required*)
218
+ # @option arguments [String] :analyzer The analyzer to use for the query string
219
+ # @option arguments [Boolean] :analyze_wildcard Specify whether wildcard and prefix queries should be analyzed (default: false)
220
+ # @option arguments [String] :default_operator The default operator for query string query (AND or OR) (options: AND, OR)
221
+ # @option arguments [String] :df The field to use as default where no field prefix is given in the query string
222
+ # @option arguments [Number] :from Starting offset (default: 0)
223
+ # @option arguments [Boolean] :ignore_unavailable Whether specified concrete indices should be ignored when unavailable (missing or closed)
224
+ # @option arguments [Boolean] :allow_no_indices Whether to ignore if a wildcard indices expression resolves into no concrete indices. (This includes `_all` string or when no indices have been specified)
225
+ # @option arguments [String] :conflicts What to do when the update by query hits version conflicts? (options: abort, proceed)
226
+ # @option arguments [String] :expand_wildcards Whether to expand wildcard expression to concrete indices that are open, closed or both. (options: open, closed, hidden, none, all)
227
+ # @option arguments [Boolean] :lenient Specify whether format-based query failures (such as providing text to a numeric field) should be ignored
228
+ # @option arguments [String] :pipeline Ingest pipeline to set on index requests made by this action. (default: none)
229
+ # @option arguments [String] :preference Specify the node or shard the operation should be performed on (default: random)
230
+ # @option arguments [String] :q Query in the Lucene query string syntax
231
+ # @option arguments [List] :routing A comma-separated list of specific routing values
232
+ # @option arguments [Time] :scroll Specify how long a consistent view of the index should be maintained for scrolled search
233
+ # @option arguments [String] :search_type Search operation type (options: query_then_fetch, dfs_query_then_fetch)
234
+ # @option arguments [Time] :search_timeout Explicit timeout for each search request. Defaults to no timeout.
235
+ # @option arguments [Number] :size Deprecated, please use `max_docs` instead
236
+ # @option arguments [Number] :max_docs Maximum number of documents to process (default: all documents)
237
+ # @option arguments [List] :sort A comma-separated list of <field>:<direction> pairs
238
+ # @option arguments [List] :_source True or false to return the _source field or not, or a list of fields to return
239
+ # @option arguments [List] :_source_excludes A list of fields to exclude from the returned _source field
240
+ # @option arguments [List] :_source_includes A list of fields to extract and return from the _source field
241
+ # @option arguments [Number] :terminate_after The maximum number of documents to collect for each shard, upon reaching which the query execution will terminate early.
242
+ # @option arguments [List] :stats Specific 'tag' of the request for logging and statistical purposes
243
+ # @option arguments [Boolean] :version Specify whether to return document version as part of a hit
244
+ # @option arguments [Boolean] :version_type Should the document increment the version number (internal) on hit or not (reindex)
245
+ # @option arguments [Boolean] :request_cache Specify if request cache should be used for this request or not, defaults to index level setting
246
+ # @option arguments [Boolean] :refresh Should the affected indexes be refreshed?
247
+ # @option arguments [Time] :timeout Time each individual bulk request should wait for shards that are unavailable.
248
+ # @option arguments [String] :wait_for_active_shards Sets the number of shard copies that must be active before proceeding with the update by query operation. Defaults to 1, meaning the primary shard only. Set to `all` for all shard copies, otherwise set to any non-negative value less than or equal to the total number of copies for the shard (number of replicas + 1)
249
+ # @option arguments [Number] :scroll_size Size on the scroll request powering the update by query
250
+ # @option arguments [Boolean] :wait_for_completion Should the request should block until the update by query operation is complete.
251
+ # @option arguments [Number] :requests_per_second The throttle to set on this request in sub-requests per second. -1 means no throttle.
252
+ # @option arguments [Number|string] :slices The number of slices this task should be divided into. Defaults to 1, meaning the task isn't sliced into subtasks. Can be set to `auto`.
253
+ # @option arguments [Hash] :headers Custom HTTP headers
254
+ # @option arguments [Hash] :body The search definition using the Query DSL
255
+ #
256
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html
257
+ def update_by_query(index:, **options)
258
+ throw_error_when_readonly!
259
+
260
+ Esse::Events.instrument('elasticsearch.update_by_query') do |payload|
261
+ payload[:request] = opts = options.merge(index: index)
262
+ payload[:response] = coerce_exception { client.update_by_query(**opts) }
263
+ end
264
+ end
188
265
  end
189
266
 
190
267
  include InstanceMethods
data/lib/esse/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Esse
4
- VERSION = '0.3.4'
4
+ VERSION = '0.3.5'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: esse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcos G. Zimmermann
8
8
  autorequire:
9
9
  bindir: exec
10
10
  cert_chain: []
11
- date: 2024-07-18 00:00:00.000000000 Z
11
+ date: 2024-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: multi_json