airbyte-source-iterable 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: airbyte-source-iterable
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: Source implementation for Iterable.
5
5
  Home-page: https://airbyte.com
6
6
  License: MIT
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.9
13
13
  Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
- Requires-Dist: airbyte-cdk (==0.63.2)
15
+ Requires-Dist: airbyte-cdk (>=0,<1)
16
16
  Requires-Dist: pendulum (==2.1.2)
17
17
  Requires-Dist: python-dateutil (==2.8.2)
18
18
  Requires-Dist: requests (==2.31.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=RC-3qvp_M6n4vKXsxyyOk8c6tqwh6t7FKX64XeMHcAk,6707
23
- source_iterable/spec.json,sha256=QQ2V2eUQW88gtgpNjpxOe_zFbBDFIU8zI7Gtv25LC2I,1083
24
- source_iterable/streams.py,sha256=6x2T0r89IIGsB7iO7suSBBAI96uyFlM4PuTky5MxTek,23693
25
- source_iterable/utils.py,sha256=ppDgm3BpTDsXY6AsLg3POL3mKj2gkyEmontg86l3ifQ,932
26
- airbyte_source_iterable-0.3.0.dist-info/METADATA,sha256=4ecXD0HVzjJ-8rRY7r7AzJYsR4fcyUZECKE1WneAJlI,5352
27
- airbyte_source_iterable-0.3.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
28
- airbyte_source_iterable-0.3.0.dist-info/entry_points.txt,sha256=pjzrNtsOX3jW37IK0U5pSmLiraiuLTPr5aB5-hBXpAo,59
29
- airbyte_source_iterable-0.3.0.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.8.1
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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, Tuple
5
+ from typing import Any, List, Mapping
6
6
 
7
- import requests.exceptions
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
- class SourceIterable(AbstractSource):
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
- def check_connection(self, logger, config) -> Tuple[bool, any]:
72
- try:
73
- authenticator = TokenAuthenticator(token=config["api_key"], auth_header="Api-Key", auth_method="")
74
- list_gen = Lists(authenticator=authenticator).read_records(sync_mode=SyncMode.full_refresh)
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
- def all_streams_accessible():
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
- streams = [
96
- Campaigns(authenticator=authenticator),
97
- CampaignsMetrics(authenticator=authenticator, **date_range),
98
- Channels(authenticator=authenticator),
99
- Lists(authenticator=authenticator),
100
- MessageTypes(authenticator=authenticator),
101
- Metadata(authenticator=authenticator),
102
- Templates(authenticator=authenticator, **date_range),
103
- ]
104
- # Iterable supports two types of Server-side api keys:
105
- # - read only
106
- # - server side
107
- # The first one has a limited set of supported APIs, so others are filtered out here.
108
- # A simple check is done - a read operation on a stream that can be accessed only via a Server side API key.
109
- # If read is successful - other streams should be supported as well.
110
- # More on this - https://support.iterable.com/hc/en-us/articles/360043464871-API-Keys-
111
- if all_streams_accessible():
112
- streams.extend(
113
- [
114
- Users(authenticator=authenticator, **date_range),
115
- ListUsers(authenticator=authenticator),
116
- EmailBounce(authenticator=authenticator, **date_range),
117
- EmailClick(authenticator=authenticator, **date_range),
118
- EmailComplaint(authenticator=authenticator, **date_range),
119
- EmailOpen(authenticator=authenticator, **date_range),
120
- EmailSend(authenticator=authenticator, **date_range),
121
- EmailSendSkip(authenticator=authenticator, **date_range),
122
- EmailSubscribe(authenticator=authenticator, **date_range),
123
- EmailUnsubscribe(authenticator=authenticator, **date_range),
124
- PushSend(authenticator=authenticator, **date_range),
125
- PushSendSkip(authenticator=authenticator, **date_range),
126
- PushOpen(authenticator=authenticator, **date_range),
127
- PushUninstall(authenticator=authenticator, **date_range),
128
- PushBounce(authenticator=authenticator, **date_range),
129
- WebPushSend(authenticator=authenticator, **date_range),
130
- WebPushClick(authenticator=authenticator, **date_range),
131
- WebPushSendSkip(authenticator=authenticator, **date_range),
132
- InAppSend(authenticator=authenticator, **date_range),
133
- InAppOpen(authenticator=authenticator, **date_range),
134
- InAppClick(authenticator=authenticator, **date_range),
135
- InAppClose(authenticator=authenticator, **date_range),
136
- InAppDelete(authenticator=authenticator, **date_range),
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=\"https://docs.airbyte.com/integrations/sources/iterable\">docs</a> for more information on how to obtain this key.",
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. All data generated after this date will be replicated.",
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,
@@ -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