airbyte-source-iterable 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
- {airbyte_source_iterable-0.4.0.dist-info → airbyte_source_iterable-0.5.0.dist-info}/METADATA +1 -1
- {airbyte_source_iterable-0.4.0.dist-info → airbyte_source_iterable-0.5.0.dist-info}/RECORD +10 -8
- source_iterable/components.py +41 -0
- source_iterable/manifest.yaml +440 -0
- source_iterable/source.py +54 -96
- source_iterable/spec.json +2 -3
- source_iterable/streams.py +0 -123
- source_iterable/utils.py +0 -7
- {airbyte_source_iterable-0.4.0.dist-info → airbyte_source_iterable-0.5.0.dist-info}/WHEEL +0 -0
- {airbyte_source_iterable-0.4.0.dist-info → airbyte_source_iterable-0.5.0.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,6 @@
|
|
1
1
|
source_iterable/__init__.py,sha256=8WKQT800ggxG1vGPoA_ZHUkozwEM7qMhGMhzEPCdA4o,65
|
2
|
+
source_iterable/components.py,sha256=dHp23THc43TmogP_tCxcv12mAgZSRs5fKir2ckr4hHI,1374
|
3
|
+
source_iterable/manifest.yaml,sha256=uO7z-bIU6CtGms6IsLwjyZ-cb3tVY-vLUMLOZGkZDts,13996
|
2
4
|
source_iterable/run.py,sha256=-Bvz772Z7iJWExDoJS7chxv725hG4e9e5Engrtp66iE,236
|
3
5
|
source_iterable/schemas/campaigns.json,sha256=3sDq9ekGVuIoAb0kXhul-5sbaz1Vqm36MnLr0cNfPpY,1191
|
4
6
|
source_iterable/schemas/campaigns_metrics.json,sha256=huOj1N5iXi-jvM0ztAZjf2mUIfd2KeWKSwQ7pNmsx6g,109
|
@@ -19,11 +21,11 @@ source_iterable/schemas/metadata.json,sha256=WP_wq49tAsvetXEtQ3KeGxVpqTYCbLjZacN
|
|
19
21
|
source_iterable/schemas/templates.json,sha256=dC7lKUnG3mQe9-Wj-jd7OyYruQEWvYT8DHfW9oDLXT4,569
|
20
22
|
source_iterable/schemas/users.json,sha256=WBl7sJBKU3r_BqGF5Nzv0C-rbHj0DoI-PRHopxA7s2k,7490
|
21
23
|
source_iterable/slice_generators.py,sha256=neRuWD52Xnr84KvWQ0kjaErR44sOKjxIyLCNeofohzE,6482
|
22
|
-
source_iterable/source.py,sha256=
|
23
|
-
source_iterable/spec.json,sha256=
|
24
|
-
source_iterable/streams.py,sha256=
|
25
|
-
source_iterable/utils.py,sha256=
|
26
|
-
airbyte_source_iterable-0.
|
27
|
-
airbyte_source_iterable-0.
|
28
|
-
airbyte_source_iterable-0.
|
29
|
-
airbyte_source_iterable-0.
|
24
|
+
source_iterable/source.py,sha256=AhZQ4L54VTo6jf2QltvFCqDNgqaPSEKCfR0GInCLEEc,4537
|
25
|
+
source_iterable/spec.json,sha256=y5_YFTkPPtxjvzGtaZZql90Tc37fyZZpQKXFGPL_cYE,1009
|
26
|
+
source_iterable/streams.py,sha256=ZEC8IqBrTeD2oYOvLL51E62iOsIOK3jiTN7pLI-CwRs,19468
|
27
|
+
source_iterable/utils.py,sha256=2oM8AjBZXs9nTG_PhSWmxBWEmp-w-tWFaxQQvAfUuMM,649
|
28
|
+
airbyte_source_iterable-0.5.0.dist-info/METADATA,sha256=6K8Owfuh9l-DQAw969Ex5-OalA5t6bkod1ttmDe2qqE,5350
|
29
|
+
airbyte_source_iterable-0.5.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
30
|
+
airbyte_source_iterable-0.5.0.dist-info/entry_points.txt,sha256=pjzrNtsOX3jW37IK0U5pSmLiraiuLTPr5aB5-hBXpAo,59
|
31
|
+
airbyte_source_iterable-0.5.0.dist-info/RECORD,,
|
@@ -0,0 +1,41 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
|
3
|
+
#
|
4
|
+
|
5
|
+
import json
|
6
|
+
from dataclasses import dataclass
|
7
|
+
from io import StringIO
|
8
|
+
|
9
|
+
import requests
|
10
|
+
from airbyte_cdk.sources.declarative.extractors.dpath_extractor import DpathExtractor
|
11
|
+
from airbyte_cdk.sources.declarative.types import Config, Record, StreamSlice, StreamState
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass
|
15
|
+
class XJsonRecordExtractor(DpathExtractor):
|
16
|
+
def extract_records(self, response: requests.Response) -> list[Record]:
|
17
|
+
return [json.loads(record) for record in response.iter_lines()]
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class ListUsersRecordExtractor(DpathExtractor):
|
22
|
+
def extract_records(self, response: requests.Response) -> list[Record]:
|
23
|
+
return [{"email": record.decode()} for record in response.iter_lines()]
|
24
|
+
|
25
|
+
|
26
|
+
@dataclass
|
27
|
+
class EventsRecordExtractor(DpathExtractor):
|
28
|
+
common_fields = ("itblInternal", "_type", "createdAt", "email")
|
29
|
+
|
30
|
+
def extract_records(self, response: requests.Response) -> list[Record]:
|
31
|
+
jsonl_records = StringIO(response.text)
|
32
|
+
records = []
|
33
|
+
for record in jsonl_records:
|
34
|
+
record_dict = json.loads(record)
|
35
|
+
record_dict_common_fields = {}
|
36
|
+
for field in self.common_fields:
|
37
|
+
record_dict_common_fields[field] = record_dict.pop(field, None)
|
38
|
+
|
39
|
+
records.append({**record_dict_common_fields, "data": record_dict})
|
40
|
+
|
41
|
+
return records
|
@@ -0,0 +1,440 @@
|
|
1
|
+
spec:
|
2
|
+
type: Spec
|
3
|
+
connection_specification:
|
4
|
+
$schema: http://json-schema.org/draft-07/schema#
|
5
|
+
title: Iterable Spec
|
6
|
+
type: object
|
7
|
+
required:
|
8
|
+
- start_date
|
9
|
+
- api_key
|
10
|
+
additionalProperties: true
|
11
|
+
properties:
|
12
|
+
api_key:
|
13
|
+
type: "string"
|
14
|
+
title: "API Key"
|
15
|
+
description: >-
|
16
|
+
Iterable API Key. See the <a href=\"https://docs.airbyte.com/integrations/sources/iterable\">docs</a>
|
17
|
+
for more information on how to obtain this key.
|
18
|
+
airbyte_secret: true
|
19
|
+
order: 0
|
20
|
+
start_date:
|
21
|
+
type: "string"
|
22
|
+
title: "Start Date"
|
23
|
+
description: >-
|
24
|
+
The date from which you'd like to replicate data for Iterable, in the format YYYY-MM-DDT00:00:00Z.
|
25
|
+
All data generated after this date will be replicated.
|
26
|
+
examples: ["2021-04-01T00:00:00Z"]
|
27
|
+
pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$
|
28
|
+
order: 1
|
29
|
+
format: "date-time"
|
30
|
+
type: DeclarativeSource
|
31
|
+
check:
|
32
|
+
type: CheckStream
|
33
|
+
stream_names:
|
34
|
+
- lists
|
35
|
+
streams:
|
36
|
+
- name: lists
|
37
|
+
type: DeclarativeStream
|
38
|
+
retriever:
|
39
|
+
type: SimpleRetriever
|
40
|
+
paginator:
|
41
|
+
type: NoPagination
|
42
|
+
requester:
|
43
|
+
path: lists
|
44
|
+
type: HttpRequester
|
45
|
+
url_base: https://api.iterable.com/api/
|
46
|
+
http_method: GET
|
47
|
+
authenticator:
|
48
|
+
type: ApiKeyAuthenticator
|
49
|
+
api_token: "{{ config['api_key'] }}"
|
50
|
+
inject_into:
|
51
|
+
type: RequestOption
|
52
|
+
field_name: Api-Key
|
53
|
+
inject_into: header
|
54
|
+
request_headers: {}
|
55
|
+
request_body_json: {}
|
56
|
+
request_parameters: {}
|
57
|
+
record_selector:
|
58
|
+
type: RecordSelector
|
59
|
+
extractor:
|
60
|
+
type: DpathExtractor
|
61
|
+
field_path:
|
62
|
+
- lists
|
63
|
+
partition_router: []
|
64
|
+
primary_key:
|
65
|
+
- id
|
66
|
+
- name: list_users
|
67
|
+
type: DeclarativeStream
|
68
|
+
retriever:
|
69
|
+
type: SimpleRetriever
|
70
|
+
paginator:
|
71
|
+
type: NoPagination
|
72
|
+
requester:
|
73
|
+
path: lists/getUsers
|
74
|
+
type: HttpRequester
|
75
|
+
url_base: https://api.iterable.com/api/
|
76
|
+
http_method: GET
|
77
|
+
authenticator:
|
78
|
+
type: ApiKeyAuthenticator
|
79
|
+
api_token: "{{ config['api_key'] }}"
|
80
|
+
inject_into:
|
81
|
+
type: RequestOption
|
82
|
+
field_name: Api-Key
|
83
|
+
inject_into: header
|
84
|
+
request_headers: {}
|
85
|
+
request_body_json: {}
|
86
|
+
request_parameters: {}
|
87
|
+
record_selector:
|
88
|
+
type: RecordSelector
|
89
|
+
extractor:
|
90
|
+
class_name: source_iterable.components.ListUsersRecordExtractor
|
91
|
+
field_path:
|
92
|
+
- getUsers
|
93
|
+
partition_router:
|
94
|
+
- type: SubstreamPartitionRouter
|
95
|
+
parent_stream_configs:
|
96
|
+
- type: ParentStreamConfig
|
97
|
+
parent_key: id
|
98
|
+
request_option:
|
99
|
+
inject_into: request_parameter
|
100
|
+
type: RequestOption
|
101
|
+
field_name: listId
|
102
|
+
partition_field: list_id
|
103
|
+
stream:
|
104
|
+
type: DeclarativeStream
|
105
|
+
name: lists
|
106
|
+
primary_key:
|
107
|
+
- id
|
108
|
+
retriever:
|
109
|
+
type: SimpleRetriever
|
110
|
+
requester:
|
111
|
+
type: HttpRequester
|
112
|
+
url_base: https://api.iterable.com/api/
|
113
|
+
path: lists
|
114
|
+
http_method: GET
|
115
|
+
request_parameters: {}
|
116
|
+
request_headers: {}
|
117
|
+
authenticator:
|
118
|
+
type: ApiKeyAuthenticator
|
119
|
+
api_token: "{{ config['api_key'] }}"
|
120
|
+
inject_into:
|
121
|
+
type: RequestOption
|
122
|
+
field_name: Api-Key
|
123
|
+
inject_into: header
|
124
|
+
request_body_json: {}
|
125
|
+
record_selector:
|
126
|
+
type: RecordSelector
|
127
|
+
extractor:
|
128
|
+
type: DpathExtractor
|
129
|
+
field_path:
|
130
|
+
- lists
|
131
|
+
paginator:
|
132
|
+
type: NoPagination
|
133
|
+
partition_router: []
|
134
|
+
primary_key:
|
135
|
+
- listId
|
136
|
+
transformations:
|
137
|
+
- type: AddFields
|
138
|
+
fields:
|
139
|
+
- path:
|
140
|
+
- listId
|
141
|
+
value: "{{ stream_slice.list_id }}"
|
142
|
+
- name: campaigns
|
143
|
+
type: DeclarativeStream
|
144
|
+
retriever:
|
145
|
+
type: SimpleRetriever
|
146
|
+
paginator:
|
147
|
+
type: NoPagination
|
148
|
+
requester:
|
149
|
+
path: campaigns
|
150
|
+
type: HttpRequester
|
151
|
+
url_base: https://api.iterable.com/api/
|
152
|
+
http_method: GET
|
153
|
+
authenticator:
|
154
|
+
type: ApiKeyAuthenticator
|
155
|
+
api_token: "{{ config['api_key'] }}"
|
156
|
+
inject_into:
|
157
|
+
type: RequestOption
|
158
|
+
field_name: Api-Key
|
159
|
+
inject_into: header
|
160
|
+
request_headers: {}
|
161
|
+
request_body_json: {}
|
162
|
+
request_parameters: {}
|
163
|
+
record_selector:
|
164
|
+
type: RecordSelector
|
165
|
+
extractor:
|
166
|
+
type: DpathExtractor
|
167
|
+
field_path:
|
168
|
+
- campaigns
|
169
|
+
partition_router: []
|
170
|
+
primary_key:
|
171
|
+
- id
|
172
|
+
- name: channels
|
173
|
+
type: DeclarativeStream
|
174
|
+
retriever:
|
175
|
+
type: SimpleRetriever
|
176
|
+
paginator:
|
177
|
+
type: NoPagination
|
178
|
+
requester:
|
179
|
+
path: channels
|
180
|
+
type: HttpRequester
|
181
|
+
url_base: https://api.iterable.com/api/
|
182
|
+
http_method: GET
|
183
|
+
authenticator:
|
184
|
+
type: ApiKeyAuthenticator
|
185
|
+
api_token: "{{ config['api_key'] }}"
|
186
|
+
inject_into:
|
187
|
+
type: RequestOption
|
188
|
+
field_name: Api-Key
|
189
|
+
inject_into: header
|
190
|
+
request_headers: {}
|
191
|
+
request_body_json: {}
|
192
|
+
request_parameters: {}
|
193
|
+
record_selector:
|
194
|
+
type: RecordSelector
|
195
|
+
extractor:
|
196
|
+
type: DpathExtractor
|
197
|
+
field_path:
|
198
|
+
- channels
|
199
|
+
partition_router: []
|
200
|
+
primary_key:
|
201
|
+
- id
|
202
|
+
- name: message_types
|
203
|
+
type: DeclarativeStream
|
204
|
+
retriever:
|
205
|
+
type: SimpleRetriever
|
206
|
+
paginator:
|
207
|
+
type: NoPagination
|
208
|
+
requester:
|
209
|
+
path: messageTypes
|
210
|
+
type: HttpRequester
|
211
|
+
url_base: https://api.iterable.com/api/
|
212
|
+
http_method: GET
|
213
|
+
authenticator:
|
214
|
+
type: ApiKeyAuthenticator
|
215
|
+
api_token: "{{ config['api_key'] }}"
|
216
|
+
inject_into:
|
217
|
+
type: RequestOption
|
218
|
+
field_name: Api-Key
|
219
|
+
inject_into: header
|
220
|
+
request_headers: {}
|
221
|
+
request_body_json: {}
|
222
|
+
request_parameters: {}
|
223
|
+
record_selector:
|
224
|
+
type: RecordSelector
|
225
|
+
extractor:
|
226
|
+
type: DpathExtractor
|
227
|
+
field_path:
|
228
|
+
- messageTypes
|
229
|
+
partition_router: []
|
230
|
+
primary_key:
|
231
|
+
- id
|
232
|
+
- name: metadata
|
233
|
+
type: DeclarativeStream
|
234
|
+
retriever:
|
235
|
+
type: SimpleRetriever
|
236
|
+
paginator:
|
237
|
+
type: NoPagination
|
238
|
+
requester:
|
239
|
+
path: metadata
|
240
|
+
type: HttpRequester
|
241
|
+
url_base: https://api.iterable.com/api/
|
242
|
+
http_method: GET
|
243
|
+
authenticator:
|
244
|
+
type: ApiKeyAuthenticator
|
245
|
+
api_token: "{{ config['api_key'] }}"
|
246
|
+
inject_into:
|
247
|
+
type: RequestOption
|
248
|
+
field_name: Api-Key
|
249
|
+
inject_into: header
|
250
|
+
request_headers: {}
|
251
|
+
request_body_json: {}
|
252
|
+
request_parameters: {}
|
253
|
+
record_selector:
|
254
|
+
type: RecordSelector
|
255
|
+
extractor:
|
256
|
+
type: DpathExtractor
|
257
|
+
field_path:
|
258
|
+
- results
|
259
|
+
partition_router: []
|
260
|
+
primary_key: []
|
261
|
+
- name: users
|
262
|
+
type: DeclarativeStream
|
263
|
+
retriever:
|
264
|
+
type: SimpleRetriever
|
265
|
+
paginator:
|
266
|
+
type: NoPagination
|
267
|
+
requester:
|
268
|
+
path: export/data.json
|
269
|
+
type: HttpRequester
|
270
|
+
url_base: https://api.iterable.com/api/
|
271
|
+
http_method: GET
|
272
|
+
authenticator:
|
273
|
+
type: ApiKeyAuthenticator
|
274
|
+
api_token: "{{ config['api_key'] }}"
|
275
|
+
inject_into:
|
276
|
+
type: RequestOption
|
277
|
+
field_name: Api-Key
|
278
|
+
inject_into: header
|
279
|
+
request_headers: {}
|
280
|
+
request_body_json: {}
|
281
|
+
request_parameters:
|
282
|
+
stream: "True"
|
283
|
+
dataTypeName: user
|
284
|
+
record_selector:
|
285
|
+
type: RecordSelector
|
286
|
+
extractor:
|
287
|
+
class_name: source_iterable.components.XJsonRecordExtractor
|
288
|
+
field_path:
|
289
|
+
- users
|
290
|
+
partition_router: []
|
291
|
+
primary_key: []
|
292
|
+
incremental_sync:
|
293
|
+
step: P90D
|
294
|
+
type: DatetimeBasedCursor
|
295
|
+
cursor_field: profileUpdatedAt
|
296
|
+
end_datetime:
|
297
|
+
type: MinMaxDatetime
|
298
|
+
datetime: "{{ config['end_date'] if config['end_date'] else now_utc().strftime('%Y-%m-%dT%H:%M:%SZ') }}"
|
299
|
+
datetime_format: "%Y-%m-%dT%H:%M:%SZ"
|
300
|
+
start_datetime:
|
301
|
+
type: MinMaxDatetime
|
302
|
+
datetime: "{{ config['start_date'] }}"
|
303
|
+
datetime_format: "%Y-%m-%dT%H:%M:%SZ"
|
304
|
+
datetime_format: "%Y-%m-%d %H:%M:%S"
|
305
|
+
end_time_option:
|
306
|
+
type: RequestOption
|
307
|
+
field_name: endDateTime
|
308
|
+
inject_into: request_parameter
|
309
|
+
start_time_option:
|
310
|
+
type: RequestOption
|
311
|
+
field_name: startDateTime
|
312
|
+
inject_into: request_parameter
|
313
|
+
cursor_granularity: PT1S
|
314
|
+
cursor_datetime_formats:
|
315
|
+
- "%Y-%m-%d %H:%M:%S %z"
|
316
|
+
- "%Y-%m-%dT%H:%M:%S%z"
|
317
|
+
- name: events
|
318
|
+
primary_key: []
|
319
|
+
retriever:
|
320
|
+
type: SimpleRetriever
|
321
|
+
requester:
|
322
|
+
type: HttpRequester
|
323
|
+
url_base: https://api.iterable.com/api/
|
324
|
+
path: export/userEvents
|
325
|
+
http_method: GET
|
326
|
+
request_parameters:
|
327
|
+
includeCustomEvents: "true"
|
328
|
+
request_headers: {}
|
329
|
+
authenticator:
|
330
|
+
type: ApiKeyAuthenticator
|
331
|
+
api_token: "{{ config['api_key'] }}"
|
332
|
+
inject_into:
|
333
|
+
type: RequestOption
|
334
|
+
field_name: Api-Key
|
335
|
+
inject_into: header
|
336
|
+
request_body_json: {}
|
337
|
+
record_selector:
|
338
|
+
type: RecordSelector
|
339
|
+
extractor:
|
340
|
+
class_name: source_iterable.components.EventsRecordExtractor
|
341
|
+
field_path:
|
342
|
+
- events
|
343
|
+
paginator:
|
344
|
+
type: NoPagination
|
345
|
+
partition_router:
|
346
|
+
- type: SubstreamPartitionRouter
|
347
|
+
parent_stream_configs:
|
348
|
+
- type: ParentStreamConfig
|
349
|
+
parent_key: email
|
350
|
+
request_option:
|
351
|
+
inject_into: request_parameter
|
352
|
+
type: RequestOption
|
353
|
+
field_name: email
|
354
|
+
partition_field: email
|
355
|
+
stream:
|
356
|
+
name: list_users
|
357
|
+
type: DeclarativeStream
|
358
|
+
retriever:
|
359
|
+
type: SimpleRetriever
|
360
|
+
paginator:
|
361
|
+
type: NoPagination
|
362
|
+
requester:
|
363
|
+
path: lists/getUsers
|
364
|
+
type: HttpRequester
|
365
|
+
url_base: https://api.iterable.com/api/
|
366
|
+
http_method: GET
|
367
|
+
authenticator:
|
368
|
+
type: ApiKeyAuthenticator
|
369
|
+
api_token: "{{ config['api_key'] }}"
|
370
|
+
inject_into:
|
371
|
+
type: RequestOption
|
372
|
+
field_name: Api-Key
|
373
|
+
inject_into: header
|
374
|
+
request_headers: {}
|
375
|
+
request_body_json: {}
|
376
|
+
request_parameters: {}
|
377
|
+
record_selector:
|
378
|
+
type: RecordSelector
|
379
|
+
extractor:
|
380
|
+
class_name: source_iterable.components.ListUsersRecordExtractor
|
381
|
+
field_path:
|
382
|
+
- getUsers
|
383
|
+
partition_router:
|
384
|
+
- type: SubstreamPartitionRouter
|
385
|
+
parent_stream_configs:
|
386
|
+
- type: ParentStreamConfig
|
387
|
+
parent_key: id
|
388
|
+
request_option:
|
389
|
+
inject_into: request_parameter
|
390
|
+
type: RequestOption
|
391
|
+
field_name: listId
|
392
|
+
partition_field: list_id
|
393
|
+
stream:
|
394
|
+
type: DeclarativeStream
|
395
|
+
name: lists
|
396
|
+
primary_key:
|
397
|
+
- id
|
398
|
+
retriever:
|
399
|
+
type: SimpleRetriever
|
400
|
+
requester:
|
401
|
+
type: HttpRequester
|
402
|
+
url_base: https://api.iterable.com/api/
|
403
|
+
path: lists
|
404
|
+
http_method: GET
|
405
|
+
request_parameters: {}
|
406
|
+
request_headers: {}
|
407
|
+
authenticator:
|
408
|
+
type: ApiKeyAuthenticator
|
409
|
+
api_token: "{{ config['api_key'] }}"
|
410
|
+
inject_into:
|
411
|
+
type: RequestOption
|
412
|
+
field_name: Api-Key
|
413
|
+
inject_into: header
|
414
|
+
request_body_json: {}
|
415
|
+
record_selector:
|
416
|
+
type: RecordSelector
|
417
|
+
extractor:
|
418
|
+
type: DpathExtractor
|
419
|
+
field_path:
|
420
|
+
- lists
|
421
|
+
paginator:
|
422
|
+
type: NoPagination
|
423
|
+
partition_router: []
|
424
|
+
primary_key:
|
425
|
+
- id
|
426
|
+
transformations:
|
427
|
+
- type: AddFields
|
428
|
+
fields:
|
429
|
+
- path:
|
430
|
+
- list_id
|
431
|
+
value: "{{ stream_slice.list_id }}"
|
432
|
+
version: 0.65.0
|
433
|
+
metadata:
|
434
|
+
autoImportSchema:
|
435
|
+
users: false
|
436
|
+
lists: false
|
437
|
+
channels: false
|
438
|
+
metadata: false
|
439
|
+
campaigns: false
|
440
|
+
message_types: false
|
source_iterable/source.py
CHANGED
@@ -2,20 +2,14 @@
|
|
2
2
|
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
|
3
3
|
#
|
4
4
|
|
5
|
-
from typing import Any, List, Mapping
|
5
|
+
from typing import Any, List, Mapping
|
6
6
|
|
7
|
-
import
|
8
|
-
from airbyte_cdk.models import SyncMode
|
9
|
-
from airbyte_cdk.sources import AbstractSource
|
7
|
+
from airbyte_cdk.sources.declarative.yaml_declarative_source import YamlDeclarativeSource
|
10
8
|
from airbyte_cdk.sources.streams import Stream
|
11
9
|
from airbyte_cdk.sources.streams.http.requests_native_auth import TokenAuthenticator
|
12
|
-
from source_iterable.utils import read_full_refresh
|
13
10
|
|
14
11
|
from .streams import (
|
15
|
-
AccessCheck,
|
16
|
-
Campaigns,
|
17
12
|
CampaignsMetrics,
|
18
|
-
Channels,
|
19
13
|
CustomEvent,
|
20
14
|
EmailBounce,
|
21
15
|
EmailClick,
|
@@ -25,7 +19,6 @@ from .streams import (
|
|
25
19
|
EmailSendSkip,
|
26
20
|
EmailSubscribe,
|
27
21
|
EmailUnsubscribe,
|
28
|
-
Events,
|
29
22
|
HostedUnsubscribeClick,
|
30
23
|
InAppClick,
|
31
24
|
InAppClose,
|
@@ -36,10 +29,6 @@ from .streams import (
|
|
36
29
|
InAppSendSkip,
|
37
30
|
InboxMessageImpression,
|
38
31
|
InboxSession,
|
39
|
-
Lists,
|
40
|
-
ListUsers,
|
41
|
-
MessageTypes,
|
42
|
-
Metadata,
|
43
32
|
Purchase,
|
44
33
|
PushBounce,
|
45
34
|
PushOpen,
|
@@ -53,101 +42,70 @@ from .streams import (
|
|
53
42
|
SmsSendSkip,
|
54
43
|
SmsUsageInfo,
|
55
44
|
Templates,
|
56
|
-
Users,
|
57
45
|
WebPushClick,
|
58
46
|
WebPushSend,
|
59
47
|
WebPushSendSkip,
|
60
48
|
)
|
61
49
|
|
50
|
+
"""
|
51
|
+
This file provides the necessary constructs to interpret a provided declarative YAML configuration file into
|
52
|
+
source connector.
|
62
53
|
|
63
|
-
|
64
|
-
|
65
|
-
Note: there are some redundant endpoints
|
66
|
-
(e.g. [`export/userEvents`](https://api.iterable.com/api/docs#export_exportUserEvents)
|
67
|
-
and [`events/{email}`](https://api.iterable.com/api/docs#events_User_events)).
|
68
|
-
In this case it's better to use the one which takes params as a query param rather than as part of the url param.
|
69
|
-
"""
|
54
|
+
WARNING: Do not modify this file.
|
55
|
+
"""
|
70
56
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
next(list_gen)
|
76
|
-
return True, None
|
77
|
-
except Exception as e:
|
78
|
-
return False, f"Unable to connect to Iterable API with the provided credentials - {e}"
|
57
|
+
# Declarative Source
|
58
|
+
class SourceIterable(YamlDeclarativeSource):
|
59
|
+
def __init__(self):
|
60
|
+
super().__init__(**{"path_to_yaml": "manifest.yaml"})
|
79
61
|
|
80
62
|
def streams(self, config: Mapping[str, Any]) -> List[Stream]:
|
81
|
-
|
82
|
-
access_check_stream = AccessCheck(authenticator=authenticator)
|
83
|
-
try:
|
84
|
-
next(read_full_refresh(access_check_stream), None)
|
85
|
-
except requests.exceptions.RequestException as e:
|
86
|
-
if e.response.status_code == requests.codes.UNAUTHORIZED:
|
87
|
-
return False
|
88
|
-
raise
|
89
|
-
return True
|
63
|
+
streams = super().streams(config=config)
|
90
64
|
|
91
65
|
authenticator = TokenAuthenticator(token=config["api_key"], auth_header="Api-Key", auth_method="")
|
92
66
|
# end date is provided for integration tests only
|
93
67
|
start_date, end_date = config["start_date"], config.get("end_date")
|
94
68
|
date_range = {"start_date": start_date, "end_date": end_date}
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
InAppDelivery(authenticator=authenticator, **date_range),
|
138
|
-
InAppSendSkip(authenticator=authenticator, **date_range),
|
139
|
-
InboxSession(authenticator=authenticator, **date_range),
|
140
|
-
InboxMessageImpression(authenticator=authenticator, **date_range),
|
141
|
-
SmsSend(authenticator=authenticator, **date_range),
|
142
|
-
SmsBounce(authenticator=authenticator, **date_range),
|
143
|
-
SmsClick(authenticator=authenticator, **date_range),
|
144
|
-
SmsReceived(authenticator=authenticator, **date_range),
|
145
|
-
SmsSendSkip(authenticator=authenticator, **date_range),
|
146
|
-
SmsUsageInfo(authenticator=authenticator, **date_range),
|
147
|
-
Purchase(authenticator=authenticator, **date_range),
|
148
|
-
CustomEvent(authenticator=authenticator, **date_range),
|
149
|
-
HostedUnsubscribeClick(authenticator=authenticator, **date_range),
|
150
|
-
Events(authenticator=authenticator),
|
151
|
-
]
|
152
|
-
)
|
69
|
+
|
70
|
+
# TODO: migrate streams below to low code as slicer logic will be migrated to generator based
|
71
|
+
streams.extend(
|
72
|
+
[
|
73
|
+
CampaignsMetrics(authenticator=authenticator, **date_range),
|
74
|
+
Templates(authenticator=authenticator, **date_range),
|
75
|
+
EmailBounce(authenticator=authenticator, **date_range),
|
76
|
+
EmailClick(authenticator=authenticator, **date_range),
|
77
|
+
EmailComplaint(authenticator=authenticator, **date_range),
|
78
|
+
EmailOpen(authenticator=authenticator, **date_range),
|
79
|
+
EmailSend(authenticator=authenticator, **date_range),
|
80
|
+
EmailSendSkip(authenticator=authenticator, **date_range),
|
81
|
+
EmailSubscribe(authenticator=authenticator, **date_range),
|
82
|
+
EmailUnsubscribe(authenticator=authenticator, **date_range),
|
83
|
+
PushSend(authenticator=authenticator, **date_range),
|
84
|
+
PushSendSkip(authenticator=authenticator, **date_range),
|
85
|
+
PushOpen(authenticator=authenticator, **date_range),
|
86
|
+
PushUninstall(authenticator=authenticator, **date_range),
|
87
|
+
PushBounce(authenticator=authenticator, **date_range),
|
88
|
+
WebPushSend(authenticator=authenticator, **date_range),
|
89
|
+
WebPushClick(authenticator=authenticator, **date_range),
|
90
|
+
WebPushSendSkip(authenticator=authenticator, **date_range),
|
91
|
+
InAppSend(authenticator=authenticator, **date_range),
|
92
|
+
InAppOpen(authenticator=authenticator, **date_range),
|
93
|
+
InAppClick(authenticator=authenticator, **date_range),
|
94
|
+
InAppClose(authenticator=authenticator, **date_range),
|
95
|
+
InAppDelete(authenticator=authenticator, **date_range),
|
96
|
+
InAppDelivery(authenticator=authenticator, **date_range),
|
97
|
+
InAppSendSkip(authenticator=authenticator, **date_range),
|
98
|
+
InboxSession(authenticator=authenticator, **date_range),
|
99
|
+
InboxMessageImpression(authenticator=authenticator, **date_range),
|
100
|
+
SmsSend(authenticator=authenticator, **date_range),
|
101
|
+
SmsBounce(authenticator=authenticator, **date_range),
|
102
|
+
SmsClick(authenticator=authenticator, **date_range),
|
103
|
+
SmsReceived(authenticator=authenticator, **date_range),
|
104
|
+
SmsSendSkip(authenticator=authenticator, **date_range),
|
105
|
+
SmsUsageInfo(authenticator=authenticator, **date_range),
|
106
|
+
Purchase(authenticator=authenticator, **date_range),
|
107
|
+
CustomEvent(authenticator=authenticator, **date_range),
|
108
|
+
HostedUnsubscribeClick(authenticator=authenticator, **date_range),
|
109
|
+
]
|
110
|
+
)
|
153
111
|
return streams
|
source_iterable/spec.json
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
{
|
2
|
-
"documentationUrl": "https://docs.airbyte.com/integrations/sources/iterable",
|
3
2
|
"connectionSpecification": {
|
4
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
5
4
|
"title": "Iterable Spec",
|
@@ -10,14 +9,14 @@
|
|
10
9
|
"api_key": {
|
11
10
|
"type": "string",
|
12
11
|
"title": "API Key",
|
13
|
-
"description": "Iterable API Key. See the <a href
|
12
|
+
"description": "Iterable API Key. See the <a href=\\\"https://docs.airbyte.com/integrations/sources/iterable\\\">docs</a> for more information on how to obtain this key.",
|
14
13
|
"airbyte_secret": true,
|
15
14
|
"order": 0
|
16
15
|
},
|
17
16
|
"start_date": {
|
18
17
|
"type": "string",
|
19
18
|
"title": "Start Date",
|
20
|
-
"description": "The date from which you'd like to replicate data for Iterable, in the format YYYY-MM-DDT00:00:00Z.
|
19
|
+
"description": "The date from which you'd like to replicate data for Iterable, in the format YYYY-MM-DDT00:00:00Z. All data generated after this date will be replicated.",
|
21
20
|
"examples": ["2021-04-01T00:00:00Z"],
|
22
21
|
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$",
|
23
22
|
"order": 1,
|
source_iterable/streams.py
CHANGED
@@ -4,7 +4,6 @@
|
|
4
4
|
|
5
5
|
import csv
|
6
6
|
import json
|
7
|
-
import urllib.parse as urlparse
|
8
7
|
from abc import ABC, abstractmethod
|
9
8
|
from io import StringIO
|
10
9
|
from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Optional, Union
|
@@ -66,11 +65,6 @@ class IterableStream(HttpStream, ABC):
|
|
66
65
|
"""
|
67
66
|
return None
|
68
67
|
|
69
|
-
def check_unauthorized_key(self, response: requests.Response) -> bool:
|
70
|
-
if response.status_code == codes.UNAUTHORIZED:
|
71
|
-
self.logger.warning(f"Provided API Key has not sufficient permissions to read from stream: {self.data_field}")
|
72
|
-
return True
|
73
|
-
|
74
68
|
def check_generic_error(self, response: requests.Response) -> bool:
|
75
69
|
"""
|
76
70
|
https://github.com/airbytehq/oncall/issues/1592#issuecomment-1499109251
|
@@ -129,9 +123,6 @@ class IterableStream(HttpStream, ABC):
|
|
129
123
|
yield from super().read_records(sync_mode, cursor_field=cursor_field, stream_slice=stream_slice, stream_state=stream_state)
|
130
124
|
except (HTTPError, UserDefinedBackoffException, DefaultBackoffException) as e:
|
131
125
|
response = e.response
|
132
|
-
if self.check_unauthorized_key(response):
|
133
|
-
self.ignore_further_slices = True
|
134
|
-
return
|
135
126
|
if self.check_generic_error(response):
|
136
127
|
return
|
137
128
|
raise e
|
@@ -342,42 +333,6 @@ class IterableExportEventsStreamAdjustableRange(IterableExportStreamAdjustableRa
|
|
342
333
|
return ResourceSchemaLoader(package_name_from_class(self.__class__)).get_schema("events")
|
343
334
|
|
344
335
|
|
345
|
-
class Lists(IterableStream):
|
346
|
-
data_field = "lists"
|
347
|
-
|
348
|
-
def path(self, **kwargs) -> str:
|
349
|
-
return "lists"
|
350
|
-
|
351
|
-
|
352
|
-
class ListUsers(IterableStream):
|
353
|
-
primary_key = "listId"
|
354
|
-
data_field = "getUsers"
|
355
|
-
name = "list_users"
|
356
|
-
# enable caching, because this stream used by other ones
|
357
|
-
use_cache = True
|
358
|
-
|
359
|
-
def path(self, stream_slice: Optional[Mapping[str, Any]] = None, **kwargs) -> str:
|
360
|
-
return f"lists/{self.data_field}?listId={stream_slice['list_id']}"
|
361
|
-
|
362
|
-
def stream_slices(self, **kwargs) -> Iterable[Optional[Mapping[str, any]]]:
|
363
|
-
lists = Lists(authenticator=self._cred)
|
364
|
-
for list_record in lists.read_records(sync_mode=kwargs.get("sync_mode", SyncMode.full_refresh)):
|
365
|
-
yield {"list_id": list_record["id"]}
|
366
|
-
|
367
|
-
def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
|
368
|
-
list_id = self._get_list_id(response.url)
|
369
|
-
for user in response.iter_lines():
|
370
|
-
yield {"email": user.decode(), "listId": list_id}
|
371
|
-
|
372
|
-
@staticmethod
|
373
|
-
def _get_list_id(url: str) -> int:
|
374
|
-
parsed_url = urlparse.urlparse(url)
|
375
|
-
for q in parsed_url.query.split("&"):
|
376
|
-
key, value = q.split("=")
|
377
|
-
if key == "listId":
|
378
|
-
return int(value)
|
379
|
-
|
380
|
-
|
381
336
|
class Campaigns(IterableStream):
|
382
337
|
data_field = "campaigns"
|
383
338
|
|
@@ -465,71 +420,6 @@ class CampaignsMetrics(IterableStream):
|
|
465
420
|
return result
|
466
421
|
|
467
422
|
|
468
|
-
class Channels(IterableStream):
|
469
|
-
data_field = "channels"
|
470
|
-
|
471
|
-
def path(self, **kwargs) -> str:
|
472
|
-
return "channels"
|
473
|
-
|
474
|
-
|
475
|
-
class MessageTypes(IterableStream):
|
476
|
-
data_field = "messageTypes"
|
477
|
-
name = "message_types"
|
478
|
-
|
479
|
-
def path(self, **kwargs) -> str:
|
480
|
-
return "messageTypes"
|
481
|
-
|
482
|
-
|
483
|
-
class Metadata(IterableStream):
|
484
|
-
primary_key = None
|
485
|
-
data_field = "results"
|
486
|
-
|
487
|
-
def path(self, **kwargs) -> str:
|
488
|
-
return "metadata"
|
489
|
-
|
490
|
-
|
491
|
-
class Events(IterableStream):
|
492
|
-
"""
|
493
|
-
https://api.iterable.com/api/docs#export_exportUserEvents
|
494
|
-
"""
|
495
|
-
|
496
|
-
primary_key = None
|
497
|
-
data_field = "events"
|
498
|
-
common_fields = ("itblInternal", "_type", "createdAt", "email")
|
499
|
-
|
500
|
-
def path(self, **kwargs) -> str:
|
501
|
-
return "export/userEvents"
|
502
|
-
|
503
|
-
def request_params(self, stream_slice: Optional[Mapping[str, Any]], **kwargs) -> MutableMapping[str, Any]:
|
504
|
-
params = super().request_params(**kwargs)
|
505
|
-
params.update({"email": stream_slice["email"], "includeCustomEvents": "true"})
|
506
|
-
|
507
|
-
return params
|
508
|
-
|
509
|
-
def stream_slices(self, **kwargs) -> Iterable[Optional[Mapping[str, any]]]:
|
510
|
-
lists = ListUsers(authenticator=self._cred)
|
511
|
-
stream_slices = lists.stream_slices()
|
512
|
-
|
513
|
-
for stream_slice in stream_slices:
|
514
|
-
for list_record in lists.read_records(sync_mode=kwargs.get("sync_mode", SyncMode.full_refresh), stream_slice=stream_slice):
|
515
|
-
yield {"email": list_record["email"]}
|
516
|
-
|
517
|
-
def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
|
518
|
-
"""
|
519
|
-
Parse jsonl response body.
|
520
|
-
Put common event fields at the top level.
|
521
|
-
Put the rest of the fields in the `data` subobject.
|
522
|
-
"""
|
523
|
-
jsonl_records = StringIO(response.text)
|
524
|
-
for record in jsonl_records:
|
525
|
-
record_dict = json.loads(record)
|
526
|
-
record_dict_common_fields = {}
|
527
|
-
for field in self.common_fields:
|
528
|
-
record_dict_common_fields[field] = record_dict.pop(field, None)
|
529
|
-
|
530
|
-
yield {**record_dict_common_fields, "data": record_dict}
|
531
|
-
|
532
|
-
|
533
423
|
class EmailBounce(IterableExportStreamAdjustableRange):
|
534
424
|
data_field = "emailBounce"
|
535
425
|
|
@@ -687,16 +577,3 @@ class Templates(IterableExportStreamRanged):
|
|
687
577
|
for record in records:
|
688
578
|
record[self.cursor_field] = self._field_to_datetime(record[self.cursor_field])
|
689
579
|
yield record
|
690
|
-
|
691
|
-
|
692
|
-
class Users(IterableExportStreamRanged):
|
693
|
-
data_field = "user"
|
694
|
-
cursor_field = "profileUpdatedAt"
|
695
|
-
|
696
|
-
|
697
|
-
class AccessCheck(ListUsers):
|
698
|
-
# since 401 error is failed silently in all the streams,
|
699
|
-
# we need another class to distinguish an empty stream from 401 response
|
700
|
-
def check_unauthorized_key(self, response: requests.Response) -> bool:
|
701
|
-
# this allows not retrying 401 and raising the error upstream
|
702
|
-
return response.status_code != codes.UNAUTHORIZED
|
source_iterable/utils.py
CHANGED
@@ -24,10 +24,3 @@ def dateutil_parse(text):
|
|
24
24
|
dt.microsecond,
|
25
25
|
tz=dt.tzinfo or pendulum.tz.UTC,
|
26
26
|
)
|
27
|
-
|
28
|
-
|
29
|
-
def read_full_refresh(stream_instance: Stream):
|
30
|
-
slices = stream_instance.stream_slices(sync_mode=SyncMode.full_refresh)
|
31
|
-
for _slice in slices:
|
32
|
-
for record in stream_instance.read_records(stream_slice=_slice, sync_mode=SyncMode.full_refresh):
|
33
|
-
yield record
|
File without changes
|
{airbyte_source_iterable-0.4.0.dist-info → airbyte_source_iterable-0.5.0.dist-info}/entry_points.txt
RENAMED
File without changes
|