multiwoven-integrations 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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