multiwoven-integrations 0.33.6 → 0.34.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 48b04e24af2a47fa9b506e924fa3b5540e2a8da35cd5abac444e495a68bd3440
4
- data.tar.gz: b9d01d845e32acaa279aa02ccec2699900e36469640ae0f5b6d5ce8d42773077
3
+ metadata.gz: 3ebf8ef48419c9724217ce90888b0cea454b715dcaec7cab8903c3b994e0e23f
4
+ data.tar.gz: 94048a4a4d2794fdeee73b9b2426f18e77f5412d6af9eab4a3bbafcac934277d
5
5
  SHA512:
6
- metadata.gz: 3568483d79a0260b4742b7042595a5ad27783bd70adf14f388b095a0fb37fd45d83d0250eb90cf2c1d3870a7ca7ed4955b39585ff54c5ffe580a8b3aced55d9b
7
- data.tar.gz: 0d67ef9289ebf131ab54385adb81477574ac8c6ff207f110211d976f8090cc0d6f5abf85a9f6f3a1f075025301466e469d2e575a609c3218f8d249ed3393effd
6
+ metadata.gz: 5f02b62f2097263bac9db7cbf9ea3961d72816ca6af8628a8fb56fc54626c344647471d2774da61abcff0d7031f20fdc76fea2631651535435a797da3c9eea79
7
+ data.tar.gz: f7de34a831108a64f987f510882568c2ac19d58177be6178ffbe25b97f4baf80ca59bca3d62afb0055d2d0e86d97b98383dc8756ead177b63302fc8e424f2229
@@ -14,6 +14,18 @@ module Multiwoven
14
14
 
15
15
  super(sync_config, records, action)
16
16
  end
17
+
18
+ def read(sync_config)
19
+ stream = sync_config.stream
20
+
21
+ @queue ||= Limiter::RateQueue.new(stream.request_rate_limit, interval: stream.rate_limit_unit_seconds) do
22
+ Integrations::Service.logger.info("Hit the limit for stream: #{stream.name}, waiting")
23
+ end
24
+
25
+ @queue.shift
26
+
27
+ super(sync_config)
28
+ end
17
29
  end
18
30
  end
19
31
  end
@@ -165,6 +165,12 @@ module Multiwoven
165
165
  end
166
166
  end
167
167
 
168
+ class IncrementStrategyConfig < ProtocolModel
169
+ attr_accessor :offset, :limit, :offset_variable, :limit_variable
170
+
171
+ attribute :increment_strategy, Types::String.default("page")
172
+ end
173
+
168
174
  class SyncConfig < ProtocolModel
169
175
  attr_accessor :offset, :limit, :sync_run_id
170
176
 
@@ -178,6 +184,8 @@ module Multiwoven
178
184
  attribute :destination_sync_mode, DestinationSyncMode
179
185
  # reference ids
180
186
  attribute :sync_id, Types::String.default("unknown")
187
+ # increment strategy
188
+ attribute? :increment_strategy_config, IncrementStrategyConfig.optional
181
189
  end
182
190
 
183
191
  class VectorConfig < ProtocolModel
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Multiwoven
4
4
  module Integrations
5
- VERSION = "0.33.6"
5
+ VERSION = "0.34.0"
6
6
 
7
7
  ENABLED_SOURCES = %w[
8
8
  Snowflake
@@ -33,6 +33,7 @@ module Multiwoven
33
33
  Firecrawl
34
34
  Odoo
35
35
  GoogleDrive
36
+ Http
36
37
  ].freeze
37
38
 
38
39
  ENABLED_DESTINATIONS = %w[
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Multiwoven::Integrations::Source
4
+ module Http
5
+ include Multiwoven::Integrations::Core
6
+ class Client < SourceConnector
7
+ def check_connection(connection_config)
8
+ connection_config = prepare_config(connection_config)
9
+ create_connection(connection_config)
10
+ if connection_config[:sample_query].blank?
11
+ build_paginated_request(connection_config, {})
12
+ else
13
+ sample_query = JSON.parse(connection_config[:sample_query])
14
+ build_paginated_request(connection_config, sample_query.values.first)
15
+ end
16
+ response = send_request(
17
+ url: @url,
18
+ http_method: connection_config[:http_method],
19
+ payload: connection_config[:request_format],
20
+ headers: connection_config[:headers],
21
+ config: connection_config[:config],
22
+ params: connection_config[:params]
23
+ )
24
+ success?(response) ? success_status : failure_status(nil)
25
+ rescue StandardError => e
26
+ handle_exception(e, { context: "HTTP:CHECK_CONNECTION:EXCEPTION", type: "error" })
27
+ failure_status(e)
28
+ end
29
+
30
+ def discover(connection_config)
31
+ connection_config = prepare_config(connection_config)
32
+ create_connection(connection_config)
33
+ if connection_config[:sample_query].blank?
34
+ build_paginated_request(connection_config, {})
35
+ else
36
+ sample_query = JSON.parse(connection_config[:sample_query])
37
+ build_paginated_request(connection_config, sample_query.values.first)
38
+ end
39
+ response = send_request(
40
+ url: @url,
41
+ http_method: connection_config[:http_method],
42
+ payload: connection_config[:request_format],
43
+ headers: connection_config[:headers],
44
+ config: connection_config[:config],
45
+ params: connection_config[:params]
46
+ )
47
+ raise StandardError, "Response code: #{response.code}, Body: #{response.body}" unless success?(response)
48
+
49
+ catalog = Catalog.new(streams: create_streams(JSON.parse(response.body)))
50
+ catalog.to_multiwoven_message
51
+ rescue StandardError => e
52
+ handle_exception(e, { context: "HTTP:DISCOVER:EXCEPTION", type: "error" })
53
+ end
54
+
55
+ def read(sync_config)
56
+ connection_config = sync_config.source.connection_specification
57
+ connection_config = connection_config.with_indifferent_access
58
+ connection_config = create_connection(connection_config)
59
+ if sync_config.increment_strategy_config
60
+ @limit = sync_config.increment_strategy_config.limit
61
+ @offset = sync_config.increment_strategy_config.offset
62
+ else
63
+ @limit = sync_config.limit
64
+ @offset = sync_config.offset
65
+ end
66
+ query = sync_config.model.query
67
+ query(connection_config, query)
68
+ rescue StandardError => e
69
+ handle_exception(e, {
70
+ context: "HTTP:READ:EXCEPTION",
71
+ type: "error",
72
+ sync_id: sync_config.sync_id,
73
+ sync_run_id: sync_config.sync_run_id
74
+ })
75
+ end
76
+
77
+ private
78
+
79
+ def prepare_config(config)
80
+ config.with_indifferent_access.tap do |conf|
81
+ conf[:config][:timeout] ||= 30
82
+ end
83
+ end
84
+
85
+ def create_connection(connection_config)
86
+ @url = "#{connection_config[:base_url]}#{connection_config[:path]}"
87
+ connection_config
88
+ end
89
+
90
+ def build_paginated_request(connection_config, query)
91
+ connection_config[:request_format] = JSON.parse(connection_config[:request_format] || "{}")
92
+
93
+ apply_param_pagination(connection_config)
94
+ apply_batched_query(connection_config, query)
95
+ end
96
+
97
+ def apply_param_pagination(connection_config)
98
+ return unless connection_config[:limit_param].present? && connection_config[:offset_param].present?
99
+
100
+ connection_config[:params] = {} if connection_config[:params].nil?
101
+ connection_config[:params].merge!({ connection_config[:limit_param] => @limit }) if @limit.present?
102
+ connection_config[:params].merge!({ connection_config[:offset_param] => @offset }) if @offset.present?
103
+ end
104
+
105
+ def apply_batched_query(connection_config, query)
106
+ return unless connection_config[:sample_query].present?
107
+
108
+ sample_query = JSON.parse(connection_config[:sample_query])
109
+ query = batched_query(query, @limit, @offset) unless @limit.nil? && @offset.nil?
110
+ connection_config[:request_format].merge!({ sample_query.keys.first => query }) unless query.nil?
111
+ end
112
+
113
+ def query(connection_config, query)
114
+ connection_config = prepare_config(connection_config)
115
+ build_paginated_request(connection_config, query)
116
+ response = send_request(
117
+ url: @url,
118
+ http_method: connection_config[:http_method],
119
+ payload: connection_config[:request_format],
120
+ headers: connection_config[:headers],
121
+ config: connection_config[:config],
122
+ params: connection_config[:params] || {}
123
+ )
124
+ if success?(response)
125
+ response_body = JSON.parse(response.body)
126
+ parse_response = get_parse_response(connection_config[:parse_response])
127
+ parse_response(response_body, parse_response)
128
+ else
129
+ handle_exception("Failed to fetch data", { context: "HTTP:QUERY:EXCEPTION", type: "error" })
130
+ end
131
+ end
132
+
133
+ def create_streams(response_body)
134
+ group_by_table(response_body).map do |r|
135
+ Multiwoven::Integrations::Protocol::Stream.new(name: r["name"], action: StreamAction["fetch"], json_schema: r["schema"])
136
+ end
137
+ end
138
+
139
+ def get_parse_response(parse_response)
140
+ parse_response = JSON.parse(parse_response) if parse_response.is_a?(String) && parse_response.start_with?("[")
141
+ parse_response
142
+ end
143
+
144
+ def parse_response(response_body, parse_response)
145
+ case parse_response
146
+ when Array
147
+ records = []
148
+ parse_response.each do |path|
149
+ records << JsonPath.on(response_body, path)
150
+ end
151
+ records[1].each_slice(records[0].size).map do |row_values|
152
+ data = Hash[records[0].zip(row_values)]
153
+ RecordMessage.new(data: data, emitted_at: Time.now.to_i).to_multiwoven_message
154
+ end
155
+ else
156
+ records = JsonPath.on(response_body, parse_response)
157
+ records.map do |data|
158
+ RecordMessage.new(data: data, emitted_at: Time.now.to_i).to_multiwoven_message
159
+ end
160
+ end
161
+ end
162
+
163
+ def build_schema(record)
164
+ case record
165
+ when Hash
166
+ {
167
+ "type" => "object",
168
+ "properties" => record.transform_values { |value| build_schema(value) }
169
+ }
170
+ when Array
171
+ {
172
+ "type" => "array",
173
+ "items" => build_schema(record.first)
174
+ }
175
+ else
176
+ { "type" => %w[string null] }
177
+ end
178
+ end
179
+
180
+ def group_by_table(response_body)
181
+ schema = []
182
+ response_body.each do |key, values|
183
+ schema << {
184
+ "name" => key.to_s,
185
+ "schema" => {
186
+ "$schema" => "http://json-schema.org/draft-07/schema#",
187
+ "type" => "object",
188
+ "properties" => {
189
+ key.to_s => build_schema(values)
190
+ }
191
+ }
192
+ }
193
+ end
194
+ schema
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,16 @@
1
+ {
2
+ "data": {
3
+ "name": "Http",
4
+ "title": "HTTP",
5
+ "connector_type": "source",
6
+ "category": "Data Warehouse",
7
+ "sub_category": "Relational Database",
8
+ "documentation_url": "https://docs.squared.ai/guides/sources/data-sources/http",
9
+ "github_issue_label": "source-http",
10
+ "icon": "icon.svg",
11
+ "license": "MIT",
12
+ "release_stage": "alpha",
13
+ "support_level": "community",
14
+ "tags": ["language:ruby", "multiwoven"]
15
+ }
16
+ }
@@ -0,0 +1,117 @@
1
+ {
2
+ "documentation_url": "https://docs.squared.ai/activation/ai-ml-sources/http-model-endpoint",
3
+ "stream_type": "dynamic",
4
+ "connector_query_type": "raw_sql",
5
+ "connection_specification": {
6
+ "$schema": "http://json-schema.org/draft-07/schema#",
7
+ "title": "HTTP",
8
+ "type": "object",
9
+ "required": ["increment_type","base_url", "http_method"],
10
+ "properties": {
11
+ "increment_type": {
12
+ "type": "string",
13
+ "title": "Increment Type",
14
+ "enum": ["Page", "Offset"],
15
+ "default": "Page",
16
+ "order": 0
17
+ },
18
+ "http_method": {
19
+ "type": "string",
20
+ "title": "HTTP Method",
21
+ "enum": ["POST", "GET"],
22
+ "order": 1
23
+ },
24
+ "base_url": {
25
+ "type": "string",
26
+ "title": "URL",
27
+ "order": 2
28
+ },
29
+ "path": {
30
+ "type": "string",
31
+ "title": "Path",
32
+ "description": "The path to the resource on the server. This is appended to the base URL.",
33
+ "order": 3,
34
+ "default": "/"
35
+ },
36
+ "headers": {
37
+ "title": "HTTP Headers",
38
+ "description": "Custom headers to include in the HTTP request. Useful for authentication, content type specifications, and other request metadata.",
39
+ "order": 4,
40
+ "additionalProperties": {
41
+ "type": "string"
42
+ }
43
+ },
44
+ "config": {
45
+ "title": "",
46
+ "type": "object",
47
+ "properties": {
48
+ "timeout": {
49
+ "type": "string",
50
+ "default": "30",
51
+ "title": "HTTP Timeout",
52
+ "description": "The maximum time, in seconds, to wait for a response from the server before the request is canceled.",
53
+ "order": 0
54
+ }
55
+ },
56
+ "order": 5
57
+ },
58
+ "request_format":{
59
+ "title": "Request Format",
60
+ "description": "Request payload format. This should be a valid JSON string that matches the expected input of the HTTP endpoint.",
61
+ "type": "string",
62
+ "x-request-format": true,
63
+ "order": 6
64
+ },
65
+ "sample_query": {
66
+ "title": "Sample Query",
67
+ "description": "A sample query to test the connection and retrieve a sample response from the HTTP endpoint. This should be a valid JSON string that matches the expected input of the HTTP endpoint.",
68
+ "type": "string",
69
+ "x-request-format": true,
70
+ "order": 7,
71
+ "default": "{ \"query\": \"Select * from table\" }"
72
+ },
73
+ "parse_response": {
74
+ "title": "Parse Response",
75
+ "description": "A JSONPath expression that points to a single record (usually the first row) in the response. The keys of this object are treated as the column names. Example: `$.data[0]`",
76
+ "type": "string",
77
+ "x-response-format": true,
78
+ "order": 8,
79
+ "default": "$.data[0]"
80
+ },
81
+ "params": {
82
+ "title": "Query Parameters",
83
+ "description": "Key-value pairs to be included as query parameters in the HTTP request URL.",
84
+ "order": 9,
85
+ "additionalProperties": {
86
+ "type": "string"
87
+ }
88
+ },
89
+ "offset_param": {
90
+ "type": "string",
91
+ "title": "Offset Parameter",
92
+ "description": "The name of the query parameter used to specify the offset for pagination.",
93
+ "order": 10,
94
+ "default": "offset"
95
+ },
96
+ "limit_param": {
97
+ "type": "string",
98
+ "title": "Limit Parameter",
99
+ "description": "The name of the query parameter used to specify the limit for pagination.",
100
+ "order": 11,
101
+ "default": "limit"
102
+ },
103
+ "page_size": {
104
+ "type": "string",
105
+ "title": "Page Size",
106
+ "description": "The number of records to fetch per page. This value is used in conjunction with the limit parameter for pagination.",
107
+ "order": 12
108
+ },
109
+ "page_start": {
110
+ "type": "string",
111
+ "title": "Page Start",
112
+ "description": "The starting page number for pagination. This value is used to calculate the offset for the initial request.",
113
+ "order": 13
114
+ }
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+
3
+ <svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
4
+
5
+
6
+
7
+
8
+
9
+
@@ -45,6 +45,7 @@ require "xmlrpc/client"
45
45
  require "googleauth"
46
46
  require "google/apis/drive_v3"
47
47
  require "aws-sdk-textract"
48
+ require "jsonpath"
48
49
 
49
50
  # Service
50
51
  require_relative "integrations/config"
@@ -96,6 +97,7 @@ require_relative "integrations/source/qdrant/client"
96
97
  require_relative "integrations/source/firecrawl/client"
97
98
  require_relative "integrations/source/odoo/client"
98
99
  require_relative "integrations/source/google_drive/client"
100
+ require_relative "integrations/source/http/client"
99
101
 
100
102
  # Destination
101
103
  require_relative "integrations/destination/klaviyo/client"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: multiwoven-integrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.33.6
4
+ version: 0.34.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Subin T P
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-09-13 00:00:00.000000000 Z
11
+ date: 2025-09-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -775,6 +775,10 @@ files:
775
775
  - lib/multiwoven/integrations/source/google_vertex_model/config/meta.json
776
776
  - lib/multiwoven/integrations/source/google_vertex_model/config/spec.json
777
777
  - lib/multiwoven/integrations/source/google_vertex_model/icon.svg
778
+ - lib/multiwoven/integrations/source/http/client.rb
779
+ - lib/multiwoven/integrations/source/http/config/meta.json
780
+ - lib/multiwoven/integrations/source/http/config/spec.json
781
+ - lib/multiwoven/integrations/source/http/icon.svg
778
782
  - lib/multiwoven/integrations/source/http_model/client.rb
779
783
  - lib/multiwoven/integrations/source/http_model/config/catalog.json
780
784
  - lib/multiwoven/integrations/source/http_model/config/meta.json