thronglets 0.0.1 → 0.0.2

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: cdda6c742974526a61c6a4f091b18b6c5c95825c6f21c6e243435bd34faea281
4
- data.tar.gz: 283b488716218b7d8f070a23858754267ed12371492cc5511cfc5532f738b0f6
3
+ metadata.gz: 2069a8ba6a74b72c831a2db5a2f309ed6dda12c85bc91ec2f09172c509d7fcb1
4
+ data.tar.gz: 6bbe85fde0072e98312c600acbb4be3c902b8734c329a09e1edc271c6f95e96b
5
5
  SHA512:
6
- metadata.gz: 34d1b956a592f8d14c2dd4d19ebcbf12138a3ae39a65d9499ee3161e3176d31e3fdfa22d4d82c494c3c805749b5f0794c404deb49365a7dd0755231104fe5943
7
- data.tar.gz: 1fba370c4feabaaa70ed62d0b4a79a888c2f2afe922d286c112ab27cae73e0e94cd296e022bd94d6bd451b5667ad3c0827def5a3b8e2b2adbf178153ed671ad1
6
+ metadata.gz: 0ba8543c6ad46a7bf85a476ddc4a749bdee16fee65a136f41d36af633185d6c132024cd223c548f27b8cf3fd5cf6827b503c96196f887e9837e995f053d0bc02
7
+ data.tar.gz: 344d171f7c884bd8d1ad5294072393f4cac2342e81aa4e9ea400a3aab5d14b5d282a4e4cf44e30d61e1ce0de6042dd892149f64e7bd46385a04424c891cc3b88
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Thronglets::Activity < Temporal::Activity
4
+ include Thronglets::Concerns::AbstractClass
5
+ include Thronglets::Concerns::Input
6
+ include Thronglets::Concerns::Output
7
+
8
+ attr_reader :params
9
+
10
+ def call
11
+ raise "NotImplemented"
12
+ end
13
+
14
+ def execute(args)
15
+ @params = validate_input!(args.as_json)
16
+
17
+ data = call.as_json
18
+
19
+ if output_schema
20
+ validate_output!(data)
21
+ else
22
+ data
23
+ end
24
+ rescue InputValidationError, OutputValidationError => e
25
+ {
26
+ errors: e.errors,
27
+ }.as_json
28
+ end
29
+ end
@@ -1,16 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
+ require "irb"
5
+ require_relative "worker"
6
+ require_relative "listener"
4
7
 
5
- module Thronglets
6
- class CLI < Thor
7
- extend ThorExt::Start
8
+ class Thronglets::CLI < Thor
9
+ extend Thronglets::ThorExt::Start
8
10
 
9
- map %w[-v --version] => "version"
11
+ map %w[-v --version] => "version"
12
+ map %w[-w --worker] => "worker"
13
+ map %w[-l --listen] => "listen"
14
+ map %w[-c --console] => "console"
10
15
 
11
- desc "version", "Display thronglets version", hide: true
12
- def version
13
- say "thronglets/#{VERSION} #{RUBY_DESCRIPTION}"
14
- end
16
+ desc "version", "Display thronglets version", hide: true
17
+ def version
18
+ say "thronglets/#{Thronglets::VERSION} #{RUBY_DESCRIPTION}"
19
+ end
20
+
21
+ desc "worker", "Start worker"
22
+ def worker
23
+ say "Starting worker"
24
+ require File.join(Dir.pwd, "config", "temporal/env.rb")
25
+
26
+ app = Thronglets::Worker.new
27
+ app.run
28
+ end
29
+
30
+ desc "listen", "Start worker in listen mode"
31
+ def listen
32
+ say "Starting worker in listen mode"
33
+ require File.join(Dir.pwd, "config", "temporal/env.rb")
34
+
35
+ app = Thronglets::Listener.new
36
+ app.run
37
+ end
38
+
39
+ desc "console", "Start console"
40
+ def console
41
+ say "Starting console"
42
+ require File.join(Dir.pwd, "config", "temporal/env.rb")
43
+ loader = Thronglets::Loader.new
44
+ loader.load
45
+
46
+ ARGV.clear # otherwise all script parameters get passed to IRB
47
+ IRB.start
15
48
  end
16
49
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thronglets::Concerns::AbstractClass
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ def abstract_class?
8
+ defined?(@abstract_class) && @abstract_class == true
9
+ end
10
+
11
+ def abstract_class=(value)
12
+ @abstract_class = value
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thronglets::Concerns::Input
4
+ extend ActiveSupport::Concern
5
+
6
+ attr_reader :input_errors, :input_result
7
+
8
+ delegate :input_schema, to: :class
9
+
10
+ class_methods do
11
+ def input(&)
12
+ @input = Dry::Schema.Params(&)
13
+ end
14
+
15
+ def input_schema
16
+ @input
17
+ end
18
+ end
19
+
20
+ class InputValidationError < StandardError
21
+ attr_reader :errors
22
+
23
+ def initialize(errors)
24
+ @errors = errors
25
+ super("Parameters are not valid")
26
+ end
27
+ end
28
+
29
+ def validate_input!(args)
30
+ return args unless input_schema
31
+
32
+ @input_result = input_schema.call(args)
33
+ input_result_data = input_result.to_h.as_json
34
+ @input_errors = input_result.errors.to_h
35
+ return input_result_data if input_errors.blank?
36
+
37
+ raise InputValidationError, input_errors
38
+ end
39
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Thronglets::Concerns::Output
4
+ extend ActiveSupport::Concern
5
+
6
+ attr_reader :output_errors, :output_result
7
+
8
+ delegate :output_schema, to: :class
9
+
10
+ class_methods do
11
+ def output(&)
12
+ @output = Dry::Schema.Params(&)
13
+ end
14
+
15
+ def output_schema
16
+ @output
17
+ end
18
+ end
19
+
20
+ class OutputValidationError < StandardError
21
+ attr_reader :errors
22
+
23
+ def initialize(errors)
24
+ @errors = errors
25
+ super("Results are not valid")
26
+ end
27
+ end
28
+
29
+ def validate_output!(data)
30
+ @output_result = output_schema.call(data)
31
+ output_result_data = output_result.to_h.as_json
32
+ @output_errors = output_result.errors.to_h
33
+ return output_result_data if output_errors.blank?
34
+
35
+ raise OutputValidationError, output_errors
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "listen"
4
+ require "childprocess"
5
+
6
+ class Thronglets::Listener
7
+ attr_reader :process
8
+
9
+ def initialize
10
+ @process = nil
11
+ end
12
+
13
+ def run
14
+ spawn_process
15
+
16
+ listener = Listen.to("app") do |_modified, _added, _removed|
17
+ process.stop
18
+ ensure
19
+ spawn_process
20
+ end
21
+ listener.start
22
+ sleep
23
+ rescue Interrupt
24
+ process.stop # tries increasingly harsher methods to kill the process.
25
+ end
26
+
27
+ private
28
+
29
+ def spawn_process
30
+ @process = ChildProcess.build("thronglets", "-w")
31
+ process.io.inherit!
32
+ process.start
33
+ end
34
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Thronglets::Loader
4
+ attr_reader :loader
5
+
6
+ def initialize
7
+ @loader = Zeitwerk::Loader.new
8
+ [
9
+ "app/activities",
10
+ "app/actors",
11
+ "app/workflows",
12
+ "app/models",
13
+ "app/models/concerns",
14
+ ].each do |path|
15
+ loader.push_dir(path) if Dir.exist?(path)
16
+ end
17
+ end
18
+
19
+ def load
20
+ loader.setup
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Thronglets::Registry
4
+ attr_reader :worker
5
+
6
+ def initialize(worker)
7
+ @worker = worker
8
+ end
9
+
10
+ def load!
11
+ list_classes_in_dir("app/activities").each do |activity|
12
+ if can_register_class?(activity)
13
+ puts format("Registered: %s", activity)
14
+ worker.register_activity(activity)
15
+ end
16
+ end
17
+ list_classes_in_dir("app/workflows").each do |workflow|
18
+ if can_register_class?(workflow)
19
+ puts format("Registered: %s", workflow)
20
+ worker.register_workflow(workflow)
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def list_classes_in_dir(path)
28
+ Dir.glob("#{path}/**/*.{rb}").map do |file|
29
+ name = file.delete_prefix("#{path}/").delete_suffix(".rb")
30
+ name.camelize.constantize
31
+ end
32
+ end
33
+
34
+ def can_register_class?(klass)
35
+ !klass.abstract_class?
36
+ end
37
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configures Thor to behave more like a typical CLI, with better help and error handling.
4
+ #
5
+ # - Passing -h or --help to a command will show help for that command.
6
+ # - Unrecognized options will be treated as errors (instead of being silently ignored).
7
+ # - Error messages will be printed in red to stderr, without stack trace.
8
+ # - Full stack traces can be enabled by setting the VERBOSE environment variable.
9
+ # - Errors will cause Thor to exit with a non-zero status.
10
+ #
11
+ # To take advantage of this behavior, your CLI should subclass Thor and extend this module.
12
+ #
13
+ # class CLI < Thor
14
+ # extend ThorExt::Start
15
+ # end
16
+ #
17
+ # Start your CLI with:
18
+ #
19
+ # CLI.start
20
+ #
21
+ # In tests, prevent Kernel.exit from being called when an error occurs, like this:
22
+ #
23
+ # CLI.start(args, exit_on_failure: false)
24
+ #
25
+ module Thronglets::ThorExt::Start
26
+ def self.extended(base)
27
+ super
28
+ base.check_unknown_options!
29
+ end
30
+
31
+ def start(given_args = ARGV, config = {})
32
+ config[:shell] ||= Thor::Base.shell.new
33
+ handle_help_switches(given_args) do |args|
34
+ dispatch(nil, args, nil, config)
35
+ end
36
+ rescue StandardError => e
37
+ handle_exception_on_start(e, config)
38
+ end
39
+
40
+ private
41
+
42
+ def handle_help_switches(given_args)
43
+ yield(given_args.dup)
44
+ rescue Thor::UnknownArgumentError => e
45
+ retry_with_args = []
46
+
47
+ if given_args.first == "help"
48
+ retry_with_args = [ "help" ] if given_args.length > 1
49
+ elsif e.unknown.intersect?(%w[-h --help])
50
+ retry_with_args = [ "help", (given_args - e.unknown).first ]
51
+ end
52
+ raise unless retry_with_args.any?
53
+
54
+ yield(retry_with_args)
55
+ end
56
+
57
+ def handle_exception_on_start(error, config)
58
+ return if error.is_a?(Errno::EPIPE)
59
+ raise if ENV["VERBOSE"] || !config.fetch(:exit_on_failure, true)
60
+
61
+ message = error.message.to_s
62
+ message.prepend("[#{error.class}] ") if message.empty? || !error.is_a?(Thor::Error)
63
+
64
+ config[:shell]&.say_error(message, :red)
65
+ exit(false)
66
+ end
67
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Thronglets
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.2"
5
5
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+ require "temporal"
5
+ require "temporal/worker"
6
+ require_relative "registry"
7
+
8
+ class Thronglets::Worker
9
+ attr_reader :loader
10
+
11
+ def initialize
12
+ @loader = Thronglets::Loader.new
13
+ loader.load
14
+ end
15
+
16
+ def run
17
+ registry.load!
18
+ worker.start # runs forever
19
+ end
20
+
21
+ private
22
+
23
+ def worker
24
+ @worker ||= Temporal::Worker.new(
25
+ # how many threads poll for activities
26
+ activity_thread_pool_size: 20,
27
+ # how many threads poll for workflows
28
+ workflow_thread_pool_size: 10,
29
+ )
30
+ end
31
+
32
+ def registry
33
+ @registry ||= Thronglets::Registry.new(worker)
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Thronglets::Workflow < Temporal::Workflow
4
+ include Thronglets::Concerns::AbstractClass
5
+ include Thronglets::Concerns::Input
6
+ include Thronglets::Concerns::Output
7
+
8
+ attr_reader :params
9
+
10
+ def call
11
+ raise "NotImplemented"
12
+ end
13
+
14
+ def execute(args)
15
+ @params = validate_input!(args.as_json)
16
+
17
+ data = call.as_json
18
+
19
+ if output_schema
20
+ validate_output!(data)
21
+ else
22
+ data
23
+ end
24
+ rescue InputValidationError, OutputValidationError => e
25
+ {
26
+ errors: e.errors,
27
+ }.as_json
28
+ end
29
+ end
data/lib/thronglets.rb CHANGED
@@ -1,7 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Thronglets
4
+ module ThorExt
5
+ autoload :Start, "thronglets/thor_ext/start"
6
+ end
7
+
8
+ module Concerns
9
+ autoload :AbstractClass, "thronglets/concerns/abstract_class"
10
+ autoload :Output, "thronglets/concerns/output"
11
+ autoload :Input, "thronglets/concerns/input"
12
+ end
13
+
4
14
  autoload :CLI, "thronglets/cli"
5
15
  autoload :VERSION, "thronglets/version"
6
- autoload :ThorExt, "thronglets/thor_ext"
16
+ autoload :Workflow, "thronglets/workflow"
17
+ autoload :Activity, "thronglets/activity"
18
+ autoload :Loader, "thronglets/loader"
7
19
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thronglets
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artem Mashchenko
@@ -9,6 +9,48 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: childprocess
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: listen
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
12
54
  - !ruby/object:Gem::Dependency
13
55
  name: thor
14
56
  requirement: !ruby/object:Gem::Requirement
@@ -23,6 +65,20 @@ dependencies:
23
65
  - - "~>"
24
66
  - !ruby/object:Gem::Version
25
67
  version: '1.2'
68
+ - !ruby/object:Gem::Dependency
69
+ name: zeitwerk
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
26
82
  email:
27
83
  - artem.maschenko@gmail.com
28
84
  executables:
@@ -34,9 +90,18 @@ files:
34
90
  - README.md
35
91
  - exe/thronglets
36
92
  - lib/thronglets.rb
93
+ - lib/thronglets/activity.rb
37
94
  - lib/thronglets/cli.rb
38
- - lib/thronglets/thor_ext.rb
95
+ - lib/thronglets/concerns/abstract_class.rb
96
+ - lib/thronglets/concerns/input.rb
97
+ - lib/thronglets/concerns/output.rb
98
+ - lib/thronglets/listener.rb
99
+ - lib/thronglets/loader.rb
100
+ - lib/thronglets/registry.rb
101
+ - lib/thronglets/thor_ext/start.rb
39
102
  - lib/thronglets/version.rb
103
+ - lib/thronglets/worker.rb
104
+ - lib/thronglets/workflow.rb
40
105
  homepage: https://github.com/kkdoo/thronglets
41
106
  licenses:
42
107
  - MIT
@@ -53,7 +118,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
53
118
  requirements:
54
119
  - - ">="
55
120
  - !ruby/object:Gem::Version
56
- version: '3.1'
121
+ version: '3.2'
57
122
  required_rubygems_version: !ruby/object:Gem::Requirement
58
123
  requirements:
59
124
  - - ">="
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Thronglets
4
- module ThorExt
5
- # Configures Thor to behave more like a typical CLI, with better help and error handling.
6
- #
7
- # - Passing -h or --help to a command will show help for that command.
8
- # - Unrecognized options will be treated as errors (instead of being silently ignored).
9
- # - Error messages will be printed in red to stderr, without stack trace.
10
- # - Full stack traces can be enabled by setting the VERBOSE environment variable.
11
- # - Errors will cause Thor to exit with a non-zero status.
12
- #
13
- # To take advantage of this behavior, your CLI should subclass Thor and extend this module.
14
- #
15
- # class CLI < Thor
16
- # extend ThorExt::Start
17
- # end
18
- #
19
- # Start your CLI with:
20
- #
21
- # CLI.start
22
- #
23
- # In tests, prevent Kernel.exit from being called when an error occurs, like this:
24
- #
25
- # CLI.start(args, exit_on_failure: false)
26
- #
27
- module Start
28
- def self.extended(base)
29
- super
30
- base.check_unknown_options!
31
- end
32
-
33
- def start(given_args=ARGV, config={})
34
- config[:shell] ||= Thor::Base.shell.new
35
- handle_help_switches(given_args) do |args|
36
- dispatch(nil, args, nil, config)
37
- end
38
- rescue StandardError => e
39
- handle_exception_on_start(e, config)
40
- end
41
-
42
- private
43
-
44
- def handle_help_switches(given_args)
45
- yield(given_args.dup)
46
- rescue Thor::UnknownArgumentError => e
47
- retry_with_args = []
48
-
49
- if given_args.first == "help"
50
- retry_with_args = ["help"] if given_args.length > 1
51
- elsif e.unknown.intersect?(%w[-h --help])
52
- retry_with_args = ["help", (given_args - e.unknown).first]
53
- end
54
- raise unless retry_with_args.any?
55
-
56
- yield(retry_with_args)
57
- end
58
-
59
- def handle_exception_on_start(error, config)
60
- return if error.is_a?(Errno::EPIPE)
61
- raise if ENV["VERBOSE"] || !config.fetch(:exit_on_failure, true)
62
-
63
- message = error.message.to_s
64
- message.prepend("[#{error.class}] ") if message.empty? || !error.is_a?(Thor::Error)
65
-
66
- config[:shell]&.say_error(message, :red)
67
- exit(false)
68
- end
69
- end
70
- end
71
- end