novu 0.1.0 → 1.1.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.
data/Rakefile CHANGED
@@ -9,4 +9,5 @@ require "rubocop/rake_task"
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
12
- task default: %i[spec rubocop]
12
+ # TODO: add rubocop back to default task once all offenses are corrected
13
+ task default: %i[spec]
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Novu
4
+ class Api
5
+ # Module Novu::Api::Blueprints provides an API for managing templated for notifications that are sent out in the Novu application.
6
+ #
7
+ # This module includes methods for retrieving blueprints by templateID and grouping blueprints by category.
8
+ #
9
+ # For more information on the Novu Blueprint API, see https://docs.novu.co/api/get-messages/.
10
+ module Blueprints
11
+ # Returns the details of a particular template
12
+ #
13
+ # @pathParams
14
+ # @param `template_id` [String]
15
+ #
16
+ # @return [Hash] The list of properties that pertains to the template e.g. _id, name, description, active, draft, preferenceSettings, and many others.
17
+ # @return [number] status
18
+ # - Returns 200 if successful
19
+ def get_blueprint(template_id)
20
+ get("/blueprints/#{template_id}")
21
+ end
22
+
23
+ # Get V1blueprintsgroup by Category
24
+ def group_blueprints_by_category
25
+ get("/blueprints/group-by-category")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -33,11 +33,13 @@ module Novu
33
33
 
34
34
  # Apply Bulk Change
35
35
  #
36
+ # @bodyParams
37
+ # @param changeIds [Array] The list of environment IDs to apply changes to.
36
38
  # @return [Hash] updated change.
37
39
  # @return [number] status
38
40
  # - Returns 201 if the bulk change has been updated correctly.
39
- def apply_bulk_changes
40
- post("/changes/bulk/apply")
41
+ def apply_bulk_changes(changeIds)
42
+ post("/changes/bulk/apply", body: changeIds)
41
43
  end
42
44
 
43
45
  # Apply change
@@ -3,6 +3,7 @@
3
3
  module Novu
4
4
  class Api
5
5
  module Connection
6
+
6
7
  def get(path, options = {})
7
8
  request :get, path, options
8
9
  end
@@ -25,8 +26,36 @@ module Novu
25
26
 
26
27
  private
27
28
 
29
+ # Send API Request
30
+ #
31
+ # It applies exponential backoff strategy (if enabled) for failed requests.
32
+ # It also performs an idempotent request to safely retry requests without having duplication operations.
33
+ #
28
34
  def request(http_method, path, options)
35
+
36
+ if http_method.to_s == 'post' || http_method.to_s == 'patch'
37
+ self.class.default_options[:headers].merge!({ "Idempotency-Key" => "#{@idempotency_key.to_s.strip}" })
38
+ end
39
+
29
40
  response = self.class.send(http_method, path, options)
41
+
42
+ if ! [401, 403, 409, 500, 502, 503, 504].include?(response.code) && ! @enable_retry
43
+ response
44
+ elsif @enable_retry
45
+
46
+ if @retry_attempts < @max_retries
47
+ @retry_attempts += 1
48
+
49
+ @backoff.intervals.each do |interval|
50
+ sleep(interval)
51
+ request(http_method, path, options)
52
+ end
53
+ else
54
+ raise StandardError, "Max retry attempts reached"
55
+ end
56
+ else
57
+ response
58
+ end
30
59
  end
31
60
  end
32
61
  end
@@ -100,6 +100,17 @@ module Novu
100
100
  def in_app_status
101
101
  get("/integrations/in-app/status")
102
102
  end
103
+
104
+ # Set integration as primary
105
+ #
106
+ # @pathparams
107
+ # @param `integration_id` [String]
108
+ #
109
+ # @return [number] status
110
+ # - Returns 200 if successful
111
+ def set_integration_as_primary(integration_id)
112
+ post("/integrations/#{integration_id}/set-primary")
113
+ end
103
114
  end
104
115
  end
105
116
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Novu
4
+ class Api
5
+ # Module Novu::Api::Organizations provides an API for managing Organization within your Novu account.
6
+ #
7
+ # This module includes methods for creating, retrieving, updating and deleting organization.
8
+ #
9
+ # For more information on the Novu API see https://api.novu.co/api#/Organizations, https://docs.novu.co/api-reference/organizations/create-organization.
10
+ module Organizations
11
+ # Create an organization
12
+ #
13
+ # @bodyparams:
14
+ # @param `logo` [String] - A valid image URL with one of the following extensions: jpg, jpeg, png, gif, svg.
15
+ # @param `name` [String] - A human-readable name of the organization.
16
+ #
17
+ # @return [Hash] data - The list of information with respect to the created organization.
18
+ # @return [number] status - The status code. Returns 200 if the organization has been successfully created.
19
+ def create_organization(body)
20
+ post("/organizations", body: body)
21
+ end
22
+
23
+ # Get all organizations
24
+ #
25
+ # @return [Hash] data - The list of organizations already created.
26
+ # @return [number] status - Returns 200 if successful
27
+ def organizations()
28
+ get("/organizations")
29
+ end
30
+
31
+ # Get details of the current organization
32
+ #
33
+ # @return [Hash] data - The details of the current organization.
34
+ # @return [number] status - Returns 200 if successful
35
+ def current_organization()
36
+ get("/organizations/me")
37
+ end
38
+
39
+ # Get all members of the current organization
40
+ #
41
+ # @return [Hash] data - The list of all members of the current organization.
42
+ # @return [number] status - Returns 200 if successful
43
+ def current_organization_members()
44
+ get("/organizations/members")
45
+ end
46
+
47
+ # Rename organization name
48
+ #
49
+ # @return [Hash] data - The list of updated details of the organization.
50
+ # @return [number] status - Returns 200 if successful
51
+ def rename_organization(body)
52
+ patch("/organizations", body: body)
53
+ end
54
+
55
+ # Update organization branding details
56
+ #
57
+ # @bodyparams:
58
+ # @param `logo` [String] - A valid image URL with one of the following extensions: jpg, jpeg, png, gif, svg.
59
+ # @param `color` [String] - The hexadecimal color style of the organization.
60
+ # @param `contentBackground` [String] - The hexadecimal content background style of the organization.
61
+ # @param `fontColor` [String] - The hexadecimal font color style of the organization.
62
+ # @param `fontFamily` [String(optional)] - The font family style of the organization.
63
+ #
64
+ # @return [Hash] data - The list of branding details of the organization.
65
+ # @return [number] status - Returns 200 if successful
66
+ def organization_branding(body)
67
+ put("/organizations/branding", body: body)
68
+ end
69
+
70
+ # Remove a member from organization
71
+ #
72
+ # @pathparams
73
+ # @param `member_id` [String]
74
+ #
75
+ # @return [number] status - The status code. Returns 200 if memeber was removed successfully by their id.
76
+ def delete_organization_member(member_id)
77
+ delete("/organizations/members/#{member_id}")
78
+ end
79
+ end
80
+ end
81
+ end
@@ -45,7 +45,7 @@ module Novu
45
45
  # Retrieves the subscriber with the given ID.
46
46
  #
47
47
  # @pathparams
48
- # @param `subscribe_id` [String] The ID of the subscriber to retrieve.
48
+ # @param `subscriber_id` [String] The ID of the subscriber to retrieve.
49
49
  #
50
50
  # @return [Hash] The retrieved subscriber.
51
51
  # @return [number] status
@@ -103,6 +103,19 @@ module Novu
103
103
  put("/subscribers/#{subscriber_id}/credentials", body: body)
104
104
  end
105
105
 
106
+ # Delete subscriber credentials by providerId
107
+ # Delete subscriber credentials such as slack and expo tokens.
108
+ #
109
+ # @pathParams:
110
+ # @param `subscriberId` [String] The ID of the subscriber to update credentials.
111
+ # @param `providerId` [String] The provider identifier for the credentials
112
+ #
113
+ # @return [number] status
114
+ # - Returns 204 if the subscriber credentials has been deleted successfully.
115
+ def delete_subscriber_credentials(subscriberId, providerId)
116
+ delete("/subscribers/#{subscriberId}/credentials/#{providerId}")
117
+ end
118
+
106
119
  # Used to update the subscriber isOnline flag.
107
120
  #
108
121
  # @pathparams:
@@ -207,6 +220,81 @@ module Novu
207
220
  # - Returns 201 if successful
208
221
  def mark_message_action_seen(subscriber_id, message_id, type)
209
222
  post("/subscribers/#{subscriber_id}/messages/#{message_id}/actions/#{type}")
223
+ end
224
+
225
+ # Using this endpoint you can create multiple subscribers at once, to avoid multiple calls to the API.
226
+ # The bulk API is limited to 500 subscribers per request.
227
+ #
228
+ # @bodyparams:
229
+ # @param `subscribers` [Array[subscriber]]
230
+ # @subscriber : subscriber structure
231
+ # @param `firstName` [Stringoptional)] The first name of the subscriber.
232
+ # @param `lastName` [Stringoptional)] The last name of the subscriber.
233
+ # @param `email` [Stringoptional)] The email of the subscriber.
234
+ # @param `data` [Hash(optional)] The data object is used to pass additional custom information that could be used to identify the subscriber.
235
+ # @param `phone` [Hash(optional)] This phone of the subscriber.
236
+ # @param `locale` [String(optional)] The location of the subscriber.
237
+ # @param `subscriberId` [String] A unique identifier for the subscriber, usually correlates to the id the user in your systems.
238
+ # @param `avatar` [String(optional)] An http url to the profile image of your subscriber
239
+ #
240
+ # @return data [Hash]
241
+ # - updated [Array] - If the subscriber was updated
242
+ # - created [Array] - Array of objects for the subsribers ID created
243
+ # - failed [Array] - In case of an error, this field will contain the error message
244
+ #
245
+ # @return [number] status - The status code. Returns 201 if the subscribers were created successfully.
246
+ def bulk_create_subscribers(body)
247
+ post("/subscribers/bulk", body: body.to_json, headers: {'Content-Type': 'application/json'})
248
+ end
249
+ # Marks all the subscriber messages as read, unread, seen or unseen.
250
+ #
251
+ # @pathparams
252
+ # @param `subscriber_id` [String]
253
+ #
254
+ # @bodyParams:
255
+ # @param `markAs` [String] The type of action to perform either read, unread, seen or unseen.
256
+ # @param `feedIdentifier` [String|Array(Optional)] The feed id (or array) to mark messages of a particular feed.
257
+ #
258
+ # @return [number] status
259
+ # - Returns 201 if successful
260
+ def mark_all_subscriber_messages(subscriber_id, body)
261
+ post("/subscribers/#{subscriber_id}/messages/mark-all", body: body)
262
+ end
263
+
264
+ # Handle providers OAUTH redirect
265
+ #
266
+ # @pathparams:
267
+ # @param `subscriberId` [String]
268
+ # @param `providerId` [String]
269
+ #
270
+ # @queryparams:
271
+ # @param `code` [String]
272
+ # @param `hmacHash` [String]
273
+ # @param `environmentId` [String]
274
+ # @param `integrationIdentifier` [String]
275
+ #
276
+ # @return [Hash] The list of changes that match the criteria of the query params are successfully returned.
277
+ # @return [number] status
278
+ # - Returns 200 if successful
279
+ def provider_oauth_redirect(subscriberId, providerId, query = {})
280
+ get("/subscribers/#{subscriberId}/credentials/#{providerId}/oauth/callback", query: query)
281
+ end
282
+
283
+ # Handle chat OAUTH
284
+ #
285
+ # @pathparams:
286
+ # @param `subscriberId` [String]
287
+ # @param `providerId` [String]
288
+ #
289
+ # @queryparams:
290
+ # @param `hmacHash` [String]
291
+ # @param `environmentId` [String]
292
+ # @param `integrationIdentifier` [String]
293
+ #
294
+ # @return [number] status
295
+ # - Returns 200 if successful
296
+ def chat_oauth(subscriberId, providerId, query = {})
297
+ get("/subscribers/#{subscriberId}/credentials/#{providerId}/oauth", query: query)
210
298
  end
211
299
  end
212
300
  end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Novu
4
+ class Api
5
+ # Module Novu::Api::Tenants provides an API for managing tenants in the Novu application.
6
+ #
7
+ # This module includes methods for creating, retrieving, updating and deleting tenant.
8
+ #
9
+ # For more information on the tenants feature (https://docs.novu.co/tenants/introduction),
10
+ # for API documentation see https://docs.novu.co/api-reference/tenants/get-tenants.
11
+ module Tenants
12
+ # Create a tenant
13
+ #
14
+ # @bodyparams:
15
+ # @param `identifier` [String] - A unique value, and can be used later when pointing to this tenant during trigger calls.
16
+ # @param `name` [String] - A human-readable name of the tenant.
17
+ # @param `data` [Hash] - A custom data object that can store information about the tenant.
18
+ #
19
+ # @return [Hash] data - The list of information with respect to the created tenant are successfully returned.
20
+ # @return [number] status - The status code. Returns 200 if the tenant has been successfully created.
21
+ def create_tenant(body)
22
+ post("/tenants", body: body)
23
+ end
24
+
25
+ # Returns a list of tenant that can be paginated using the `page` query parameter and
26
+ # set the number of tenants to be with the `limit` query parameter
27
+ #
28
+ # @queryparams:
29
+ # @param `page` [Integer(optional)] - Number of page for the pagination.
30
+ # @param `limit` [Integer(optional)] - Size of page for the pagination.
31
+ #
32
+ # @return [Hash] data - The list of tenants that match the criteria of the query params are successfully returned.
33
+ # @return [Boolean] hasMore - To specify if the list have more items to fetch
34
+ # @return [number] page - The current page of the paginated response
35
+ # @return [number] pageSize - The number of size of each page
36
+ # @return [number] status
37
+ # - Returns 200 if successful
38
+ def tenants(query = {})
39
+ get("/tenants", query: query)
40
+ end
41
+
42
+ # Get a tenant by the tenant identifier
43
+ #
44
+ # @pathparams
45
+ # @param `identifier` [String]
46
+ #
47
+ # @return [Hash] data -The retrieved topic.
48
+ # @return [number] status
49
+ # - Returns 200 if successful
50
+ def tenant(identifier)
51
+ get("/tenants/#{identifier}")
52
+ end
53
+
54
+ # Update a tenant
55
+ #
56
+ # @pathparams
57
+ # @param `identifier` [String]
58
+ #
59
+ # @bodyparams:
60
+ # @param `identifier` [String] - A unique value, and can be used later when pointing to this tenant during trigger calls.
61
+ # @param `name` [String] - A human-readable name of the tenant.
62
+ # @param `data` [Hash] - A custom data object that can store information about the tenant. This data can be later accessed inside workflows.
63
+ #
64
+ # @return [Hash] data - The list of information with respect to the created tenant are successfully returned.
65
+ # @return [number] status - The status code. Returns 200 if the tenant has been successfully created.
66
+ def update_tenant(identifier, body)
67
+ patch("/tenants/#{identifier}", body: body)
68
+ end
69
+
70
+ # Using a previously create identifier during the tenant ceation, will cancel any active or pending workflows.
71
+ # This is useful to cancel active digests, delays etc...
72
+ #
73
+ # @pathparams:
74
+ # @param `identifier` [String] - identifier of the tenant
75
+ #
76
+ # @return [number] status - The status code. Returns 200 if the event has been successfully cancelled.
77
+ def delete_tenant(identifier)
78
+ delete("/tenants/#{identifier}")
79
+ end
80
+ end
81
+ end
82
+ end
@@ -61,6 +61,18 @@ module Novu
61
61
  post("/topics/#{topic_key}/subscribers/removal", body: body)
62
62
  end
63
63
 
64
+ # Check topic subsriber
65
+ # Check if a subscriber belongs to a certain topic
66
+ #
67
+ # @pathparams
68
+ # @param `topic_key` [String]
69
+ # @param `externalSubscriberId` [String] The id of the subscriber created on `/subscribers` endpoint
70
+ #
71
+ # @return [number] status - The status code. Returns 200 if subscriber was added to the topic.
72
+ def subscriber_topic(topic_key, externalSubscriberId)
73
+ get("/topics/#{topic_key}/subscribers/#{externalSubscriberId}")
74
+ end
75
+
64
76
  # Get a topic by its topic key
65
77
  #
66
78
  # @pathparams
@@ -87,6 +99,17 @@ module Novu
87
99
  def rename_topic(topic_key, body)
88
100
  patch("/topics/#{topic_key}", body: body)
89
101
  end
102
+
103
+ # Delete topic
104
+ # Delete a topic by its topic key if it has no subscribers
105
+ #
106
+ # @pathparams
107
+ # @param `topic_key` [String]
108
+ #
109
+ # @return [number] status - The status code. Returns 204 if successfully deleted topic.
110
+ def delete_topic(topic_key)
111
+ delete("/topics/#{topic_key}")
112
+ end
90
113
  end
91
114
  end
92
115
  end
data/lib/novu/client.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "novu/api/blueprints"
3
4
  require "novu/api/changes"
4
5
  require "novu/api/connection"
5
6
  require "novu/api/environments"
@@ -13,12 +14,15 @@ require "novu/api/messages"
13
14
  require "novu/api/notification_groups"
14
15
  require "novu/api/notification_templates"
15
16
  require "novu/api/notification"
17
+ require "novu/api/organizations"
16
18
  require "novu/api/subscribers"
19
+ require "novu/api/tenants"
17
20
  require "novu/api/topics"
18
21
 
19
22
  module Novu
20
23
  class Client
21
24
  include HTTParty
25
+ include Novu::Api::Blueprints
22
26
  include Novu::Api::Changes
23
27
  include Novu::Api::Connection
24
28
  include Novu::Api::Environments
@@ -32,19 +36,56 @@ module Novu
32
36
  include Novu::Api::NotificationGroups
33
37
  include Novu::Api::NotificationTemplates
34
38
  include Novu::Api::Notification
39
+ include Novu::Api::Organizations
35
40
  include Novu::Api::Subscribers
41
+ include Novu::Api::Tenants
36
42
  include Novu::Api::Topics
37
43
 
38
44
  base_uri "https://api.novu.co/v1"
39
45
  format :json
40
46
 
41
- def initialize(access_token = nil)
47
+ attr_accessor :enable_retry, :max_retries, :initial_delay, :max_delay, :idempotency_key
48
+
49
+ # @param `access_token` [String]
50
+ # @param `idempotency_key` [String]
51
+ # @param `enable_retry` [Boolean]
52
+ # @param `retry_config` [Hash]
53
+ # - max_retries [Integer]
54
+ # - initial_delay [Integer]
55
+ # - max_delay [Integer]
56
+ def initialize(access_token: nil, idempotency_key: nil, enable_retry: false, retry_config: {} )
42
57
  raise ArgumentError, "Api Key cannot be blank or nil" if access_token.blank?
43
58
 
59
+ @idempotency_key = idempotency_key.blank? ? UUID.new.generate : idempotency_key
60
+
61
+ @enable_retry = enable_retry
44
62
  @access_token = access_token.to_s.strip
63
+ @retry_attempts = 0
64
+
65
+ retry_config = defaults_retry_config.merge(retry_config)
66
+ @max_retries = retry_config[:max_retries]
67
+ @initial_delay = retry_config[:initial_delay]
68
+ @max_delay = retry_config[:max_delay]
69
+
45
70
  self.class.default_options.merge!(headers: { "Authorization" => "ApiKey #{@access_token}" })
71
+
72
+ # Configure the exponential backoff - specifying initial and maximal delays, default is 4s and 60s respectively
73
+ if @enable_retry
74
+ @retry_config = retry_config
75
+ @backoff = ExponentialBackoff.new(@initial_delay, @max_delay)
76
+ end
46
77
  rescue ArgumentError => e
47
78
  puts "Error initializing Novu client: #{e.message}"
48
79
  end
80
+
81
+ private
82
+
83
+ # @retun [Hash]
84
+ # - max_retries [Integer]
85
+ # - initial_delay [Integer]
86
+ # - max_delay [Integer]
87
+ def defaults_retry_config
88
+ { max_retries: 1, initial_delay: 4, max_delay: 60 }
89
+ end
49
90
  end
50
91
  end
data/lib/novu/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Novu
4
- VERSION = "0.1.0"
4
+ VERSION = "1.1.0"
5
5
  end
data/lib/novu.rb CHANGED
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/hash"
4
+ require "exponential_backoff"
4
5
  require "httparty"
5
6
  require_relative "novu/version"
6
7
  require_relative "novu/client"
8
+ require "uuid"
7
9
 
8
10
  module Novu
9
11
  # class Error < StandardError; end
data/techstack.md ADDED
@@ -0,0 +1,125 @@
1
+ <!--
2
+ &lt;--- Readme.md Snippet without images Start ---&gt;
3
+ ## Tech Stack
4
+ novuhq/novu-ruby is built on the following main stack:
5
+
6
+ - [Ruby](https://www.ruby-lang.org) – Languages
7
+ - [RSpec](https://rspec.info/) – Testing Frameworks
8
+ - [RuboCop](http://batsov.com/rubocop/) – Code Review
9
+ - [Shell](https://en.wikipedia.org/wiki/Shell_script) – Shells
10
+ - [GitHub Actions](https://github.com/features/actions) – Continuous Integration
11
+
12
+ Full tech stack [here](/techstack.md)
13
+
14
+ &lt;--- Readme.md Snippet without images End ---&gt;
15
+
16
+ &lt;--- Readme.md Snippet with images Start ---&gt;
17
+ ## Tech Stack
18
+ novuhq/novu-ruby is built on the following main stack:
19
+
20
+ - <img width='25' height='25' src='https://img.stackshare.io/service/989/ruby.png' alt='Ruby'/> [Ruby](https://www.ruby-lang.org) – Languages
21
+ - <img width='25' height='25' src='https://img.stackshare.io/service/2539/logo.png' alt='RSpec'/> [RSpec](https://rspec.info/) – Testing Frameworks
22
+ - <img width='25' height='25' src='https://img.stackshare.io/service/2643/rubocop.png' alt='RuboCop'/> [RuboCop](http://batsov.com/rubocop/) – Code Review
23
+ - <img width='25' height='25' src='https://img.stackshare.io/service/4631/default_c2062d40130562bdc836c13dbca02d318205a962.png' alt='Shell'/> [Shell](https://en.wikipedia.org/wiki/Shell_script) – Shells
24
+ - <img width='25' height='25' src='https://img.stackshare.io/service/11563/actions.png' alt='GitHub Actions'/> [GitHub Actions](https://github.com/features/actions) – Continuous Integration
25
+
26
+ Full tech stack [here](/techstack.md)
27
+
28
+ &lt;--- Readme.md Snippet with images End ---&gt;
29
+ -->
30
+ <div align="center">
31
+
32
+ # Tech Stack File
33
+ ![](https://img.stackshare.io/repo.svg "repo") [novuhq/novu-ruby](https://github.com/novuhq/novu-ruby)![](https://img.stackshare.io/public_badge.svg "public")
34
+ <br/><br/>
35
+ |11<br/>Tools used|01/05/24 <br/>Report generated|
36
+ |------|------|
37
+ </div>
38
+
39
+ ## <img src='https://img.stackshare.io/languages.svg'/> Languages (1)
40
+ <table><tr>
41
+ <td align='center'>
42
+ <img width='36' height='36' src='https://img.stackshare.io/service/989/ruby.png' alt='Ruby'>
43
+ <br>
44
+ <sub><a href="https://www.ruby-lang.org">Ruby</a></sub>
45
+ <br>
46
+ <sub></sub>
47
+ </td>
48
+
49
+ </tr>
50
+ </table>
51
+
52
+ ## <img src='https://img.stackshare.io/devops.svg'/> DevOps (5)
53
+ <table><tr>
54
+ <td align='center'>
55
+ <img width='36' height='36' src='https://img.stackshare.io/service/1046/git.png' alt='Git'>
56
+ <br>
57
+ <sub><a href="http://git-scm.com/">Git</a></sub>
58
+ <br>
59
+ <sub></sub>
60
+ </td>
61
+
62
+ <td align='center'>
63
+ <img width='36' height='36' src='https://img.stackshare.io/service/11563/actions.png' alt='GitHub Actions'>
64
+ <br>
65
+ <sub><a href="https://github.com/features/actions">GitHub Actions</a></sub>
66
+ <br>
67
+ <sub></sub>
68
+ </td>
69
+
70
+ <td align='center'>
71
+ <img width='36' height='36' src='https://img.stackshare.io/service/2539/logo.png' alt='RSpec'>
72
+ <br>
73
+ <sub><a href="https://rspec.info/">RSpec</a></sub>
74
+ <br>
75
+ <sub>v3.12.0</sub>
76
+ </td>
77
+
78
+ <td align='center'>
79
+ <img width='36' height='36' src='https://img.stackshare.io/service/2643/rubocop.png' alt='RuboCop'>
80
+ <br>
81
+ <sub><a href="http://batsov.com/rubocop/">RuboCop</a></sub>
82
+ <br>
83
+ <sub>v1.46.0</sub>
84
+ </td>
85
+
86
+ <td align='center'>
87
+ <img width='36' height='36' src='https://img.stackshare.io/service/12795/5jL6-BA5_400x400.jpeg' alt='RubyGems'>
88
+ <br>
89
+ <sub><a href="https://rubygems.org/">RubyGems</a></sub>
90
+ <br>
91
+ <sub></sub>
92
+ </td>
93
+
94
+ </tr>
95
+ </table>
96
+
97
+ ## Other (1)
98
+ <table><tr>
99
+ <td align='center'>
100
+ <img width='36' height='36' src='https://img.stackshare.io/service/4631/default_c2062d40130562bdc836c13dbca02d318205a962.png' alt='Shell'>
101
+ <br>
102
+ <sub><a href="https://en.wikipedia.org/wiki/Shell_script">Shell</a></sub>
103
+ <br>
104
+ <sub></sub>
105
+ </td>
106
+
107
+ </tr>
108
+ </table>
109
+
110
+
111
+ ## <img src='https://img.stackshare.io/group.svg' /> Open source packages (4)</h2>
112
+
113
+ ## <img width='24' height='24' src='https://img.stackshare.io/service/12795/5jL6-BA5_400x400.jpeg'/> RubyGems (4)
114
+
115
+ |NAME|VERSION|LAST UPDATED|LAST UPDATED BY|LICENSE|VULNERABILITIES|
116
+ |:------|:------|:------|:------|:------|:------|
117
+ |[activesupport](https://rubygems.org/activesupport)|v6.1.7|10/12/23|unicodeveloper |MIT|N/A|
118
+ |[httparty](https://rubygems.org/httparty)|v0.21.0|10/12/23|unicodeveloper |MIT|N/A|
119
+ |[rake](https://rubygems.org/rake)|v13.0.6|02/28/23|Aman Saini |MIT|N/A|
120
+ |[webmock](https://rubygems.org/webmock)|v3.18.1|03/10/23|Aman Saini |MIT|N/A|
121
+
122
+ <br/>
123
+ <div align='center'>
124
+
125
+ Generated via [Stack File](https://github.com/marketplace/stack-file)