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 +4 -4
- data/lib/multiwoven/integrations/core/rate_limiter.rb +12 -0
- data/lib/multiwoven/integrations/protocol/protocol.rb +8 -0
- data/lib/multiwoven/integrations/rollout.rb +2 -1
- data/lib/multiwoven/integrations/source/http/client.rb +198 -0
- data/lib/multiwoven/integrations/source/http/config/meta.json +16 -0
- data/lib/multiwoven/integrations/source/http/config/spec.json +117 -0
- data/lib/multiwoven/integrations/source/http/icon.svg +9 -0
- data/lib/multiwoven/integrations.rb +2 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3ebf8ef48419c9724217ce90888b0cea454b715dcaec7cab8903c3b994e0e23f
|
4
|
+
data.tar.gz: 94048a4a4d2794fdeee73b9b2426f18e77f5412d6af9eab4a3bbafcac934277d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
+
}
|
@@ -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.
|
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-
|
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
|