multiwoven-integrations 0.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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +34 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +5 -0
  6. data/CODE_OF_CONDUCT.md +84 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +43 -0
  9. data/Rakefile +12 -0
  10. data/lib/multiwoven/integrations/config.rb +13 -0
  11. data/lib/multiwoven/integrations/core/base_connector.rb +44 -0
  12. data/lib/multiwoven/integrations/core/constants.rb +34 -0
  13. data/lib/multiwoven/integrations/core/destination_connector.rb +12 -0
  14. data/lib/multiwoven/integrations/core/http_client.rb +34 -0
  15. data/lib/multiwoven/integrations/core/source_connector.rb +12 -0
  16. data/lib/multiwoven/integrations/core/utils.rb +68 -0
  17. data/lib/multiwoven/integrations/destination/klaviyo/client.rb +119 -0
  18. data/lib/multiwoven/integrations/destination/klaviyo/config/catalog.json +124 -0
  19. data/lib/multiwoven/integrations/destination/klaviyo/config/meta.json +15 -0
  20. data/lib/multiwoven/integrations/destination/klaviyo/config/spec.json +22 -0
  21. data/lib/multiwoven/integrations/protocol/protocol.json +189 -0
  22. data/lib/multiwoven/integrations/protocol/protocol.rb +179 -0
  23. data/lib/multiwoven/integrations/rollout.rb +17 -0
  24. data/lib/multiwoven/integrations/service.rb +57 -0
  25. data/lib/multiwoven/integrations/source/bigquery/client.rb +86 -0
  26. data/lib/multiwoven/integrations/source/bigquery/config/meta.json +15 -0
  27. data/lib/multiwoven/integrations/source/bigquery/config/spec.json +28 -0
  28. data/lib/multiwoven/integrations/source/redshift/client.rb +100 -0
  29. data/lib/multiwoven/integrations/source/redshift/config/meta.json +15 -0
  30. data/lib/multiwoven/integrations/source/redshift/config/spec.json +74 -0
  31. data/lib/multiwoven/integrations/source/snowflake/client.rb +84 -0
  32. data/lib/multiwoven/integrations/source/snowflake/config/meta.json +15 -0
  33. data/lib/multiwoven/integrations/source/snowflake/config/spec.json +87 -0
  34. data/lib/multiwoven/integrations.rb +41 -0
  35. data/multiwoven-integrations.gemspec +54 -0
  36. data/sig/multiwoven/integrations.rbs +6 -0
  37. metadata +291 -0
@@ -0,0 +1,124 @@
1
+ {
2
+ "streams": [
3
+ {
4
+ "name": "profile",
5
+ "action": "create",
6
+ "url": "https://a.klaviyo.com/api/profiles",
7
+ "method": "POST",
8
+ "json_schema": {
9
+ "type": "object",
10
+ "additionalProperties": true,
11
+ "properties": {
12
+ "type": {
13
+ "type": [
14
+ "null",
15
+ "string"
16
+ ]
17
+ },
18
+ "id": {
19
+ "type": "string"
20
+ },
21
+ "updated": {
22
+ "type": [
23
+ "null",
24
+ "string"
25
+ ],
26
+ "format": "date-time"
27
+ },
28
+ "attributes": {
29
+ "type": [
30
+ "null",
31
+ "object"
32
+ ],
33
+ "additionalProperties": true,
34
+ "properties": {
35
+ "email": {
36
+ "type": [
37
+ "null",
38
+ "string"
39
+ ]
40
+ },
41
+ "phone_number": {
42
+ "type": [
43
+ "null",
44
+ "string"
45
+ ]
46
+ },
47
+ "first_name": {
48
+ "type": [
49
+ "null",
50
+ "string"
51
+ ]
52
+ },
53
+ "last_name": {
54
+ "type": [
55
+ "null",
56
+ "string"
57
+ ]
58
+ },
59
+ "properties": {
60
+ "type": [
61
+ "null",
62
+ "object"
63
+ ],
64
+ "additionalProperties": true
65
+ },
66
+ "organization": {
67
+ "type": [
68
+ "null",
69
+ "string"
70
+ ]
71
+ },
72
+ "title": {
73
+ "type": [
74
+ "null",
75
+ "string"
76
+ ]
77
+ },
78
+ "last_event_date": {
79
+ "type": [
80
+ "null",
81
+ "string"
82
+ ],
83
+ "format": "date-time"
84
+ }
85
+ }
86
+ },
87
+ "links": {
88
+ "type": [
89
+ "null",
90
+ "object"
91
+ ]
92
+ },
93
+ "relationships": {
94
+ "type": [
95
+ "null",
96
+ "object"
97
+ ]
98
+ },
99
+ "segments": {
100
+ "type": [
101
+ "null",
102
+ "object"
103
+ ]
104
+ }
105
+ }
106
+ },
107
+ "supported_sync_modes": [
108
+ "full_refresh",
109
+ "incremental"
110
+ ],
111
+ "source_defined_cursor": true,
112
+ "default_cursor_field": [
113
+ "updated"
114
+ ],
115
+ "source_defined_primary_key": [
116
+ [
117
+ "id",
118
+ "email"
119
+ ]
120
+ ]
121
+
122
+ }
123
+ ]
124
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "data":
3
+ {
4
+ "name": "Klaviyo",
5
+ "connector_type": "destination",
6
+ "connector_subtype": "API",
7
+ "documentation_url": "https://docs.mutliwoven.com",
8
+ "github_issue_label": "destination-klaviyo",
9
+ "icon": "klaviyo.svg",
10
+ "license": "MIT",
11
+ "release_stage": "alpha",
12
+ "support_level": "community",
13
+ "tags": ["language:ruby", "multiwoven"]
14
+ }
15
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "documentation_url": "https://docs.multiwoven.com/integrations/destination/klaviyo",
3
+ "stream_type": "static",
4
+ "connection_specification": {
5
+ "$schema": "http://json-schema.org/draft-07/schema#",
6
+ "title": "Klaviyo Destination Spec",
7
+ "type": "object",
8
+ "required": ["public_api_key", "private_api_key"],
9
+ "properties": {
10
+ "public_api_keyhost": {
11
+ "type": "string",
12
+ "title": "Public API Key",
13
+ "order": 0
14
+ },
15
+ "private_api_key": {
16
+ "type": "string",
17
+ "title": "Private API Key",
18
+ "order": 1
19
+ }
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,189 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Mutliwoven Protocol",
4
+ "type": "object",
5
+ "description": "Mutliwoven protocol schema",
6
+ "version": "1.0.0",
7
+ "definitions": {
8
+ "SyncMode": {
9
+ "type": "string",
10
+ "enum": [
11
+ "full_refresh",
12
+ "incremental"
13
+ ]
14
+ },
15
+ "SyncStatus": {
16
+ "type": "string",
17
+ "enum": [
18
+ "STARTED",
19
+ "RUNNING",
20
+ "COMPLETE",
21
+ "INCOMPLETE"
22
+ ]
23
+ },
24
+ "DestinationSyncMode": {
25
+ "type": "string",
26
+ "enum": [
27
+ "append",
28
+ "overwrite",
29
+ "append_dedup"
30
+ ]
31
+ },
32
+ "ProtocolModel": {
33
+ "type": "object",
34
+ "additionalProperties": true
35
+ },
36
+ "ConnectionStatus": {
37
+ "type": "object",
38
+ "properties": {
39
+ "status": {
40
+ "$ref": "#/definitions/Types/String.enum(SUCCEEDED,FAILED)"
41
+ },
42
+ "message": {
43
+ "type": "string"
44
+ }
45
+ },
46
+ "additionalProperties": true
47
+ },
48
+ "ConnectorSpecification": {
49
+ "type": "object",
50
+ "properties": {
51
+ "documentation_url": {
52
+ "type": "string"
53
+ },
54
+ "changelog_url": {
55
+ "type": "string"
56
+ },
57
+ "connection_specification": {
58
+ "type": "object"
59
+ },
60
+ "supports_normalization": {
61
+ "type": "boolean",
62
+ "default": false
63
+ },
64
+ "supports_dbt": {
65
+ "type": "boolean",
66
+ "default": false
67
+ },
68
+ "supported_destination_sync_modes": {
69
+ "type": "array",
70
+ "items": {
71
+ "$ref": "#/definitions/DestinationSyncMode"
72
+ }
73
+ }
74
+ },
75
+ "additionalProperties": true
76
+ },
77
+ "LogMessage": {
78
+ "type": "object",
79
+ "properties": {
80
+ "level": {
81
+ "$ref": "#/definitions/Types/String.enum(FATAL,ERROR,WARN,INFO,DEBUG,TRACE)"
82
+ },
83
+ "message": {
84
+ "type": "string"
85
+ },
86
+ "stack_trace": {
87
+ "type": "string"
88
+ }
89
+ },
90
+ "additionalProperties": true
91
+ },
92
+ "RecordMessage": {
93
+ "type": "object",
94
+ "properties": {
95
+ "stream": {
96
+ "type": "string"
97
+ },
98
+ "data": {
99
+ "type": "object"
100
+ },
101
+ "emitted_at": {
102
+ "type": "integer"
103
+ }
104
+ },
105
+ "additionalProperties": true
106
+ },
107
+ "Stream": {
108
+ "type": "object",
109
+ "properties": {
110
+ "name": {
111
+ "type": "string"
112
+ },
113
+ "json_schema": {
114
+ "type": "object"
115
+ },
116
+ "supported_sync_modes": {
117
+ "type": "array",
118
+ "items": {
119
+ "$ref": "#/definitions/SyncMode"
120
+ }
121
+ },
122
+ "source_defined_cursor": {
123
+ "type": "boolean"
124
+ },
125
+ "default_cursor_field": {
126
+ "type": "array",
127
+ "items": {
128
+ "type": "string"
129
+ }
130
+ },
131
+ "source_defined_primary_key": {
132
+ "type": "array",
133
+ "items": {
134
+ "type": "array",
135
+ "items": {
136
+ "type": "string"
137
+ }
138
+ }
139
+ },
140
+ "namespace": {
141
+ "type": "string"
142
+ }
143
+ },
144
+ "additionalProperties": true
145
+ },
146
+ "Catalog": {
147
+ "type": "object",
148
+ "properties": {
149
+ "streams": {
150
+ "type": "array",
151
+ "items": {
152
+ "$ref": "#/definitions/Stream"
153
+ }
154
+ }
155
+ },
156
+ "additionalProperties": true
157
+ },
158
+ "SyncConfig": {
159
+ "type": "object",
160
+ "properties": {
161
+ "stream": {
162
+ "$ref": "#/definitions/Stream"
163
+ },
164
+ "sync_mode": {
165
+ "$ref": "#/definitions/SyncMode"
166
+ },
167
+ "cursor_field": {
168
+ "type": "array",
169
+ "items": {
170
+ "type": "string"
171
+ }
172
+ },
173
+ "destination_sync_mode": {
174
+ "$ref": "#/definitions/DestinationSyncMode"
175
+ },
176
+ "primary_key": {
177
+ "type": "array",
178
+ "items": {
179
+ "type": "array",
180
+ "items": {
181
+ "type": "string"
182
+ }
183
+ }
184
+ }
185
+ },
186
+ "additionalProperties": true
187
+ }
188
+ }
189
+ }
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Multiwoven
4
+ module Integrations::Protocol
5
+ module Types
6
+ include Dry.Types()
7
+ end
8
+
9
+ SyncMode = Types::String.enum("full_refresh", "incremental")
10
+ SyncStatus = Types::String.enum("started", "running", "complete", "incomplete")
11
+ DestinationSyncMode = Types::String.enum("insert", "upsert")
12
+ ConnectorType = Types::String.enum("source", "destination")
13
+ ModelQueryType = Types::String.enum("raw_sql", "dbt")
14
+ ConnectionStatusType = Types::String.enum("succeeded", "failed")
15
+ StreamType = Types::String.enum("static", "dynamic")
16
+ StreamAction = Types::String.enum("fetch", "create", "update", "delete")
17
+ MultiwovenMessageType = Types::String.enum(
18
+ "record", "log", "connector_spec",
19
+ "connection_status", "catalog", "control",
20
+ "tracking"
21
+ )
22
+ ControlMessageType = Types::String.enum(
23
+ "rate_limit", "connection_config"
24
+ )
25
+ LogLevel = Types::String.enum("fatal", "error", "warn", "info", "debug", "trace")
26
+
27
+ class ProtocolModel < Dry::Struct
28
+ extend Multiwoven::Integrations::Core::Utils
29
+ class << self
30
+ def from_json(json_string)
31
+ data = JSON.parse(json_string)
32
+ new(keys_to_symbols(data))
33
+ end
34
+ end
35
+ end
36
+
37
+ class ConnectionStatus < ProtocolModel
38
+ attribute :status, ConnectionStatusType
39
+ attribute? :message, Types::String.optional
40
+
41
+ def to_multiwoven_message
42
+ MultiwovenMessage.new(
43
+ type: MultiwovenMessageType["connection_status"],
44
+ connection_status: self
45
+ )
46
+ end
47
+ end
48
+
49
+ class ConnectorSpecification < ProtocolModel
50
+ attribute? :documentation_url, Types::String.optional
51
+ attribute? :changelog_url, Types::String.optional
52
+ attribute :connection_specification, Types::Hash
53
+ attribute :supports_normalization, Types::Bool.default(false)
54
+ attribute :supports_dbt, Types::Bool.default(false)
55
+ attribute :stream_type, StreamType
56
+ attribute? :supported_destination_sync_modes, Types::Array.of(DestinationSyncMode).optional
57
+
58
+ def to_multiwoven_message
59
+ MultiwovenMessage.new(
60
+ type: MultiwovenMessageType["connector_spec"],
61
+ connector_spec: self
62
+ )
63
+ end
64
+ end
65
+
66
+ class Connector < ProtocolModel
67
+ attribute :name, Types::String
68
+ attribute :type, ConnectorType
69
+ attribute :connection_specification, Types::Hash
70
+ end
71
+
72
+ class LogMessage < ProtocolModel
73
+ attribute :level, LogLevel
74
+ attribute :message, Types::String
75
+ attribute? :name, Types::String.optional
76
+ attribute? :stack_trace, Types::String.optional
77
+
78
+ def to_multiwoven_message
79
+ MultiwovenMessage.new(
80
+ type: MultiwovenMessageType["log"],
81
+ log: self
82
+ )
83
+ end
84
+ end
85
+
86
+ class Model < ProtocolModel
87
+ attribute? :name, Types::String.optional
88
+ attribute :query, Types::String
89
+ attribute :query_type, ModelQueryType
90
+ attribute :primary_key, Types::String
91
+ end
92
+
93
+ class RecordMessage < ProtocolModel
94
+ attribute :data, Types::Hash
95
+ attribute :emitted_at, Types::Integer
96
+
97
+ def to_multiwoven_message
98
+ MultiwovenMessage.new(
99
+ type: MultiwovenMessageType["record"],
100
+ record: self
101
+ )
102
+ end
103
+ end
104
+
105
+ class Stream < ProtocolModel
106
+ # Common
107
+ attribute :name, Types::String
108
+ attribute? :action, StreamAction
109
+ attribute :json_schema, Types::Hash
110
+ attribute? :supported_sync_modes, Types::Array.of(SyncMode).optional
111
+
112
+ attribute? :source_defined_cursor, Types::Bool.optional
113
+ attribute? :default_cursor_field, Types::Array.of(Types::String).optional
114
+ attribute? :source_defined_primary_key, Types::Array.of(Types::Array.of(Types::String)).optional
115
+ attribute? :namespace, Types::String.optional
116
+ # Applicable for API streams
117
+ attribute? :url, Types::String.optional
118
+ attribute? :request_method, Types::String.optional
119
+ end
120
+
121
+ class Catalog < ProtocolModel
122
+ attribute :streams, Types::Array.of(Stream)
123
+
124
+ def to_multiwoven_message
125
+ MultiwovenMessage.new(
126
+ type: MultiwovenMessageType["catalog"],
127
+ catalog: self
128
+ )
129
+ end
130
+ end
131
+
132
+ class SyncConfig < ProtocolModel
133
+ attribute :source, Connector
134
+ attribute :destination, Connector
135
+ attribute :model, Model
136
+ attribute :stream, Stream
137
+ attribute :sync_mode, SyncMode
138
+ attribute? :cursor_field, Types::String.optional
139
+ attribute :destination_sync_mode, DestinationSyncMode
140
+ end
141
+
142
+ class ControlMessage < ProtocolModel
143
+ attribute :type, ControlMessageType
144
+ attribute :emitted_at, Types::Integer
145
+ attribute? :meta, Types::Hash
146
+
147
+ def to_multiwoven_message
148
+ MultiwovenMessage.new(
149
+ type: MultiwovenMessageType["control"],
150
+ control: self
151
+ )
152
+ end
153
+ end
154
+
155
+ class TrackingMessage < ProtocolModel
156
+ attribute :success, Types::Integer.default(0)
157
+ attribute :failed, Types::Integer.default(0)
158
+ attribute? :meta, Types::Hash
159
+
160
+ def to_multiwoven_message
161
+ MultiwovenMessage.new(
162
+ type: MultiwovenMessageType["tracking"],
163
+ tracking: self
164
+ )
165
+ end
166
+ end
167
+
168
+ class MultiwovenMessage < ProtocolModel
169
+ attribute :type, MultiwovenMessageType
170
+ attribute? :log, LogMessage.optional
171
+ attribute? :connection_status, ConnectionStatus.optional
172
+ attribute? :connector_spec, ConnectorSpecification.optional
173
+ attribute? :catalog, Catalog.optional
174
+ attribute? :record, RecordMessage.optional
175
+ attribute? :control, ControlMessage.optional
176
+ attribute? :tracking, TrackingMessage.optional
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Multiwoven
4
+ module Integrations
5
+ VERSION = "0.1.0"
6
+
7
+ ENABLED_SOURCES = %w[
8
+ Snowflake
9
+ Redshift
10
+ Bigquery
11
+ ].freeze
12
+
13
+ ENABLED_DESTINATIONS = %w[
14
+ Klaviyo
15
+ ].freeze
16
+ end
17
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Multiwoven
4
+ module Integrations
5
+ class Service
6
+ class << self
7
+ def initialize
8
+ yield(config) if block_given?
9
+ end
10
+
11
+ def connectors
12
+ {
13
+ source: build_connectors(
14
+ ENABLED_SOURCES, "Source"
15
+ ),
16
+ destination: build_connectors(
17
+ ENABLED_DESTINATIONS, "Destination"
18
+ )
19
+ }
20
+ end
21
+
22
+ def connector_class(connector_type, connector_name)
23
+ Object.const_get(
24
+ "Multiwoven::Integrations::#{connector_type}::#{connector_name}::Client"
25
+ )
26
+ end
27
+
28
+ def logger
29
+ config.logger || default_logger
30
+ end
31
+
32
+ def config
33
+ @config ||= Config.new
34
+ end
35
+
36
+ private
37
+
38
+ def build_connectors(enabled_connectors, type)
39
+ enabled_connectors.map do |connector|
40
+ client = connector_class(
41
+ type, connector
42
+ ).new
43
+ client.meta_data["data"].to_h.merge(
44
+ {
45
+ connector_spec: client.connector_spec.to_h
46
+ }
47
+ )
48
+ end
49
+ end
50
+
51
+ def default_logger
52
+ @default_logger ||= Logger.new($stdout)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "google/cloud/bigquery"
4
+
5
+ module Multiwoven::Integrations::Source
6
+ module Bigquery
7
+ include Multiwoven::Integrations::Core
8
+ class Client < SourceConnector
9
+ def check_connection(connection_config)
10
+ bigquery = create_connection(connection_config)
11
+ bigquery.datasets
12
+ ConnectionStatus.new(status: ConnectionStatusType["succeeded"]).to_multiwoven_message
13
+ rescue StandardError => e
14
+ ConnectionStatus.new(status: ConnectionStatusType["failed"], message: e.message).to_multiwoven_message
15
+ end
16
+
17
+ def discover(connection_config)
18
+ bigquery = create_connection(connection_config)
19
+ target_dataset_id = connection_config["dataset_id"]
20
+ records = bigquery.datasets.flat_map do |dataset|
21
+ next unless dataset.dataset_id == target_dataset_id
22
+
23
+ dataset.tables.flat_map do |table|
24
+ table.schema.fields.map do |field|
25
+ {
26
+ table_name: table.table_id,
27
+ column_name: field.name,
28
+ data_type: field.type,
29
+ is_nullable: field.mode == "NULLABLE"
30
+ }
31
+ end
32
+ end
33
+ end
34
+ catalog = Catalog.new(streams: create_streams(records))
35
+ catalog.to_multiwoven_message
36
+ rescue StandardError => e
37
+ handle_exception(
38
+ "BIGQUERY:DISCOVER:EXCEPTION",
39
+ "error",
40
+ e
41
+ )
42
+ end
43
+
44
+ def read(sync_config)
45
+ connection_config = sync_config.source.connection_specification
46
+ query = sync_config.model.query
47
+ bigquery = create_connection(connection_config)
48
+ records = []
49
+ results = bigquery.query query
50
+ results.each do |row|
51
+ records << RecordMessage.new(data: row, emitted_at: Time.now.to_i)
52
+ end
53
+
54
+ records
55
+ rescue StandardError => e
56
+ handle_exception(
57
+ "BIGQUERY:READ:EXCEPTION",
58
+ "error",
59
+ e
60
+ )
61
+ end
62
+
63
+ def create_connection(connection_config)
64
+ Google::Cloud::Bigquery.new(
65
+ project: connection_config["project_id"],
66
+ credentials: connection_config["credentials_json"]
67
+ )
68
+ end
69
+
70
+ def create_streams(records)
71
+ group_by_table(records).map do |r|
72
+ Multiwoven::Integrations::Protocol::Stream.new(name: r[:tablename], action: StreamAction["fetch"], json_schema: convert_to_json_schema(r[:columns]))
73
+ end
74
+ end
75
+
76
+ def group_by_table(records)
77
+ records.group_by { |entry| entry[:table_name] }.map do |table_name, columns|
78
+ {
79
+ tablename: table_name,
80
+ columns: columns.map { |column| { column_name: column[:column_name], type: column[:data_type], optional: column[:is_nullable] == "YES" } }
81
+ }
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end