grumlin 0.15.3 → 0.16.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +10 -0
  3. data/CHANGELOG.md +10 -0
  4. data/Gemfile.lock +2 -2
  5. data/README.md +4 -0
  6. data/Rakefile +21 -3
  7. data/lib/definitions.yml +114 -0
  8. data/lib/grumlin/action.rb +124 -0
  9. data/lib/grumlin/client.rb +2 -2
  10. data/lib/grumlin/expressions/operator.rb +1 -1
  11. data/lib/grumlin/expressions/order.rb +1 -1
  12. data/lib/grumlin/expressions/p.rb +12 -17
  13. data/lib/grumlin/expressions/pop.rb +1 -1
  14. data/lib/grumlin/expressions/scope.rb +1 -1
  15. data/lib/grumlin/expressions/t.rb +1 -1
  16. data/lib/grumlin/expressions/text_p.rb +15 -0
  17. data/lib/grumlin/expressions/with_options.rb +17 -14
  18. data/lib/grumlin/repository.rb +2 -2
  19. data/lib/grumlin/shortcut.rb +27 -0
  20. data/lib/grumlin/shortcuts/properties.rb +6 -2
  21. data/lib/grumlin/shortcuts.rb +15 -13
  22. data/lib/grumlin/shortcuts_applyer.rb +48 -0
  23. data/lib/grumlin/step_data.rb +18 -0
  24. data/lib/grumlin/steps.rb +77 -0
  25. data/lib/grumlin/steps_serializers/bytecode.rb +65 -0
  26. data/lib/grumlin/steps_serializers/human_readable_bytecode.rb +36 -0
  27. data/lib/grumlin/steps_serializers/serializer.rb +16 -0
  28. data/lib/grumlin/steps_serializers/string.rb +42 -0
  29. data/lib/grumlin/sugar.rb +4 -4
  30. data/lib/grumlin/traversal_start.rb +51 -0
  31. data/lib/grumlin/typed_value.rb +0 -15
  32. data/lib/grumlin/version.rb +1 -1
  33. data/lib/grumlin.rb +6 -4
  34. metadata +14 -8
  35. data/lib/grumlin/anonymous_step.rb +0 -49
  36. data/lib/grumlin/bytecode.rb +0 -70
  37. data/lib/grumlin/expressions/u.rb +0 -19
  38. data/lib/grumlin/shortcut_proxy.rb +0 -53
  39. data/lib/grumlin/step.rb +0 -43
  40. data/lib/grumlin/traversal.rb +0 -37
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class ShortcutsApplyer
5
+ class << self
6
+ def call(steps)
7
+ new.call(steps)
8
+ end
9
+ end
10
+
11
+ def call(steps)
12
+ return steps unless steps.uses_shortcuts?
13
+
14
+ shortcuts = steps.shortcuts
15
+
16
+ configuration_steps = process_steps(steps.configuration_steps, shortcuts)
17
+ regular_steps = process_steps(steps.steps, shortcuts)
18
+
19
+ Steps.new(shortcuts).tap do |processed_steps|
20
+ (configuration_steps + regular_steps).each do |step|
21
+ processed_steps.add(step.name, step.arguments)
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def process_steps(steps, shortcuts) # rubocop:disable Metrics/AbcSize
29
+ steps.each_with_object([]) do |step, result|
30
+ arguments = step.arguments.map do |arg|
31
+ arg.is_a?(Steps) ? ShortcutsApplyer.call(arg) : arg
32
+ end
33
+
34
+ if shortcuts.include?(step.name)
35
+ t = TraversalStart.new(shortcuts)
36
+ action = shortcuts[step.name].apply(t, *arguments)
37
+ next if action.nil? || action == t # Shortcut did not add any steps
38
+
39
+ new_steps = ShortcutsApplyer.call(Steps.from(action))
40
+ result.concat(new_steps.configuration_steps)
41
+ result.concat(new_steps.steps)
42
+ else
43
+ result << StepData.new(step.name, arguments)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class StepData
5
+ attr_reader :name, :arguments
6
+
7
+ def initialize(name, arguments)
8
+ @name = name
9
+ @arguments = arguments
10
+ end
11
+
12
+ def ==(other)
13
+ self.class == other.class &&
14
+ @name == other.name &&
15
+ @arguments == other.arguments
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class Steps
5
+ CONFIGURATION_STEPS = Action::CONFIGURATION_STEPS
6
+ ALL_STEPS = Action::ALL_STEPS
7
+
8
+ def self.from(action)
9
+ raise ArgumentError, "expected: #{Action}, given: #{action.class}" unless action.is_a?(Action)
10
+
11
+ shortcuts = action.shortcuts
12
+ actions = []
13
+
14
+ until action.nil?
15
+ actions.unshift(action)
16
+ action = action.previous_step
17
+ end
18
+
19
+ new(shortcuts).tap do |chain|
20
+ actions.each do |act|
21
+ chain.add(act.name, act.arguments)
22
+ end
23
+ end
24
+ end
25
+
26
+ attr_reader :configuration_steps, :steps, :shortcuts
27
+
28
+ def initialize(shortcuts, configuration_steps: [], steps: [])
29
+ @shortcuts = shortcuts
30
+ @configuration_steps = configuration_steps
31
+ @steps = steps
32
+ end
33
+
34
+ def add(name, arguments)
35
+ return add_configuration_step(name, arguments) if CONFIGURATION_STEPS.include?(name)
36
+
37
+ StepData.new(name, cast_arguments(arguments)).tap do |step|
38
+ @steps << step
39
+ end
40
+ end
41
+
42
+ def uses_shortcuts?
43
+ shortcuts?(@configuration_steps) || shortcuts?(@steps)
44
+ end
45
+
46
+ def ==(other)
47
+ self.class == other.class &&
48
+ @shortcuts == other.shortcuts &&
49
+ @configuration_steps == other.configuration_steps &&
50
+ @steps == other.steps
51
+ end
52
+
53
+ # TODO: add #bytecode, to_s, inspect
54
+
55
+ private
56
+
57
+ def shortcuts?(steps_ary)
58
+ steps_ary.any? do |step|
59
+ @shortcuts.include?(step.name) || step.arguments.any? do |arg|
60
+ arg.is_a?(Steps) ? arg.uses_shortcuts? : false
61
+ end
62
+ end
63
+ end
64
+
65
+ def add_configuration_step(name, arguments)
66
+ raise ArgumentError, "cannot use configuration steps after start step was used" unless @steps.empty?
67
+
68
+ StepData.new(name, cast_arguments(arguments)).tap do |step|
69
+ @configuration_steps << step
70
+ end
71
+ end
72
+
73
+ def cast_arguments(arguments)
74
+ arguments.map { |arg| arg.is_a?(Action) ? Steps.from(arg) : arg }
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module StepsSerializers
5
+ class Bytecode < Serializer
6
+ # constructor params: no_return: true|false, default false
7
+ # TODO: add pretty
8
+
9
+ NONE_STEP = StepData.new("none", [])
10
+
11
+ def serialize
12
+ steps = ShortcutsApplyer.call(@steps)
13
+ no_return = @params[:no_return] || false
14
+
15
+ {
16
+ step: (steps.steps + (no_return ? [NONE_STEP] : [])).map { |s| serialize_step(s) }
17
+ }.tap do |v|
18
+ v.merge!(source: steps.configuration_steps.map { |s| serialize_step(s) }) if steps.configuration_steps.any?
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def serialize_step(step)
25
+ [step.name, *step.arguments.map { |arg| serialize_arg(arg) }]
26
+ end
27
+
28
+ def serialize_arg(arg)
29
+ return serialize_typed_value(arg) if arg.is_a?(TypedValue)
30
+ return serialize_predicate(arg) if arg.is_a?(Expressions::P::Predicate)
31
+ return arg.value if arg.is_a?(Expressions::WithOptions)
32
+
33
+ return arg unless arg.is_a?(Steps)
34
+
35
+ { :@type => "g:Bytecode", :@value => Bytecode.new(arg, **@params.merge(no_return: false)).serialize }
36
+ end
37
+
38
+ def serialize_typed_value(value)
39
+ return value.value if value.type.nil?
40
+
41
+ {
42
+ "@type": "g:#{value.type}",
43
+ "@value": value.value
44
+ }
45
+ end
46
+
47
+ def serialize_predicate(value)
48
+ {
49
+ "@type": "g:#{value.namespace}",
50
+ "@value": {
51
+ predicate: value.name,
52
+ value: if value.type.nil?
53
+ value.value
54
+ else
55
+ {
56
+ "@type": "g:#{value.type}",
57
+ "@value": value.value
58
+ }
59
+ end
60
+ }
61
+ }
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module StepsSerializers
5
+ class HumanReadableBytecode < Serializer
6
+ def serialize
7
+ steps = ShortcutsApplyer.call(@steps)
8
+ [serialize_steps(steps.configuration_steps), serialize_steps(steps.steps)]
9
+ end
10
+
11
+ def serialize_steps(steps)
12
+ steps.map { |s| serialize_step(s) }
13
+ end
14
+
15
+ private
16
+
17
+ def serialize_step(step)
18
+ [step.name, *step.arguments.map { |arg| serialize_arg(arg) }]
19
+ end
20
+
21
+ def serialize_arg(arg)
22
+ return arg.to_s if arg.is_a?(TypedValue)
23
+ return serialize_predicate(arg) if arg.is_a?(Expressions::P::Predicate)
24
+ return arg.value if arg.is_a?(Expressions::WithOptions)
25
+
26
+ return arg unless arg.is_a?(Steps)
27
+
28
+ HumanReadableBytecode.new(arg, **@params.merge(no_return: false)).serialize[1]
29
+ end
30
+
31
+ def serialize_predicate(arg)
32
+ "#{arg.name}(#{arg.value})"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module StepsSerializers
5
+ class Serializer
6
+ def initialize(steps, **params)
7
+ @steps = steps
8
+ @params = params
9
+ end
10
+
11
+ def serialize
12
+ raise NotImplementedError
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ module StepsSerializers
5
+ class String < Serializer
6
+ # constructor params: apply_shortcuts: true|false, default: false
7
+ # constructor params: anonymous: true|false, default: false
8
+ # TODO: add pretty
9
+
10
+ def serialize
11
+ steps = @params[:apply_shortcuts] ? ShortcutsApplyer.call(@steps) : @steps
12
+
13
+ configuration_steps = serialize_steps(steps.configuration_steps)
14
+ regular_steps = serialize_steps(steps.steps)
15
+
16
+ "#{prefix}.#{(configuration_steps + regular_steps).join(".")}"
17
+ end
18
+
19
+ private
20
+
21
+ def prefix
22
+ @prefix ||= @params[:anonymous] ? "__" : "g"
23
+ end
24
+
25
+ def serialize_arg(arg)
26
+ return "\"#{arg}\"" if arg.is_a?(::String) || arg.is_a?(Symbol)
27
+ return "#{arg.type}.#{arg.value}" if arg.is_a?(Grumlin::TypedValue)
28
+ return arg.to_s if arg.is_a?(Grumlin::Expressions::WithOptions)
29
+
30
+ return arg unless arg.is_a?(Steps)
31
+
32
+ StepsSerializers::String.new(arg, anonymous: true, **@params).serialize
33
+ end
34
+
35
+ def serialize_steps(steps)
36
+ steps.map do |step|
37
+ "#{step.name}(#{step.arguments.map { |a| serialize_arg(a) }.join(", ")})"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
data/lib/grumlin/sugar.rb CHANGED
@@ -6,12 +6,12 @@ module Grumlin
6
6
  base.include(Grumlin::Expressions)
7
7
  end
8
8
 
9
- def __
10
- Grumlin::Expressions::U
9
+ def __(shortcuts = {})
10
+ Grumlin::TraversalStart.new(shortcuts) # TODO: allow only regular and start steps
11
11
  end
12
12
 
13
- def g
14
- Grumlin::Traversal.new
13
+ def g(shortcuts = {})
14
+ Grumlin::TraversalStart.new(shortcuts)
15
15
  end
16
16
  end
17
17
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grumlin
4
+ class TraversalStart
5
+ START_STEPS = Grumlin.definitions.dig(:steps, :start).map(&:to_sym).freeze
6
+ REGULAR_STEPS = Grumlin.definitions.dig(:steps, :regular).map(&:to_sym).freeze
7
+ CONFIGURATION_STEPS = Grumlin.definitions.dig(:steps, :configuration).map(&:to_sym).freeze
8
+
9
+ ALL_STEPS = START_STEPS + CONFIGURATION_STEPS + REGULAR_STEPS
10
+
11
+ ALL_STEPS.each do |step|
12
+ define_method step do |*args, **params|
13
+ step(step, *args, **params)
14
+ end
15
+ end
16
+
17
+ attr_reader :shortcuts
18
+
19
+ def initialize(shortcuts)
20
+ @shortcuts = shortcuts
21
+ end
22
+
23
+ def step(name, *args, **params)
24
+ Action.new(name, args: args, params: params, shortcuts: @shortcuts)
25
+ end
26
+
27
+ def method_missing(name, *args, **params)
28
+ return step(name, *args, **params) if @shortcuts.key?(name)
29
+
30
+ super
31
+ end
32
+
33
+ def __
34
+ TraversalStart.new(@shortcuts) # TODO: allow only regular and start steps
35
+ end
36
+
37
+ def to_s(*)
38
+ self.class.to_s
39
+ end
40
+
41
+ def inspect
42
+ self.class.inspect
43
+ end
44
+
45
+ private
46
+
47
+ def respond_to_missing?(name, _include_private = false)
48
+ @shortcuts.key?(name)
49
+ end
50
+ end
51
+ end
@@ -9,17 +9,6 @@ module Grumlin
9
9
  @value = value
10
10
  end
11
11
 
12
- def to_bytecode
13
- @to_bytecode ||= if type.nil?
14
- value
15
- else
16
- {
17
- "@type": "g:#{type}",
18
- "@value": value
19
- }
20
- end
21
- end
22
-
23
12
  def inspect
24
13
  "<#{type}.#{value}>"
25
14
  end
@@ -27,9 +16,5 @@ module Grumlin
27
16
  def to_s
28
17
  inspect
29
18
  end
30
-
31
- def to_readable_bytecode
32
- inspect
33
- end
34
19
  end
35
20
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grumlin
4
- VERSION = "0.15.3"
4
+ VERSION = "0.16.0"
5
5
  end
data/lib/grumlin.rb CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  require "securerandom"
4
4
  require "oj"
5
+ require "yaml"
5
6
 
7
+ # TODO: use Oj directly
6
8
  Oj.mimic_JSON
7
9
  Oj.add_to_json
8
10
 
@@ -108,10 +110,6 @@ module Grumlin
108
110
  end
109
111
  end
110
112
 
111
- def self.supported_steps
112
- @supported_steps ||= (Grumlin::AnonymousStep::SUPPORTED_STEPS + Grumlin::Expressions::U::SUPPORTED_STEPS).sort.uniq
113
- end
114
-
115
113
  @pool_mutex = Mutex.new
116
114
 
117
115
  class << self
@@ -145,6 +143,10 @@ module Grumlin
145
143
  Thread.current.thread_variable_set(:grumlin_default_pool, nil)
146
144
  end
147
145
  end
146
+
147
+ def definitions
148
+ @definitions ||= YAML.safe_load(File.read(File.join(__dir__, "definitions.yml")), symbolize_names: true)
149
+ end
148
150
  end
149
151
  end
150
152
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grumlin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.3
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gleb Sinyavskiy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-18 00:00:00.000000000 Z
11
+ date: 2022-03-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-pool
@@ -96,9 +96,9 @@ files:
96
96
  - gremlin_server/tinkergraph-empty.properties
97
97
  - grumlin.gemspec
98
98
  - lib/async/channel.rb
99
+ - lib/definitions.yml
99
100
  - lib/grumlin.rb
100
- - lib/grumlin/anonymous_step.rb
101
- - lib/grumlin/bytecode.rb
101
+ - lib/grumlin/action.rb
102
102
  - lib/grumlin/client.rb
103
103
  - lib/grumlin/edge.rb
104
104
  - lib/grumlin/expressions/expression.rb
@@ -108,22 +108,28 @@ files:
108
108
  - lib/grumlin/expressions/pop.rb
109
109
  - lib/grumlin/expressions/scope.rb
110
110
  - lib/grumlin/expressions/t.rb
111
- - lib/grumlin/expressions/u.rb
111
+ - lib/grumlin/expressions/text_p.rb
112
112
  - lib/grumlin/expressions/with_options.rb
113
113
  - lib/grumlin/path.rb
114
114
  - lib/grumlin/property.rb
115
115
  - lib/grumlin/repository.rb
116
116
  - lib/grumlin/request_dispatcher.rb
117
- - lib/grumlin/shortcut_proxy.rb
117
+ - lib/grumlin/shortcut.rb
118
118
  - lib/grumlin/shortcuts.rb
119
119
  - lib/grumlin/shortcuts/properties.rb
120
- - lib/grumlin/step.rb
120
+ - lib/grumlin/shortcuts_applyer.rb
121
+ - lib/grumlin/step_data.rb
122
+ - lib/grumlin/steps.rb
123
+ - lib/grumlin/steps_serializers/bytecode.rb
124
+ - lib/grumlin/steps_serializers/human_readable_bytecode.rb
125
+ - lib/grumlin/steps_serializers/serializer.rb
126
+ - lib/grumlin/steps_serializers/string.rb
121
127
  - lib/grumlin/sugar.rb
122
128
  - lib/grumlin/test/rspec.rb
123
129
  - lib/grumlin/test/rspec/db_cleaner_context.rb
124
130
  - lib/grumlin/test/rspec/gremlin_context.rb
125
131
  - lib/grumlin/transport.rb
126
- - lib/grumlin/traversal.rb
132
+ - lib/grumlin/traversal_start.rb
127
133
  - lib/grumlin/traverser.rb
128
134
  - lib/grumlin/typed_value.rb
129
135
  - lib/grumlin/typing.rb
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Grumlin
4
- class AnonymousStep
5
- attr_reader :name, :previous_step, :configuration_steps
6
-
7
- # TODO: add other steps
8
- SUPPORTED_STEPS = %i[E V addE addV aggregate and as both bothE by choose coalesce count dedup drop elementMap emit
9
- fold from group groupCount has hasId hasLabel hasNot id identity in inE inV is label limit
10
- map not or order out outE path project properties property range repeat sack select sideEffect
11
- skip sum tail to unfold union until valueMap values where with].freeze
12
-
13
- def initialize(name, *args, configuration_steps: [], previous_step: nil, **params)
14
- @name = name
15
- @previous_step = previous_step
16
- @args = args
17
- @params = params
18
- @configuration_steps = configuration_steps
19
- end
20
-
21
- SUPPORTED_STEPS.each do |step|
22
- define_method(step) do |*args, **params|
23
- step(step, *args, **params)
24
- end
25
- end
26
-
27
- def step(name, *args, **params)
28
- self.class.new(name, *args, previous_step: self, configuration_steps: configuration_steps, **params)
29
- end
30
-
31
- def inspect
32
- bytecode.inspect
33
- end
34
-
35
- def to_s
36
- inspect
37
- end
38
-
39
- def bytecode(no_return: false)
40
- @bytecode ||= Bytecode.new(self, no_return: no_return)
41
- end
42
-
43
- def args
44
- [*@args].tap do |args|
45
- args << @params if @params.any?
46
- end
47
- end
48
- end
49
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Grumlin
4
- # Incapsulates logic of converting step chains and step arguments to queries that can be sent to the server
5
- # and to human readable strings.
6
- class Bytecode < TypedValue
7
- class NoneStep
8
- def to_bytecode
9
- ["none"]
10
- end
11
- end
12
-
13
- NONE_STEP = NoneStep.new
14
-
15
- def initialize(step, no_return: false)
16
- super(type: "Bytecode")
17
- @step = step
18
- @no_return = no_return
19
- end
20
-
21
- def inspect
22
- configuration_steps = @step.configuration_steps.map do |s|
23
- serialize_arg(s, serialization_method: :to_readable_bytecode)
24
- end
25
- "#{configuration_steps.any? ? configuration_steps : nil}#{to_readable_bytecode}"
26
- end
27
-
28
- def to_s
29
- inspect
30
- end
31
-
32
- def to_readable_bytecode
33
- @to_readable_bytecode ||= steps.map { |s| serialize_arg(s, serialization_method: :to_readable_bytecode) }
34
- end
35
-
36
- def value
37
- @value ||= { step: (steps + (@no_return ? [NONE_STEP] : [])).map { |s| serialize_arg(s) } }.tap do |v|
38
- v.merge!(source: @step.configuration_steps.map { |s| serialize_arg(s) }) if @step.configuration_steps.any?
39
- end
40
- end
41
-
42
- private
43
-
44
- # Serializes step or a step argument to either an executable query or a human readable string representation
45
- # depending on the `serialization_method` parameter. It should be either `:to_readable_bytecode` for human readable
46
- # representation or `:to_bytecode` for query.
47
- def serialize_arg(arg, serialization_method: :to_bytecode)
48
- return arg.public_send(serialization_method) if arg.respond_to?(serialization_method)
49
- return arg unless arg.is_a?(AnonymousStep)
50
-
51
- arg.args.each.with_object([arg.name.to_s]) do |a, res|
52
- res << if a.respond_to?(:bytecode)
53
- a.bytecode.public_send(serialization_method)
54
- else
55
- serialize_arg(a, serialization_method: serialization_method)
56
- end
57
- end
58
- end
59
-
60
- def steps
61
- @steps ||= [].tap do |result|
62
- step = @step
63
- until step.nil?
64
- result.unshift(step)
65
- step = step.previous_step
66
- end
67
- end
68
- end
69
- end
70
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Grumlin
4
- module Expressions
5
- module U
6
- # TODO: add other start steps
7
- SUPPORTED_STEPS = %i[V addV coalesce constant count drop fold has hasLabel hasNot id identity in inE inV is label
8
- out outE outV project repeat select timeLimit unfold valueMap values].freeze
9
-
10
- class << self
11
- SUPPORTED_STEPS.each do |step|
12
- define_method step do |*args, **params|
13
- AnonymousStep.new(step, *args, **params)
14
- end
15
- end
16
- end
17
- end
18
- end
19
- end