orchestrate-rails 0.1.1

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.
@@ -0,0 +1,509 @@
1
+ module Orchestrate::Rails
2
+
3
+ require "active_model"
4
+
5
+ # Class to define rails models for Orchestrate.io DataBases-as-a-Service.
6
+ # The library provides a base class that, when subclassed to define a model,
7
+ # sets up a mapping between the model and an Orchestrate.io collection.
8
+ #
9
+ class Model < Orchestrate::Application::Record
10
+ include ::ActiveModel::Validations
11
+ include ::ActiveModel::Conversion
12
+ extend ::ActiveModel::Naming
13
+
14
+ validates_presence_of :id
15
+
16
+ # Creates instance variable for each model attribute defined by the schema.
17
+ # Initializes any attribute values that are passed in via the params hash
18
+ # and then initializes the @attributes instance variable.
19
+ #
20
+ # Any key/value pair in params whose key is not an attribute
21
+ # (i.e. :id or :define_collection_name) is passed on to the superclass.
22
+ #
23
+ def initialize(params={})super(params)
24
+ # Define accessor and query methods for each attribute.
25
+ attrs.each do |attribute|
26
+ self.class.send :attr_accessor, attribute
27
+ define_attr_query attribute
28
+ end
29
+
30
+ # Set instance variables and initialize @attributes with any
31
+ # attribute values that were passed in the params hash.
32
+ @attributes = {}
33
+ params.each do |k,v|
34
+ if attrs.include? k.to_s
35
+ send("#{k}=", v)
36
+ @attributes[k] = v
37
+ end
38
+ end
39
+ end
40
+
41
+ # -------------------------------------------------------------------------
42
+ # Instance methods
43
+
44
+ # Returns array of model attribute names.
45
+ def attrs
46
+ self.class.attrs
47
+ end
48
+
49
+ # Returns hash of key/value pairs.
50
+ def attributes
51
+ @attributes = Hash[attrs.map { |a| [a.to_sym, send("#{a}")] }]
52
+ # @attributes =
53
+ # Hash[attrs.map { |a| [a.to_sym, instance_variable_get("@#{a}")] }]
54
+ end
55
+
56
+ # :stopdoc:
57
+ def properties
58
+ self.class.properties
59
+ end
60
+ # :startdoc:
61
+
62
+ # -------------------------------------------------------------------------
63
+ # Public convenience methods - JMC
64
+
65
+ # Returns the collection name for the current instance.
66
+ def orchestrate_collection_name
67
+ ocollection
68
+ end
69
+
70
+ # Returns the <tt>ref</tt> value for the current instance.
71
+ # The <tt>ref</tt> value is an immutable value assigned to each
72
+ # version of primary_key data in an orchestrate.io collection.
73
+ def orchestrate_ref_value
74
+ __ref_value__
75
+ end
76
+
77
+ # -------------------------------------------------------------------------
78
+ # Instance methods to mimic some basic ActiveRecord-style functionality
79
+
80
+ #
81
+ def retval(status, obj=nil)
82
+ if status == true
83
+ (obj.blank?) ? self : obj
84
+ else
85
+ status
86
+ end
87
+ end
88
+ private :retval
89
+
90
+ # Returns the key/value data for the current instance.
91
+ def get
92
+ res = orchio_get
93
+ (res.success?) ? res.result.update_rails(self) : false
94
+ end
95
+
96
+ # Returns the key/value data for the current instance,
97
+ # found by <tt>ref</tt> value.
98
+ def get_by_ref(ref)
99
+ res = orchio_get_by_ref ref
100
+ (res.success?) ? res.result.update_rails(self) : false
101
+ end
102
+
103
+ # Updates the collection with current instance data,
104
+ # if the primary_key (id) does not exist in the collection.
105
+ #
106
+ # Returns the instance upon success; false upon failure.
107
+ def save_if_none_match
108
+ status = orchio_put_if_none_match(to_json_direct) if valid?
109
+ retval status
110
+ end
111
+
112
+
113
+ # Updates the collection with current instance data,
114
+ # if the <tt>ref</tt> value matches the <tt>ref</tt> value associated
115
+ # with the same primary_key in the collection.
116
+ #
117
+ # Returns the instance upon success; false upon failure.
118
+ def save_if_match
119
+ status = orchio_put_if_match(to_json_direct, __ref_value__) if valid?
120
+ retval status
121
+ end
122
+
123
+ # Calls #save_if_match.
124
+ def save
125
+ save_if_match
126
+ end
127
+
128
+ # Updates the collection with current instance data.
129
+ #
130
+ # Returns the instance upon success; false upon failure.
131
+ def save!
132
+ status = orchio_put(to_json_direct) if valid?
133
+ retval status
134
+ end
135
+
136
+ # Updates the attribute and calls #save_if_match.
137
+ def update_attribute(key, value)
138
+ instance_variable_set "@#{key}", value
139
+ save_if_match
140
+ end
141
+
142
+ # Updates the attributes and calls #save_if_match.
143
+ def update_attributes(key_value_pairs)
144
+ key_value_pairs.each { |k,v| instance_variable_set "@#{k}", v }
145
+ save_if_match
146
+ end
147
+
148
+ # :stopdoc:
149
+ # Updates the attribute and calls #save!. Use with caution!!!
150
+ def update_attribute!(key, value)
151
+ instance_variable_set "@#{key}", value
152
+ save!
153
+ end
154
+
155
+ # Updates the attributes and calls #save!. Use with caution!!!
156
+ def update_attributes!(key_value_pairs)
157
+ key_value_pairs.each { |k,v| instance_variable_set "@#{k}", v }
158
+ save!
159
+ end
160
+ # :startdoc:
161
+
162
+ # Deletes the current primary_key from the collection.
163
+ #
164
+ # Returns boolean status.
165
+ def destroy
166
+ orchio_delete
167
+ end
168
+
169
+ # Deletes the current primary_key and
170
+ # <b>purges all of its immutable data</b> from the collection.
171
+ #
172
+ # Returns boolean status.
173
+ def destroy!
174
+ orchio_purge
175
+ end
176
+
177
+ # -------------------------------------------------------------------------
178
+ # Instance methods for Orchestrate.io events
179
+
180
+ # :stopdoc:
181
+ def all_events
182
+ event_types.map { |etype| events(etype) }
183
+ end
184
+ # :startdoc:
185
+
186
+ # Returns array of event instances specified by event_type and timestamp range,
187
+ #
188
+ # where the timestamp range is specified as { :start => start, :end => end }
189
+ def events(event_type, timestamp={})
190
+ # orchio_get_events(event_type, timestamp).results.map { |odoc| odoc.to_event }
191
+ res = orchio_get_events(event_type, timestamp)
192
+ (res.success?) ? res.results.map { |odoc| odoc.to_event } : false
193
+ end
194
+
195
+ # Updates the collection with the specified event instance.
196
+ #
197
+ # Returns the event instance upon success; false upon failure.
198
+ def save_event(event_type, timestamp=nil, event)
199
+ retval orchio_put_event(event_type, timestamp, event.to_json), event
200
+ end
201
+
202
+ # Creates event instance; calls #save_event to update the collection.
203
+ #
204
+ # Returns the event instance upon success; false upon failure.
205
+ def create_event(event_type, timestamp=nil, event_rec)
206
+ save_event event_type, timestamp, Event.new(event_rec)
207
+ end
208
+
209
+ # -------------------------------------------------------------------------
210
+ # Instance methods for Orchestrate.io graphs
211
+
212
+ # :stopdoc:
213
+ def all_graphs
214
+ graphs.map { |kind| graph kind }
215
+ end
216
+ # :startdoc:
217
+
218
+ # Returns array of instances associated with the specified kind of relation.
219
+ def graph(kind)
220
+ # orchio_get_graph(kind).results.map { |odoc| odoc.to_rails }
221
+ res = orchio_get_graph(kind)
222
+ (res.success?) ? res.results.map { |odoc| odoc.to_rails } : false
223
+ end
224
+
225
+ # Updates the collection with the specified graph relation.
226
+ #
227
+ # Returns boolean status.
228
+ def save_graph(kind, to_collection, to_key)
229
+ retval orchio_put_graph kind, to_collection, to_key
230
+ end
231
+
232
+ # Removes the specified relation from the graph.
233
+ #
234
+ # Returns boolean status.
235
+ def delete_graph(kind, to_collection, to_key)
236
+ retval orchio_delete_graph kind, to_collection, to_key
237
+ end
238
+
239
+ # -------------------------------------------------------------------------
240
+ # Helper methods
241
+
242
+ # Defines query method to test whether the attribute value is present.
243
+ def define_attr_query(attribute)
244
+ self.class.send :define_method, "#{attribute}?" do
245
+ !instance_variable_get("@#{attribute}").blank?
246
+ end
247
+ end
248
+ private :define_attr_query
249
+
250
+ # Generates json for the attribute key/value pairs, replacing attribute
251
+ # names used by the model with the property names used in the collection.
252
+ def to_json_direct
253
+ Hash[attrs.map { |a| [qmap[a], instance_variable_get("@#{a}")] }].to_json
254
+ end
255
+ private :to_json_direct
256
+
257
+ # :stopdoc:
258
+ def to_properties(json_str)
259
+ keys = json_str.scan(/\"(\w+)\":/).map { |result| result.first }
260
+ keys.each { |key| json_str.gsub!(/\"#{key}\":/, "\"#{qmap[key]}\":") }
261
+ json_str
262
+ end
263
+
264
+ def qmap
265
+ self.class.qmap
266
+ end
267
+ # :startdoc:
268
+
269
+ # -------------------------------------------------------------------------
270
+ # Class methods
271
+
272
+ # Returns the table that maps attribute names to property names.
273
+ #
274
+ # This mapping also works when the input is a property name.
275
+ def self.qmap
276
+ schema.qmap ocollection
277
+ end
278
+
279
+ # Returns array of model attribute names.
280
+ def self.attrs
281
+ schema.attrs ocollection
282
+ end
283
+
284
+ def self.attributes
285
+ new.attributes
286
+ end
287
+
288
+ # Returns array of property names.
289
+ #
290
+ # Property names are the original key names in json documents stored
291
+ # in orchestrate collections - before they are converted to the snake_case
292
+ # style used for model attribute names. Attribute names are mapped to
293
+ # property names using ::qmap. Property names can be mapped to
294
+ # attribute names with ::Symbol#to_orchio_rails_attr or
295
+ # ::String#to_orchio_rails_attr.
296
+ #
297
+ def self.properties
298
+ schema.properties ocollection
299
+ end
300
+
301
+ # -------------------------------------------------------------------------
302
+ # Class methods to mimic some basic ActiveRecord-style functionality
303
+
304
+ # Creates a new instance; updates the collection if the primary_key
305
+ # is not already present in the collection.
306
+ #
307
+ # Returns the instance upon success; false upon failure.
308
+ def self.create(key_value_pairs)
309
+ new(key_value_pairs).save_if_none_match
310
+ end
311
+
312
+ # Creates a new instance; updates the collection.
313
+ #
314
+ # Returns the instance upon success; false upon failure.
315
+ def self.create!(key_value_pairs)
316
+ new(key_value_pairs).save!
317
+ end
318
+
319
+ # Delete the specified instance from the collection.
320
+ #
321
+ # Returns boolean status.
322
+ def self.destroy(id)
323
+ new(:id => id).destroy
324
+ end
325
+
326
+ # Delete the entire collection.
327
+ #
328
+ # Returns boolean status.
329
+ def self.destroy_all
330
+ orchio_delete ocollection
331
+ end
332
+
333
+ # Returns ordered array of all instances in the collection.
334
+ def self.all
335
+ res = list
336
+ (res.success?) ? res.results : false
337
+ end
338
+
339
+ # Returns the first (ordered) instance in the collection.
340
+ def self.first
341
+ res = list(1)
342
+ (res.success?) ? res.results.first : false
343
+ end
344
+
345
+ # Returns the last (ordered) instance in the collection.
346
+ def self.last
347
+ all.last
348
+ end
349
+
350
+ # Returns the first (random) instance in the collection.
351
+ def self.take
352
+ res = search('*', 1)
353
+ (res.success?) ? res.results.first : false
354
+ end
355
+
356
+ # Returns boolean to indicate whether the specified primary_key exists
357
+ # in the collection.
358
+ def self.exists?(id)
359
+ find(id) ? true : false
360
+ end
361
+
362
+ # Find by the primary_key (id). Can be a specific id, or an array of ids.
363
+ #
364
+ # Returns instance, or array of instances, accordingly.
365
+ def self.find(arg)
366
+ if arg.is_a? Integer or arg.is_a? String
367
+ new(:id => arg).get
368
+ elsif arg.is_a? Array
369
+ arg.map { |id| new(:id => id).get }
370
+ end
371
+ end
372
+
373
+ # Returns all instances that match the specified criteria.
374
+ def self.where(key_value_pairs)
375
+ search(key_value_pairs.map{ |k,v| "#{k}:#{v}" }.join(' AND ')).results
376
+ end
377
+
378
+ # Returns the first instance that matches the specified criteria.
379
+ def self.find_by(key_value_pairs)
380
+ where(key_value_pairs).first
381
+ end
382
+
383
+ # Calls ::find_by for properly constructed 'find_by_attribute(s)' calls.
384
+ #
385
+ # Example: <tt>User.find_by_name_and_address(name, address)</tt>
386
+ # is executed as:
387
+ #
388
+ # <tt>User.find_by(:name => name, :address => address)</tt>
389
+ def self.find_by_method(myattrs, *args, &block)
390
+ attrs_with_args = [myattrs.split('_and_'), args].transpose
391
+ attrs_with_args.each { |awa| return unless attrs.include? awa.first }
392
+ find_by Hash[attr_with_args]
393
+ end
394
+
395
+ # Calls ::find_by_method for 'find_by_attribute(s)'.
396
+ def self.method_missing(name, *args, &block)
397
+ if name.to_s =~ /^find_by_(.+)$/
398
+ find_by_method($1, *args, &block) || super
399
+ else
400
+ super
401
+ end
402
+ end
403
+
404
+ # Handles find_by_attribute methods.
405
+ def self.respond_to?(name)
406
+ (attributes.include?(name) and name.to_s =~ /^find_by_.*$/) or super
407
+ end
408
+
409
+ # -----------------------------------------------------------------------
410
+
411
+ # Returns array of model instances that match the query string.
412
+ def self.search_results(query_str)
413
+ res = search(query_str)
414
+ (res.success?) ? res.results : false
415
+ end
416
+
417
+ # Returns SearchResult. Orchestrate.io search implements Lucene Query
418
+ # Parser Syntax.
419
+ def self.search(query_str, limit=:all, offset=0)
420
+ query_str = orchio_query_str(query_str) unless query_str == '*'
421
+ if limit == :all
422
+ total_count = offset + 1
423
+ max = 100
424
+ qdocs = []
425
+ while offset < total_count
426
+ qresult = orchio_search(
427
+ ocollection, "#{query_str}&limit=#{max}&offset=#{offset}"
428
+ )
429
+ offset += qresult.count
430
+ total_count = qresult.total_count
431
+ qdocs += qresult.results
432
+ end
433
+ qresult = SearchResult.new(
434
+ results: qdocs,
435
+ count: offset,
436
+ total_count: total_count,
437
+ status: qresult.status,
438
+ response: qresult.response
439
+ )
440
+ else
441
+ qresult = orchio_search(
442
+ ocollection, "#{query_str}&limit=#{limit}&offset=#{offset}"
443
+ )
444
+ end
445
+ qresult.results.map! { |odoc| odoc.to_rails }
446
+ qresult
447
+ end
448
+
449
+ # Returns Orchestrate::Application::Result.
450
+ #
451
+ # The start index may be optionally specified by using either start_key
452
+ # (inclusive) or after_key (exclusive). The default is the first primary
453
+ # key in the collection.
454
+ def self.list(limit=:all, start_key=nil, after_key=nil)
455
+ if limit == :all
456
+ total_count = 0
457
+ max = 100
458
+ result = orchio_list ocollection, "?limit=#{max}"
459
+ count = result.count
460
+ docs = result.results
461
+ while result.next
462
+ result = orchio_list ocollection, "?#{result.next.split('?').last}"
463
+ docs += result.results
464
+ count += result.count
465
+ end
466
+ result = Orchestrate::Application::Result.new(
467
+ results: docs,
468
+ count: count,
469
+ status: result.status,
470
+ response: result.response
471
+ )
472
+ else
473
+ option_str = "?limit=#{limit}"
474
+ option_str += "&startKey=#{start_key}" if start_key and after_key.nil?
475
+ option_str += "&afterKey=#{after_key}" if after_key and start_key.nil?
476
+ result = orchio_list ocollection, option_str
477
+ end
478
+ result.results.map! { |odoc| odoc.to_rails }
479
+ result
480
+ end
481
+
482
+ # :stopdoc:
483
+ def self.ocollection
484
+ schema.get_collection_from_class(self.name).name
485
+ end
486
+ # :startdoc:
487
+
488
+ # -----------------------------------------------------------------------
489
+
490
+ private
491
+
492
+ # Returns handle to the Schema singleton instance.
493
+ def self.schema
494
+ @@schema ||= Schema.instance
495
+ end
496
+
497
+ # Returns an orchestrate-ready query string for the search request.
498
+ # The attribute names used by the model are replaced with the property
499
+ # names used in the collection.
500
+ def self.orchio_query_str(query_str)
501
+ keys = query_str.scan(/^(\w+):/).map { |r| r.first } +
502
+ query_str.scan(/\s(\w+):/).map { |r| r.first }
503
+ keys.each { |k| query_str.gsub!(/#{k}:/, "#{qmap[k]}:") if qmap[k] }
504
+ query_str
505
+ end
506
+
507
+ end
508
+
509
+ end
@@ -0,0 +1,104 @@
1
+ module Orchestrate::Rails
2
+
3
+ # Rails-specific extensions to parent class.
4
+ #
5
+ class SchemaCollection < Orchestrate::Application::SchemaCollection
6
+ attr_accessor :attr_map, :qmap, :classname, :classpath
7
+ end
8
+
9
+ # Extensions to parent class for mapping data between orchestrate and rails.
10
+ #
11
+ class Schema < Orchestrate::Application::Schema
12
+ include Singleton
13
+
14
+ def properties(collection_name)
15
+ get_collection(collection_name).properties
16
+ end
17
+
18
+ def attrs(collection_name)
19
+ get_collection(collection_name).attr_map.keys
20
+ end
21
+
22
+ # Returns the table that maps attribute names to property names.
23
+ #
24
+ # Property names are the original key names in json documents stored
25
+ # in orchestrate collections - before they are converted to the snake_case
26
+ # style used for model attribute names. Property names can be mapped to
27
+ # attribute names by using ::String#to_orchio_rails_attr or
28
+ # ::Symbol#to_orchio_rails_attr.
29
+ def attr_map(collection_name)
30
+ get_collection(collection_name).attr_map
31
+ end
32
+
33
+ # Returns a table that maps attribute names to property names. And the
34
+ # mapping conveniently works even when the attribute name is actually
35
+ # a property name.
36
+ def qmap(collection_name)
37
+ get_collection(collection_name).qmap ||= build_query_map(collection_name)
38
+ end
39
+
40
+ def classname(collection_name)
41
+ get_collection(collection_name).classname
42
+ end
43
+
44
+ def fullclassname(collection_name)
45
+ c = get_collection(collection_name)
46
+ c.classpath.nil? ? c.classname : "#{c.classpath}::#{c.classname}"
47
+ end
48
+
49
+ def get_collection_from_class(classname)
50
+ classname = classname.split('::').last
51
+ collections.values.each { |c| return c if c.classname == classname }
52
+ end
53
+
54
+ def build_query_map(collection)
55
+ q_map = {}
56
+ attr_map(collection).values.map { |a|
57
+ q_map.merge!(a => a, a.to_orchio_rails_attr => a)
58
+ }.last
59
+ end
60
+ private :build_query_map
61
+
62
+ # -------------------------------------------------------------------------
63
+ # Class methods to facilitate loading schema definition at rails startup,
64
+ # via definition file 'db/schema.rb' loaded in 'config/application.rb.'
65
+
66
+ #
67
+ def self.load(schema)
68
+ require Rails.root.join schema
69
+ end
70
+
71
+ # args: { :name, :properties, :event_types, :graphs, :classname }
72
+ #
73
+ def self.define_collection(args)
74
+ # puts "DEF_COLL: '#{args.inspect}'"
75
+ coll = instance.load_collection SchemaCollection.new(args) { |c|
76
+ c.classname = args[:classname]
77
+ c.classpath = args[:classpath] #unless args[:classpath].blank?
78
+ c.attr_map = build_attr_map args[:properties]
79
+ }
80
+ # puts "DEF_COLL: '#{coll.classpath}'"
81
+ end
82
+
83
+ def self.define_event_type(args)
84
+ # puts "DEFINE: inst = '#{instance}', '#{args[:collection]}'" #JMC
85
+ instance.define_event_type(
86
+ args[:collection], args[:event_type], args[:properties]
87
+ )
88
+ end
89
+
90
+ def self.define_graph(args)
91
+ instance.define_graph(
92
+ args[:collection], args[:relation_kind], args[:to_collection]
93
+ )
94
+ end
95
+
96
+ private
97
+ def self.build_attr_map(attrs)
98
+ attr_map = {}
99
+ attrs.map { |a| attr_map.merge!(a.to_orchio_rails_attr => a.to_s) }.last
100
+ end
101
+ end
102
+
103
+ end
104
+
@@ -0,0 +1,9 @@
1
+ module Orchestrate::Rails
2
+
3
+ # Handle search results. See parent class Orchestrate::Application::SearchResult.
4
+ #
5
+ class SearchResult < Orchestrate::Application::SearchResult
6
+ attr_writer :results
7
+ end
8
+
9
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: orchestrate-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - James Carrasquer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-02-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: orchestrate-api
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Gem to define rails models for orchestrate.io
28
+ email: jimcar@aracnet.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - lib/orchestrate-application.rb
34
+ - lib/orchestrate_application/schema_collection.rb
35
+ - lib/orchestrate_application/schema.rb
36
+ - lib/orchestrate_application/connect.rb
37
+ - lib/orchestrate_application/record.rb
38
+ - lib/orchestrate_application/document.rb
39
+ - lib/orchestrate_application/result.rb
40
+ - lib/orchestrate_application/response.rb
41
+ - lib/orchestrate_application/simple_cache_store.rb
42
+ - lib/orchestrate_application/simple_cache_request.rb
43
+ - lib/orchestrate_application/simple_cache_response.rb
44
+ - lib/orchestrate-rails.rb
45
+ - lib/orchestrate_rails/document.rb
46
+ - lib/orchestrate_rails/schema.rb
47
+ - lib/orchestrate_rails/model.rb
48
+ - lib/orchestrate_rails/event.rb
49
+ - lib/orchestrate_rails/search_result.rb
50
+ - lib/orchestrate_rails/extensions.rb
51
+ homepage: https://github.com/jimcar/orchestrate-rails
52
+ licenses:
53
+ - MIT
54
+ metadata: {}
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubyforge_project:
71
+ rubygems_version: 2.0.3
72
+ signing_key:
73
+ specification_version: 4
74
+ summary: Summary for orchestrate-rails
75
+ test_files: []