classroom 0.0.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.
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ task :default => [:test_units]
5
+
6
+ desc "Run basic tests"
7
+ Rake::TestTask.new("test_units") do |t|
8
+ t.pattern = 'test/*_test.rb'
9
+ t.verbose = true
10
+ t.warning = true
11
+ end
12
+
13
+ task :run_server do
14
+ system 'ruby -Ilib examples/server.rb'
15
+ end
data/classroom.gemspec ADDED
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+
3
+ spec = Gem::Specification.new do |s|
4
+ s.name = 'classroom'
5
+ s.version = "0.0.1"
6
+ s.platform = Gem::Platform::RUBY
7
+ s.summary = "ClassRoom is a 'class server' based on DRb"
8
+ s.files = Dir.glob("**/**/**").delete_if {|item| item.include?(".svn")}
9
+ s.test_files = Dir.glob("test/*_test.rb")
10
+ s.require_path = 'lib'
11
+ s.autorequire = 'classroom'
12
+ s.author = "Peter Cooper"
13
+ s.email = "coops@petercooper.co.uk"
14
+ s.rubyforge_project = "classroom"
15
+ s.homepage = "http://classroom.rubyforge.org"
16
+ end
@@ -0,0 +1,35 @@
1
+ # Add a basic class to the ClassRoom server and use it
2
+
3
+ require 'rubygems'
4
+ require 'classroom'
5
+
6
+ # A basic 'queue' class with a fixed length queue and
7
+ # push and pull methods to use the queue.
8
+ klass = %q{
9
+ class BasicQueue
10
+ def initialize(size = 10); @size = size; @data = Array.new; end
11
+ def push(item); @data.length < @size ? (@data << item) && true : nil; end
12
+ def pull; @data.shift; end
13
+ def self.dummy_class_method; (1..10).to_a; end
14
+ end
15
+ }
16
+
17
+ # Here's the magic..
18
+ class_server = ClassRoom::Client.new(ARGV.first || 'classroom://:2001')
19
+ class_server.add_class(klass)
20
+ class_server.load_class(binding, :all)
21
+
22
+ # Create and use objects as you would normally, but remember
23
+ # 'BasicQueue' actually exists on the ClassRoom server and,
24
+ # if you wished, could be changed mid-execution by another
25
+ # client!
26
+ q = BasicQueue.new(5)
27
+ 10.times { |i| q.push("test #{i}") }
28
+ 10.times { puts q.pull }
29
+
30
+ r = BasicQueue.new(10)
31
+ 10.times { |i| r.push("test #{i}") }
32
+ 10.times { puts r.pull }
33
+
34
+ # Yes, class methods work out of the box too!
35
+ puts BasicQueue.dummy_class_method.inspect
@@ -0,0 +1,20 @@
1
+ # This client will run ONLY IF the BasicQueue class exists on the
2
+ # ClassRoom server. If you ran demo_client.rb first, this should be
3
+ # fine.
4
+
5
+ # This client should return exactly the same as demo_client.rb,
6
+ # proving the class is running entirely off of the ClassRoom server.
7
+
8
+ require 'rubygems'
9
+ require 'classroom'
10
+
11
+ class_server = ClassRoom::Client.new(ARGV.first || 'classroom://:2001')
12
+ class_server.load_class(binding, :BasicQueue)
13
+
14
+ q = BasicQueue.new(5)
15
+ 10.times { |i| q.push("test #{i}") }
16
+ 10.times { puts q.pull }
17
+
18
+ r = BasicQueue.new(10)
19
+ 10.times { |i| r.push("test #{i}") }
20
+ 10.times { puts r.pull }
@@ -0,0 +1,32 @@
1
+ # A basic demo with a basic class
2
+
3
+ require 'rubygems'
4
+ require 'classroom'
5
+
6
+ # A basic class whose function you can mostly ignore
7
+ klass = %q{
8
+ class BasicDataClass
9
+ def initialize(params); @data = params; end
10
+ def method_missing(method, *args); @data[method]; end
11
+ def size; @data.size; end
12
+ def self.dummy_class_method; (1..10).to_a; end
13
+ end
14
+ }
15
+
16
+ # Connect to the ClassRoom server and upload the class
17
+ class_server = ClassRoom::Client.new(ARGV.first || 'classroom://:2001')
18
+ class_server.add_class(klass)
19
+
20
+ # Load local references to all the classes on the ClassRoom server,
21
+ # including BasicDataClass
22
+ class_server.load_class(binding, :all)
23
+
24
+ # Create and use objects as you would normally, but remember
25
+ # 'BasicDataClass' actually exists on the ClassRoom server
26
+ c = BasicDataClass.new(:chunky => "Bacon", :roger => "Rabbit")
27
+ puts c.class # => BasicDataClass
28
+ puts c.chunky # => Bacon
29
+ puts c.size # => 2
30
+
31
+ # Yes, class methods work out of the box too!
32
+ puts BasicDataClass.dummy_class_method
@@ -0,0 +1,49 @@
1
+ # A demonstration of nested classes all working perfectly..
2
+ # (we hope!)
3
+
4
+ require 'rubygems'
5
+ require 'classroom'
6
+
7
+ # A basic 'queue' class with a fixed length queue and
8
+ # push and pull methods to use the queue.
9
+ klass = %q{
10
+ class BasicParent
11
+ def self.give_child
12
+ BasicParent::BasicChild.new
13
+ end
14
+ def self.x
15
+ "PARENT CLASS METHOD X"
16
+ end
17
+ def x
18
+ "PARENT CHILD METHOD X"
19
+ end
20
+ attr_accessor :blah
21
+ class BasicChild
22
+ def self.x
23
+ "CHILD CLASS METHOD X"
24
+ end
25
+ def x
26
+ "CHILD OBJECT METHOD X"
27
+ end
28
+ class BasicSubChild
29
+ def self.x
30
+ "GRANDCHILD CLASS METHOD X"
31
+ end
32
+ end
33
+ end
34
+ end
35
+ }
36
+
37
+ # Here's the magic..
38
+ class_server = ClassRoom::Client.new(ARGV.first || 'druby://coop-pb:2001')
39
+ class_server.add_class(klass)
40
+ class_server.load_class(binding, :all)
41
+ #class_server.load_non_present_classes(binding)
42
+
43
+ puts BasicParent.give_child.x
44
+ puts BasicParent.x
45
+ puts BasicParent::BasicChild::BasicSubChild.x
46
+
47
+ x = BasicParent.new
48
+ x.blah = 50
49
+ puts x.blah
@@ -0,0 +1,51 @@
1
+ # Attempt to do nasty / insecure operations!
2
+
3
+ require 'rubygems'
4
+ require 'classroom'
5
+
6
+ # A basic 'queue' class with a fixed length queue and
7
+ # push and pull methods to use the queue.
8
+ klass = %q{
9
+ class BasicParent
10
+ def do_nasty
11
+ `ls -l`
12
+ end
13
+ def do_nasty2
14
+ x = 'ls'
15
+ y = '-l'
16
+ z = 'system'
17
+ eval z + '"' + x + ' ' + y + '"'
18
+ end
19
+ def do_nasty3
20
+ ClassRoom::ClassServerContainer.attr_writer(:xyzzy)
21
+ end
22
+ end
23
+ }
24
+
25
+ # Here's the magic..
26
+ class_server = ClassRoom::Client.new(ARGV.first || 'druby://coop-pb:2001')
27
+ class_server.add_class(klass)
28
+ class_server.load_class(binding, :all)
29
+
30
+ a = BasicParent.new
31
+
32
+ puts "Nasty 1"
33
+ begin
34
+ a.do_nasty
35
+ rescue
36
+ puts "FAILED WITH #{$!}}!"
37
+ end
38
+
39
+ puts "Nasty 2"
40
+ begin
41
+ a.do_nasty2
42
+ rescue
43
+ puts "FAILED WITH #{$!}}!"
44
+ end
45
+
46
+ puts "Nasty 3"
47
+ begin
48
+ a.do_nasty3
49
+ rescue
50
+ puts "FAILED WITH #{$!}}!"
51
+ end
@@ -0,0 +1,9 @@
1
+ # A very basic ClassRoom server
2
+
3
+ require 'rubygems'
4
+ require 'classroom'
5
+
6
+ url = ClassRoom::ClassServer.prepare
7
+ puts "ClassRoom server running at #{url}"
8
+ ClassRoom::ClassServer.start
9
+
@@ -0,0 +1,48 @@
1
+ # Tests that modules work in one sense
2
+
3
+ require 'rubygems'
4
+ require 'classroom'
5
+
6
+ klass = %q{
7
+ module TestModule
8
+ class BasicParent
9
+ def self.give_child
10
+ BasicParent::BasicChild.new
11
+ end
12
+ def self.x
13
+ "PARENT CLASS METHOD X"
14
+ end
15
+ def x
16
+ "PARENT CHILD METHOD X"
17
+ end
18
+ attr_accessor :blah
19
+ class BasicChild
20
+ def self.x
21
+ "CHILD CLASS METHOD X"
22
+ end
23
+ def x
24
+ "CHILD OBJECT METHOD X"
25
+ end
26
+ class BasicSubChild
27
+ def self.x
28
+ "GRANDCHILD CLASS METHOD X"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ }
35
+
36
+ # Here's the magic..
37
+ class_server = ClassRoom::Client.new(ARGV.first || 'druby://coop-pb:2001')
38
+ class_server.add_class(klass)
39
+ class_server.load_non_present_classes(binding)
40
+
41
+ puts class_server.classes.inspect
42
+ puts TestModule::BasicParent.give_child.x
43
+ puts TestModule::BasicParent.x
44
+ puts TestModule::BasicParent::BasicChild::BasicSubChild.x
45
+
46
+ x = TestModule::BasicParent.new
47
+ x.blah = 50
48
+ puts x.blah
@@ -0,0 +1,32 @@
1
+ # Tests that module mixins work entirely on the server end
2
+
3
+ require 'rubygems'
4
+ require 'classroom'
5
+
6
+ # A basic 'queue' class with a fixed length queue and
7
+ # push and pull methods to use the queue.
8
+ klass = %q{
9
+ module TestModule
10
+ def blah
11
+ "blah #{@s}"
12
+ end
13
+ end
14
+ }
15
+
16
+ klass2 = %q{
17
+ class Something
18
+ include TestModule
19
+ def initialize(s)
20
+ @s = s
21
+ end
22
+ end
23
+ }
24
+
25
+ # Here's the magic..
26
+ class_server = ClassRoom::Client.new(ARGV.first || 'druby://coop-pb:2001')
27
+ class_server.add_class(klass)
28
+ class_server.add_class(klass2)
29
+ class_server.load_class(binding, :all)
30
+
31
+ x = Something.new(10)
32
+ puts x.blah
@@ -0,0 +1,35 @@
1
+ # THIS TEST WILL NOT WORK!! UNIMPLEMENTED FEATURE!!
2
+
3
+ # Use a remote module from the ClassRoom server as a mixin
4
+ # on a local class.. (currently not possible)
5
+
6
+ require 'rubygems'
7
+ require 'classroom'
8
+
9
+ # A basic 'queue' class with a fixed length queue and
10
+ # push and pull methods to use the queue.
11
+ klass = %q{
12
+ module TestModule
13
+ def blah
14
+ "blah #{@s}"
15
+ end
16
+ end
17
+ }
18
+
19
+
20
+ # Here's the magic..
21
+ class_server = ClassRoom::Client.new(ARGV.first || 'druby://coop-pb:2001')
22
+ class_server.add_class(klass)
23
+ class_server.load_class(binding, :all)
24
+
25
+
26
+ class Something
27
+ include TestModule
28
+ def initialize(s)
29
+ @s = s
30
+ end
31
+ end
32
+
33
+
34
+ x = Something.new(10)
35
+ puts x.blah
data/lib/classroom.rb ADDED
@@ -0,0 +1,241 @@
1
+ require 'drb/drb'
2
+ require 'thread'
3
+
4
+ module ClassRoom
5
+
6
+ # Create a 'blank slate' class template so we don't have any extraneous
7
+ # methods lying around to be (potentially) abused.
8
+ # Technique by http://onestepback.org/index.cgi/Tech/Ruby/BlankSlate.rdoc
9
+ class BlankSlate
10
+ instance_methods.each { |m| undef_method m unless m =~ /^__/ }
11
+ end
12
+
13
+ # Provide a separate module as the container for our user classes
14
+ module ClassContainer
15
+ end
16
+
17
+ # The class that proxies between user classes on the ClassRoom server and
18
+ # the requests made by client apps. It stores references to the user classes,
19
+ # evals the code, and allows instances to be created of the user classes.
20
+ class ClassServerContainer < BlankSlate
21
+ @@classes = []
22
+
23
+ # Add class(es) to the ClassRoom server
24
+ def self.add_class(code)
25
+ base_class = code.scan(/(class|module)\s+([\w\:]+)/).collect{|c| c[1]}.flatten.first
26
+ base_class.untaint
27
+ @@classes << base_class
28
+
29
+ # Actually add the class to the ClassRoom server
30
+ begin
31
+ # Force the code to run by untainting it
32
+ code.untaint
33
+ # Force the code into the ClassContainer module and force a
34
+ # SAFE mode where all data is considered tainted. This stops
35
+ # users running system commands, etc.
36
+ eval("module ClassRoom; module ClassContainer\n$SAFE = 3\n" + code + "\nend; end")
37
+ rescue
38
+ # If the user's code was bad, duck out.
39
+ return nil
40
+ end
41
+
42
+ # Now the code is loaded.. loop through all of the classes and add their
43
+ # full (nested) names to the class list so automatic loading
44
+ # can work for clients
45
+ classes_to_check = [base_class]
46
+ classes_to_check.each do |class_name|
47
+ class_internal = eval("ClassRoom::ClassContainer::#{class_name}")
48
+ class_internal.constants.each do |subclass_name|
49
+ if class_internal.const_get(subclass_name).is_a?(Class) || class_internal.const_get(subclass_name).is_a?(Module)
50
+ sub_class = class_name + "::" + subclass_name
51
+ @@classes << sub_class
52
+ classes_to_check << sub_class
53
+ end
54
+ end
55
+ end
56
+
57
+ # Hack to make sure the classes array is clean
58
+ @@classes.collect! { |c| c.to_sym }
59
+ @@classes.uniq!
60
+ true
61
+ end
62
+
63
+ # THESE ARE NOT WORKING
64
+ #def self.remove_class(class_name)
65
+ # class_internal = eval("ClassRoom::ClassContainer")
66
+ # class_internal.constants.each do |subclass_name|
67
+ # if class_internal.const_get(subclass_name).is_a?(Class) || class_internal.const_get(subclass_name).is_a?(Module)
68
+ # class_internal.remove_const(subclass_name) if subclass_name.to_s == class_name.to_s
69
+ # end
70
+ # end
71
+ #end
72
+ #
73
+ #def self.remove_all
74
+ # class_internal = eval("ClassRoom::ClassContainer")
75
+ # class_internal.constants.each do |subclass_name|
76
+ # if class_internal.const_get(subclass_name).is_a?(Class) || class_internal.const_get(subclass_name).is_a?(Module)
77
+ # class_internal.remove_const(subclass_name)
78
+ # end
79
+ # end
80
+ #end
81
+
82
+ # Return a list of all classes in the ClassContainer
83
+ # (must be a nicer way to do this without logging it ourselves..?)
84
+ def self.classes
85
+ return @@classes
86
+ end
87
+
88
+ # Return a new instance of a class in the ClassContainer
89
+ def self.new_instance_of(class_name, *args)
90
+ begin
91
+ obj = eval('ClassRoom::ClassContainer::' + class_name.to_s).new(*args)
92
+ obj.extend(DRbUndumped)
93
+ obj
94
+ rescue
95
+ nil
96
+ end
97
+ end
98
+
99
+ # Call a class method on a class in the ClassContainer
100
+ # (this might need work to check what comes back and whether to
101
+ # mix in DRbUndumped if it's an object)
102
+ def self.class_method(class_name, method_name, *args)
103
+ begin
104
+ obj = eval('ClassRoom::ClassContainer::' + class_name.to_s).send(method_name.to_s, *args)
105
+ obj.extend(DRbUndumped) if self.classes.detect { |c| obj.class.to_s.index(c.to_s) }
106
+ obj
107
+ rescue
108
+ nil
109
+ end
110
+ end
111
+
112
+ end
113
+
114
+ # Provides methods to get the server up and running easily
115
+ class ClassServer < BlankSlate
116
+ # Prepare DRb to act as a server using the correct URL
117
+ # Do some messy hacks to get our own URL scheme
118
+ def self.prepare(drb_url = 'classroom://:2001')
119
+ $SAFE = 1
120
+ drb_url.gsub!('classroom:', 'druby:')
121
+ DRb.start_service(drb_url, ClassServerContainer)
122
+ return DRb.uri.gsub('druby:', 'classroom:')
123
+ end
124
+
125
+ # Do the thread.join with DRb to actually get the server processing requests
126
+ # TODO: Get this daemonizing!
127
+ def self.start
128
+ #fork do
129
+ # Process.setsid
130
+ # exit if fork
131
+ # trap("TERM") {daemon.stop; exit}
132
+ Kernel.send :remove_method, :system
133
+ class << Kernel; self; end.send :remove_method, :system
134
+ $SAFE = 3
135
+ DRb.thread.join
136
+ #end
137
+ end
138
+ end
139
+
140
+ # Provides methods for the client side
141
+ class Client < BlankSlate
142
+
143
+ def self.get_drb(drb_url)
144
+ @@drb[drb_url]
145
+ end
146
+
147
+ # Set up the DRbObject referring to the ClassRoom server
148
+ def initialize(drb_url)
149
+ @drb_url = drb_url.gsub('classroom:', 'druby:')
150
+ (@@drb ||= {})[@drb_url] = DRbObject.new(nil, @drb_url)
151
+ end
152
+
153
+ # Passes through calls for non-local methods to the DRb server
154
+ def method_missing(method, *args)
155
+ @@drb[@drb_url].send(method, *args)
156
+ end
157
+
158
+ # Returns a proxy object referring to the remote class required
159
+ def remote_class(class_name)
160
+ ClassRoom::Client::ProxyObject.new(@drb, class_name)
161
+ end
162
+
163
+ # Load a class (or all classes, if :all is specified) from the
164
+ # ClassRoom server to be used locally
165
+ def load_class(b, *classes)
166
+ return nil unless classes && b
167
+ classes = self.classes if classes.first == :all
168
+ classes.each do |class_name|
169
+ eval %Q{
170
+ class #{class_name} < ClassRoom::Client::ProxyObject
171
+ end
172
+ #{class_name}.class_name = :\"#{class_name}\"
173
+ #{class_name}.drb = ClassRoom::Client::get_drb('#{@drb_url}')
174
+ }, b
175
+ end
176
+ end
177
+
178
+ # Load all classes on the ClassRoom server that do not already
179
+ # exist locally (not fully tested yet!)
180
+ def load_non_present_classes(b)
181
+ return nil unless b
182
+ classes = self.classes
183
+ classes.each do |class_name|
184
+ eval %Q{
185
+ unless defined?(#{class_name})
186
+ class #{class_name} < ClassRoom::Client::ProxyObject
187
+ end
188
+ end
189
+ }, b
190
+ eval %Q{
191
+ begin
192
+ #{class_name}.drb = @drb
193
+ rescue
194
+ end
195
+ }
196
+ eval %Q{
197
+ begin
198
+ #{class_name}.class_name = :\"#{class_name}\"
199
+ rescue
200
+ end
201
+ }
202
+ end
203
+ end
204
+
205
+ # ProxyObject acts as a local proxy for ClassRoom clients and
206
+ # handles passing through requests to run class methods and initializations
207
+ class ProxyObject
208
+
209
+ # Set up a weird /true/ class variable system (that is, class variable ==
210
+ # class variable, not weird hierarchy variable-foo as is Ruby's wont)
211
+ def self.drb; @@drb[self.to_s.to_sym]; end
212
+ def self.drb=(d); (@@drb ||= {})[self.to_s.to_sym] = d; end
213
+ def self.class_name; @@class_name[self.to_s.to_sym]; end
214
+ def self.class_name=(d); (@@class_name ||= {})[self.to_s.to_sym] = d; end
215
+ def drb; @@drb[self.class.to_s.to_sym]; end
216
+ def class_name; @@class_name[self.class.to_s.to_sym]; end
217
+
218
+ # Create an instance of the remote class
219
+ def initialize(*args)
220
+ @drbed_object = drb.send(:new_instance_of, self.class.to_s.to_sym, *args)
221
+ end
222
+
223
+ # If we're getting methods called on the proxy, they must be for
224
+ # the remote class rather than a method, so ask the ClassRoom server
225
+ # to run the class_method on the supplied object
226
+ def self.method_missing(method, *args)
227
+ drb.send(:class_method, class_name, method, *args)
228
+ end
229
+
230
+ # Return the 'real' class name
231
+ #def class; @class_name; end
232
+
233
+ def method_missing(method, *args)
234
+ @drbed_object.send(method, *args)
235
+ end
236
+
237
+ end
238
+
239
+ end
240
+
241
+ end
@@ -0,0 +1,46 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ require 'classroom'
4
+
5
+ # Get a binding for the top level
6
+ $top_level_binding = binding
7
+
8
+ class ClassRoomTest < Test::Unit::TestCase
9
+
10
+ def setup
11
+ $class_server = ClassRoom::Client.new('classroom://:2001')
12
+
13
+ end
14
+
15
+ def test_a_classroom_server_responds
16
+ assert $class_server.classes.class == Array
17
+ end
18
+
19
+ def test_b_can_add_basic_class
20
+ klass = %q{
21
+ class SmallClass
22
+ def self.result; "GOOD CLASS"; end
23
+ def result; "GOOD INSTANCE"; end
24
+ end
25
+ }
26
+ assert $class_server.add_class(klass)
27
+ end
28
+
29
+ def test_c_can_use_class_method_directly
30
+ assert_equal "GOOD CLASS", $class_server.class_method(:SmallClass, :result)
31
+ end
32
+
33
+ def test_d_can_create_instance_directly
34
+ assert_equal "GOOD INSTANCE", $class_server.new_instance_of(:SmallClass).result
35
+ end
36
+
37
+ def test_e_can_import_classes
38
+ assert $class_server.load_class($top_level_binding, :all)
39
+ end
40
+
41
+ def test_f_can_use_imported_classes
42
+ assert_equal "GOOD CLASS", SmallClass.result
43
+ assert_equal "GOOD INSTANCE", SmallClass.new.result
44
+ end
45
+
46
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: classroom
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.0.1
7
+ date: 2006-07-12 00:00:00 +01:00
8
+ summary: ClassRoom is a 'class server' based on DRb
9
+ require_paths:
10
+ - lib
11
+ email: coops@petercooper.co.uk
12
+ homepage: http://classroom.rubyforge.org
13
+ rubyforge_project: classroom
14
+ description:
15
+ autorequire: classroom
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: false
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ authors:
29
+ - Peter Cooper
30
+ files:
31
+ - classroom.gemspec
32
+ - examples
33
+ - lib
34
+ - Rakefile
35
+ - test
36
+ - examples/demo_client.rb
37
+ - examples/demo_client2.rb
38
+ - examples/demo_client3.rb
39
+ - examples/demo_client4.rb
40
+ - examples/demo_client5.rb
41
+ - examples/server.rb
42
+ - examples/test_modules.rb
43
+ - examples/test_modules2.rb
44
+ - examples/test_modules3.rb
45
+ - lib/classroom.rb
46
+ - test/classroom_test.rb
47
+ test_files:
48
+ - test/classroom_test.rb
49
+ rdoc_options: []
50
+
51
+ extra_rdoc_files: []
52
+
53
+ executables: []
54
+
55
+ extensions: []
56
+
57
+ requirements: []
58
+
59
+ dependencies: []
60
+