multiwoven-integrations 0.26.1 → 0.27.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: 82e2969a45b00adf857272cb9968d3150c199c7f707b6c0e5d4dd68031f060e1
4
- data.tar.gz: bd52fb19761cbbb3f6d03b15869ecaa4826329769ae14138be73ab272c8d8a63
3
+ metadata.gz: dc21bbb426fb6d928bd910e3ce1d02b8dfa64c8b81d2f229cee83144a6c2fa2b
4
+ data.tar.gz: b6568f1feb3ad15f14c2092ac4299838d680d0b275c0ba7ea06327d566dec12e
5
5
  SHA512:
6
- metadata.gz: d146c4789ecf4a5ab24d92fc95de0594cf2d41667c2d79c2a96ae0b7ff3bcab13acf69710c16fff6b163cfd13926e8a837656131573abe7fff9b3764d5a4d55f
7
- data.tar.gz: 2ab7ae3b4e4a9e91c4bf0c0b296b730f3e2d5a0f1f4872267a956476e128ff72dc4d85667be92fffd64c14bb7eee2a49a3d96cb18cb3c2bbaaaf30ab0df997d1
6
+ metadata.gz: 34a9f069e6164ea242ea583a9456b3f84cf77cb0ded8ebae69df728277a6b5d985ed475b490cc7233744167e2a5e098e69d3b8768131e0dc8c96f75305ca684e
7
+ data.tar.gz: 942d74b686bdb030165662d7b76317842ecd2e35e16494fd28480a37f1fc2073670b5a1f14720e424dd1ac98672cf76a3e5686f03d1c23fbe570f69bd40e6601
@@ -83,6 +83,11 @@ module Multiwoven
83
83
  mistral.mixtral-8x7b-instruct-v0:1
84
84
  mistral.mistral-small-2402-v1:0
85
85
  ].freeze
86
+
87
+ # Intuit QuickBooks
88
+ QUICKBOOKS_SANDBOX_QUERY_URL = "https://sandbox-quickbooks.api.intuit.com/v3/company/%<realm_id>s/query?query=%<query>s"
89
+ QUICKBOOKS_PRODUCTION_QUERY_URL = "https://quickbooks.api.intuit.com/v3/company/%<realm_id>s/query?query=%<query>s"
90
+ QUICKBOOKS_REDIRECT_URL = "https://developer.intuit.com/v2/OAuth2Playground/RedirectUrl"
86
91
  end
87
92
  end
88
93
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Multiwoven
4
4
  module Integrations
5
- VERSION = "0.26.1"
5
+ VERSION = "0.27.0"
6
6
 
7
7
  ENABLED_SOURCES = %w[
8
8
  Snowflake
@@ -27,6 +27,7 @@ module Multiwoven
27
27
  Anthropic
28
28
  AwsBedrockModel
29
29
  GenericOpenAI
30
+ IntuitQuickBooks
30
31
  ].freeze
31
32
 
32
33
  ENABLED_DESTINATIONS = %w[
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Multiwoven::Integrations::Source
4
+ module IntuitQuickBooks
5
+ include Multiwoven::Integrations::Core
6
+
7
+ QUICKBOOKS_OBJECTS = %w[Account Customer Employee Invoice TimeActivity].freeze
8
+ MAX_PER_PAGE = 1000
9
+
10
+ class Client < SourceConnector
11
+ def check_connection(connection_config)
12
+ connection_config = connection_config.with_indifferent_access
13
+ access_token = create_connection(connection_config)
14
+ query = "SELECT * FROM Customer STARTPOSITION 1 MAXRESULTS 1"
15
+ response = query_quickbooks(access_token, query)
16
+ if success?(response)
17
+ success_status
18
+ else
19
+ failure_status(nil)
20
+ end
21
+ rescue StandardError => e
22
+ handle_exception(e, { context: "INTUIT_QUICKBOOKS:CHECK_CONNECTION:EXCEPTION", type: "error" })
23
+ failure_status(e)
24
+ end
25
+
26
+ def discover(connection_config)
27
+ connection_config = connection_config.with_indifferent_access
28
+ access_token = create_connection(connection_config)
29
+ catalog = build_catalog(load_catalog.with_indifferent_access)
30
+ streams = catalog[:streams]
31
+ QUICKBOOKS_OBJECTS.each do |object|
32
+ query = "SELECT * FROM #{object}"
33
+ response = query_quickbooks(access_token, query)
34
+ streams << create_streams(JSON.parse(response.body)["QueryResponse"])[0]
35
+ rescue StandardError => e
36
+ handle_exception(e, { context: "INTUIT_QUICKBOOKS:DISCOVER:LOOP_EXCEPTION", type: "error" })
37
+ next
38
+ end
39
+ catalog.to_multiwoven_message
40
+ rescue StandardError => e
41
+ handle_exception(e, { context: "INTUIT_QUICKBOOKS:DISCOVER:EXCEPTION", type: "error" })
42
+ end
43
+
44
+ def read(sync_config)
45
+ connection_config = sync_config.source.connection_specification
46
+ connection_config = connection_config.with_indifferent_access
47
+ query = sync_config.model.query
48
+ query = batched_query(query, sync_config.limit, sync_config.offset) unless sync_config.limit.nil? && sync_config.offset.nil?
49
+ access_token = create_connection(connection_config)
50
+ query(access_token, query)
51
+ rescue StandardError => e
52
+ handle_exception(e, {
53
+ context: "INTUIT_QUICKBOOKS:READ:EXCEPTION",
54
+ type: "error",
55
+ sync_id: sync_config.sync_id,
56
+ sync_run_id: sync_config.sync_run_id
57
+ })
58
+ end
59
+
60
+ private
61
+
62
+ def query(access_token, query)
63
+ parsed = batched_query_for_quickbooks(query)
64
+ base_query = parsed[:base_query]
65
+ limit = parsed[:limit]
66
+ offset = parsed[:offset]
67
+ execute_query(access_token, base_query, limit, offset).map do |r|
68
+ flat_data = flatten_hash(r)
69
+ RecordMessage.new(data: flat_data, emitted_at: Time.now.to_i).to_multiwoven_message
70
+ end
71
+ end
72
+
73
+ def execute_query(access_token, base_query, limit, offset)
74
+ total_fetched = 0
75
+ current_offset = offset
76
+ result = []
77
+
78
+ while total_fetched < limit
79
+ batch_limit = [MAX_PER_PAGE, limit - total_fetched].min
80
+ paginated_query = "#{base_query} STARTPOSITION #{current_offset + 1} MAXRESULTS #{batch_limit}"
81
+
82
+ response = query_quickbooks(access_token, paginated_query)
83
+ records = JSON.parse(response.body)["QueryResponse"] || {}
84
+
85
+ break if records.empty?
86
+
87
+ records.each_value do |rows|
88
+ next unless rows.is_a?(Array)
89
+
90
+ rows.each do |row|
91
+ result << row
92
+ end
93
+ end
94
+
95
+ fetched_count = result.size - total_fetched
96
+ break if fetched_count < batch_limit
97
+
98
+ total_fetched += fetched_count
99
+ current_offset += fetched_count
100
+ end
101
+ result
102
+ end
103
+
104
+ def query_quickbooks(access_token, query)
105
+ encoded_query = URI.encode_www_form_component(query)
106
+ query_url = @environment == "sandbox" ? QUICKBOOKS_SANDBOX_QUERY_URL : QUICKBOOKS_PRODUCTION_QUERY_URL
107
+ send_request(
108
+ url: build_url(query_url, encoded_query),
109
+ http_method: HTTP_GET,
110
+ payload: {},
111
+ headers: auth_headers(access_token),
112
+ config: {}
113
+ )
114
+ end
115
+
116
+ def create_connection(connection_config)
117
+ cache = defined?(Rails) && Rails.respond_to?(:cache) ? Rails.cache : ActiveSupport::Cache::MemoryStore.new
118
+ load_connection_config(connection_config)
119
+ get_access_token(cache)
120
+ end
121
+
122
+ def load_connection_config(connection_config)
123
+ @client_id = connection_config[:client_id]
124
+ @client_secret = connection_config[:client_secret]
125
+ @realm_id = connection_config[:realm_id]
126
+ @environment = connection_config[:environment]
127
+ @refresh_token = connection_config[:refresh_token]
128
+ end
129
+
130
+ def get_access_token(cache)
131
+ cache_key = "intuit_quickbooks_#{@client_id}_#{@client_secret}_#{@realm_id}}"
132
+ cached_token = cache.read(cache_key)
133
+ if cached_token
134
+ cached_token
135
+ else
136
+ new_token = refresh_access_token
137
+ # max expiration is 3 minutes. No way to make it higher
138
+ cache.write(cache_key, new_token, expires_in: 180)
139
+ new_token
140
+ end
141
+ end
142
+
143
+ def refresh_access_token
144
+ oauth2_client = IntuitOAuth::Client.new(@client_id, @client_secret, QUICKBOOKS_REDIRECT_URL, @environment)
145
+ oauth2_client.token.refresh_tokens(@refresh_token).access_token
146
+ end
147
+
148
+ def create_streams(records)
149
+ group_by_table(records).map do |r|
150
+ Multiwoven::Integrations::Protocol::Stream.new(name: r[:table_name], action: StreamAction["fetch"], json_schema: convert_to_json_schema(r[:columns]))
151
+ end
152
+ end
153
+
154
+ def group_by_table(records)
155
+ records.filter_map do |table_name, rows|
156
+ if rows.is_a?(Array) && rows.all? { |row| row.is_a?(Hash) }
157
+ row_sample = rows.first || {}
158
+ columns = row_sample.map do |key, value|
159
+ {
160
+ column_name: key,
161
+ data_type: normalize_type(value),
162
+ is_nullable: rows.any? { |row| row[key].nil? }
163
+ }
164
+ end
165
+ { table_name: table_name, columns: columns }
166
+ end
167
+ end
168
+ end
169
+
170
+ def batched_query_for_quickbooks(query)
171
+ query = query.strip.chomp(";")
172
+ limit = query[/LIMIT\s+(\d+)/i, 1] || 1000
173
+ offset = query[/OFFSET\s+(\d+)/i, 1]
174
+
175
+ base_query = query.gsub(/LIMIT\s+\d+/i, "").gsub(/OFFSET\s+\d+/i, "").strip
176
+ {
177
+ base_query: base_query,
178
+ limit: limit.to_i,
179
+ offset: offset.to_i
180
+ }
181
+ end
182
+
183
+ def flatten_hash(hash, parent_key = "", result = {})
184
+ hash.each do |key, value|
185
+ full_key = parent_key.empty? ? key.to_s : "#{parent_key}.#{key}"
186
+
187
+ case value
188
+ when Hash
189
+ flatten_hash(value, full_key, result)
190
+ when Array
191
+ next
192
+ else
193
+ result[full_key] = value.is_a?(Integer) || value.is_a?(Float) ? value : value.to_s
194
+ end
195
+ end
196
+ result
197
+ end
198
+
199
+ def normalize_type(value)
200
+ case value
201
+ when Integer, Float then "NUMBER"
202
+ else "string"
203
+ end
204
+ end
205
+
206
+ def load_catalog
207
+ read_json(CATALOG_SPEC_PATH)
208
+ end
209
+
210
+ def build_url(url, query)
211
+ format(url, realm_id: @realm_id, query: query)
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,6 @@
1
+ {
2
+ "request_rate_limit": 500,
3
+ "request_rate_limit_unit": "minute",
4
+ "request_rate_concurrency": 10,
5
+ "streams": []
6
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "data": {
3
+ "name": "IntuitQuickBooks",
4
+ "title": "Intuit QuickBooks",
5
+ "connector_type": "source",
6
+ "category": "Data Warehouse",
7
+ "documentation_url": "https://docs.squared.ai/guides/sources/data-sources/intuit_quickbooks",
8
+ "github_issue_label": "source-intuit-quickbooks",
9
+ "icon": "icon.svg",
10
+ "license": "MIT",
11
+ "release_stage": "alpha",
12
+ "support_level": "community",
13
+ "tags": ["language:ruby", "multiwoven"]
14
+ }
15
+ }
16
+
@@ -0,0 +1,44 @@
1
+ {
2
+ "documentation_url": "https://docs.squared.ai/guides/sources/data-sources/intuit_quickbooks",
3
+ "stream_type": "dynamic",
4
+ "connector_query_type": "raw_sql",
5
+ "connection_specification": {
6
+ "$schema": "http://json-schema.org/draft-07/schema#",
7
+ "title": "Intuit QuickBooks",
8
+ "type": "object",
9
+ "required": ["environment","client_id","client_secret","realm_id","refresh_token"],
10
+ "properties": {
11
+ "environment": {
12
+ "type": "string",
13
+ "title": "Environment",
14
+ "enum": ["sandbox", "production"],
15
+ "order": 0
16
+ },
17
+ "client_id": {
18
+ "type": "string",
19
+ "multiwoven_secret": true,
20
+ "title": "Client Id",
21
+ "order": 1
22
+ },
23
+ "client_secret": {
24
+ "type": "string",
25
+ "multiwoven_secret": true,
26
+ "title": "Client Secret",
27
+ "order": 2
28
+ },
29
+ "realm_id": {
30
+ "type": "string",
31
+ "multiwoven_secret": true,
32
+ "title": "Realm Id",
33
+ "order": 3
34
+ },
35
+ "refresh_token": {
36
+ "type": "string",
37
+ "multiwoven_secret": true,
38
+ "title": "Refresh Token",
39
+ "order": 4
40
+ }
41
+ }
42
+ }
43
+ }
44
+
@@ -0,0 +1 @@
1
+ <svg enable-background="new 0 0 2500 2500" viewBox="0 0 2500 2500" xmlns="http://www.w3.org/2000/svg"><circle cx="1250" cy="1250" fill="#2ca01c" r="1250"/><path d="m301.3 1249.6c.1 282.6 228 512.4 510.6 514.9h72.3v-188.9h-72.3c-175.2 47.8-355.9-55.5-403.6-230.7-.4-1.4-.7-2.8-1.1-4.2-49.1-177.5 53.7-361.4 230.6-412.5h36.1c45.3-9.9 92.2-9.9 137.5 0h175.6v1002.9c-.9 106.1 84.4 192.9 190.5 193.9v-1395.4h-364.5c-284.6 1.5-514 233.4-512.5 518v.1zm1387.5-519.8h-72.3v198.9h72.3c174.8-47.7 355.1 55.3 402.8 230 .4 1.3.7 2.7 1.1 4 48.8 176.9-53.7 360.1-229.9 411.1h-36.1c-45.3 9.9-92.2 9.9-137.5 0h-175.6v-1002.8c.9-106.1-84.4-192.9-190.5-193.9v1397.4h364.5c287.1-4.5 516.2-240.8 511.8-527.9-4.4-280.8-230.9-507.4-511.8-511.8z" fill="#fff"/></svg>
@@ -39,6 +39,7 @@ require "grpc"
39
39
  require "MailchimpMarketing"
40
40
  require "aws-sdk-bedrockruntime"
41
41
  require "pinecone"
42
+ require "intuit-oauth"
42
43
 
43
44
  # Service
44
45
  require_relative "integrations/config"
@@ -83,6 +84,7 @@ require_relative "integrations/source/watsonx_data/client"
83
84
  require_relative "integrations/source/anthropic/client"
84
85
  require_relative "integrations/source/aws_bedrock_model/client"
85
86
  require_relative "integrations/source/generic_open_ai/client"
87
+ require_relative "integrations/source/intuit_quick_books/client"
86
88
 
87
89
  # Destination
88
90
  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.26.1
4
+ version: 0.27.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-05-20 00:00:00.000000000 Z
11
+ date: 2025-05-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -765,6 +765,11 @@ files:
765
765
  - lib/multiwoven/integrations/source/http_model/config/meta.json
766
766
  - lib/multiwoven/integrations/source/http_model/config/spec.json
767
767
  - lib/multiwoven/integrations/source/http_model/icon.svg
768
+ - lib/multiwoven/integrations/source/intuit_quick_books/client.rb
769
+ - lib/multiwoven/integrations/source/intuit_quick_books/config/catalog.json
770
+ - lib/multiwoven/integrations/source/intuit_quick_books/config/meta.json
771
+ - lib/multiwoven/integrations/source/intuit_quick_books/config/spec.json
772
+ - lib/multiwoven/integrations/source/intuit_quick_books/icon.svg
768
773
  - lib/multiwoven/integrations/source/maria_db/client.rb
769
774
  - lib/multiwoven/integrations/source/maria_db/config/meta.json
770
775
  - lib/multiwoven/integrations/source/maria_db/config/spec.json