ingestr 0.13.2__py3-none-any.whl → 0.14.104__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.
Files changed (146) hide show
  1. ingestr/conftest.py +72 -0
  2. ingestr/main.py +134 -87
  3. ingestr/src/adjust/__init__.py +4 -4
  4. ingestr/src/adjust/adjust_helpers.py +7 -3
  5. ingestr/src/airtable/__init__.py +3 -2
  6. ingestr/src/allium/__init__.py +128 -0
  7. ingestr/src/anthropic/__init__.py +277 -0
  8. ingestr/src/anthropic/helpers.py +525 -0
  9. ingestr/src/applovin/__init__.py +262 -0
  10. ingestr/src/applovin_max/__init__.py +117 -0
  11. ingestr/src/appsflyer/__init__.py +325 -0
  12. ingestr/src/appsflyer/client.py +49 -45
  13. ingestr/src/appstore/__init__.py +1 -0
  14. ingestr/src/arrow/__init__.py +9 -1
  15. ingestr/src/asana_source/__init__.py +1 -1
  16. ingestr/src/attio/__init__.py +102 -0
  17. ingestr/src/attio/helpers.py +65 -0
  18. ingestr/src/blob.py +38 -11
  19. ingestr/src/buildinfo.py +1 -0
  20. ingestr/src/chess/__init__.py +1 -1
  21. ingestr/src/clickup/__init__.py +85 -0
  22. ingestr/src/clickup/helpers.py +47 -0
  23. ingestr/src/collector/spinner.py +43 -0
  24. ingestr/src/couchbase_source/__init__.py +118 -0
  25. ingestr/src/couchbase_source/helpers.py +135 -0
  26. ingestr/src/cursor/__init__.py +83 -0
  27. ingestr/src/cursor/helpers.py +188 -0
  28. ingestr/src/destinations.py +520 -33
  29. ingestr/src/docebo/__init__.py +589 -0
  30. ingestr/src/docebo/client.py +435 -0
  31. ingestr/src/docebo/helpers.py +97 -0
  32. ingestr/src/elasticsearch/__init__.py +80 -0
  33. ingestr/src/elasticsearch/helpers.py +138 -0
  34. ingestr/src/errors.py +8 -0
  35. ingestr/src/facebook_ads/__init__.py +47 -28
  36. ingestr/src/facebook_ads/helpers.py +59 -37
  37. ingestr/src/facebook_ads/settings.py +2 -0
  38. ingestr/src/facebook_ads/utils.py +39 -0
  39. ingestr/src/factory.py +116 -2
  40. ingestr/src/filesystem/__init__.py +8 -3
  41. ingestr/src/filters.py +46 -3
  42. ingestr/src/fluxx/__init__.py +9906 -0
  43. ingestr/src/fluxx/helpers.py +209 -0
  44. ingestr/src/frankfurter/__init__.py +157 -0
  45. ingestr/src/frankfurter/helpers.py +48 -0
  46. ingestr/src/freshdesk/__init__.py +89 -0
  47. ingestr/src/freshdesk/freshdesk_client.py +137 -0
  48. ingestr/src/freshdesk/settings.py +9 -0
  49. ingestr/src/fundraiseup/__init__.py +95 -0
  50. ingestr/src/fundraiseup/client.py +81 -0
  51. ingestr/src/github/__init__.py +41 -6
  52. ingestr/src/github/helpers.py +5 -5
  53. ingestr/src/google_analytics/__init__.py +22 -4
  54. ingestr/src/google_analytics/helpers.py +124 -6
  55. ingestr/src/google_sheets/__init__.py +4 -4
  56. ingestr/src/google_sheets/helpers/data_processing.py +2 -2
  57. ingestr/src/hostaway/__init__.py +302 -0
  58. ingestr/src/hostaway/client.py +288 -0
  59. ingestr/src/http/__init__.py +35 -0
  60. ingestr/src/http/readers.py +114 -0
  61. ingestr/src/http_client.py +24 -0
  62. ingestr/src/hubspot/__init__.py +66 -23
  63. ingestr/src/hubspot/helpers.py +52 -22
  64. ingestr/src/hubspot/settings.py +14 -7
  65. ingestr/src/influxdb/__init__.py +46 -0
  66. ingestr/src/influxdb/client.py +34 -0
  67. ingestr/src/intercom/__init__.py +142 -0
  68. ingestr/src/intercom/helpers.py +674 -0
  69. ingestr/src/intercom/settings.py +279 -0
  70. ingestr/src/isoc_pulse/__init__.py +159 -0
  71. ingestr/src/jira_source/__init__.py +340 -0
  72. ingestr/src/jira_source/helpers.py +439 -0
  73. ingestr/src/jira_source/settings.py +170 -0
  74. ingestr/src/kafka/__init__.py +4 -1
  75. ingestr/src/kinesis/__init__.py +139 -0
  76. ingestr/src/kinesis/helpers.py +82 -0
  77. ingestr/src/klaviyo/{_init_.py → __init__.py} +5 -6
  78. ingestr/src/linear/__init__.py +634 -0
  79. ingestr/src/linear/helpers.py +111 -0
  80. ingestr/src/linkedin_ads/helpers.py +0 -1
  81. ingestr/src/loader.py +69 -0
  82. ingestr/src/mailchimp/__init__.py +126 -0
  83. ingestr/src/mailchimp/helpers.py +226 -0
  84. ingestr/src/mailchimp/settings.py +164 -0
  85. ingestr/src/masking.py +344 -0
  86. ingestr/src/mixpanel/__init__.py +62 -0
  87. ingestr/src/mixpanel/client.py +99 -0
  88. ingestr/src/monday/__init__.py +246 -0
  89. ingestr/src/monday/helpers.py +392 -0
  90. ingestr/src/monday/settings.py +328 -0
  91. ingestr/src/mongodb/__init__.py +72 -8
  92. ingestr/src/mongodb/helpers.py +915 -38
  93. ingestr/src/partition.py +32 -0
  94. ingestr/src/personio/__init__.py +331 -0
  95. ingestr/src/personio/helpers.py +86 -0
  96. ingestr/src/phantombuster/__init__.py +65 -0
  97. ingestr/src/phantombuster/client.py +87 -0
  98. ingestr/src/pinterest/__init__.py +82 -0
  99. ingestr/src/pipedrive/__init__.py +198 -0
  100. ingestr/src/pipedrive/helpers/__init__.py +23 -0
  101. ingestr/src/pipedrive/helpers/custom_fields_munger.py +102 -0
  102. ingestr/src/pipedrive/helpers/pages.py +115 -0
  103. ingestr/src/pipedrive/settings.py +27 -0
  104. ingestr/src/pipedrive/typing.py +3 -0
  105. ingestr/src/plusvibeai/__init__.py +335 -0
  106. ingestr/src/plusvibeai/helpers.py +544 -0
  107. ingestr/src/plusvibeai/settings.py +252 -0
  108. ingestr/src/quickbooks/__init__.py +117 -0
  109. ingestr/src/resource.py +40 -0
  110. ingestr/src/revenuecat/__init__.py +83 -0
  111. ingestr/src/revenuecat/helpers.py +237 -0
  112. ingestr/src/salesforce/__init__.py +156 -0
  113. ingestr/src/salesforce/helpers.py +64 -0
  114. ingestr/src/shopify/__init__.py +1 -17
  115. ingestr/src/smartsheets/__init__.py +82 -0
  116. ingestr/src/snapchat_ads/__init__.py +489 -0
  117. ingestr/src/snapchat_ads/client.py +72 -0
  118. ingestr/src/snapchat_ads/helpers.py +535 -0
  119. ingestr/src/socrata_source/__init__.py +83 -0
  120. ingestr/src/socrata_source/helpers.py +85 -0
  121. ingestr/src/socrata_source/settings.py +8 -0
  122. ingestr/src/solidgate/__init__.py +219 -0
  123. ingestr/src/solidgate/helpers.py +154 -0
  124. ingestr/src/sources.py +3132 -212
  125. ingestr/src/stripe_analytics/__init__.py +49 -21
  126. ingestr/src/stripe_analytics/helpers.py +286 -1
  127. ingestr/src/stripe_analytics/settings.py +62 -10
  128. ingestr/src/telemetry/event.py +10 -9
  129. ingestr/src/tiktok_ads/__init__.py +12 -6
  130. ingestr/src/tiktok_ads/tiktok_helpers.py +0 -1
  131. ingestr/src/trustpilot/__init__.py +48 -0
  132. ingestr/src/trustpilot/client.py +48 -0
  133. ingestr/src/version.py +6 -1
  134. ingestr/src/wise/__init__.py +68 -0
  135. ingestr/src/wise/client.py +63 -0
  136. ingestr/src/zoom/__init__.py +99 -0
  137. ingestr/src/zoom/helpers.py +102 -0
  138. ingestr/tests/unit/test_smartsheets.py +133 -0
  139. ingestr-0.14.104.dist-info/METADATA +563 -0
  140. ingestr-0.14.104.dist-info/RECORD +203 -0
  141. ingestr/src/appsflyer/_init_.py +0 -24
  142. ingestr-0.13.2.dist-info/METADATA +0 -302
  143. ingestr-0.13.2.dist-info/RECORD +0 -107
  144. {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/WHEEL +0 -0
  145. {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/entry_points.txt +0 -0
  146. {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/licenses/LICENSE.md +0 -0
@@ -0,0 +1,489 @@
1
+ """Loads organizations and other data from Snapchat Marketing API"""
2
+
3
+ from typing import Iterator
4
+
5
+ import dlt
6
+ from dlt.common.typing import TDataItems
7
+
8
+ from .client import SnapchatAdsAPI, create_client
9
+ from .helpers import (
10
+ fetch_account_id_resource,
11
+ fetch_entity_stats,
12
+ fetch_snapchat_data,
13
+ fetch_snapchat_data_with_params,
14
+ fetch_with_paginate_account_id,
15
+ paginate,
16
+ )
17
+
18
+ BASE_URL = "https://adsapi.snapchat.com/v1"
19
+
20
+
21
+ @dlt.source(name="snapchat_ads", max_table_nesting=0)
22
+ def snapchat_ads_source(
23
+ refresh_token: str = dlt.secrets.value,
24
+ client_id: str = dlt.secrets.value,
25
+ client_secret: str = dlt.secrets.value,
26
+ organization_id: str | None = None,
27
+ ad_account_id: str | None = None,
28
+ start_date: str | None = None,
29
+ end_date: str | None = None,
30
+ stats_config: dict | None = None,
31
+ ):
32
+ """Returns a list of resources to load data from Snapchat Marketing API.
33
+
34
+ Args:
35
+ refresh_token (str): OAuth refresh token for Snapchat Marketing API
36
+ client_id (str): OAuth client ID
37
+ client_secret (str): OAuth client secret
38
+ organization_id (str): Organization ID (optional for organizations table, required for others)
39
+ ad_account_id (str): Ad Account ID (optional, used to filter resources by ad account)
40
+ start_date (str): Optional start date for filtering data
41
+ end_date (str): Optional end date for filtering data
42
+
43
+ Returns:
44
+ tuple: A tuple of three DltResource objects (organizations, fundingsources, billingcenters)
45
+ """
46
+ api = SnapchatAdsAPI(
47
+ refresh_token=refresh_token, client_id=client_id, client_secret=client_secret
48
+ )
49
+
50
+ @dlt.resource(primary_key="id", write_disposition="merge")
51
+ def organizations(
52
+ updated_at=dlt.sources.incremental("updated_at"),
53
+ ) -> Iterator[TDataItems]:
54
+ """Fetch all organizations for the authenticated user."""
55
+ url = f"{BASE_URL}/me/organizations"
56
+ yield from fetch_snapchat_data(
57
+ api, url, "organizations", "organization", start_date, end_date
58
+ )
59
+
60
+ @dlt.resource(primary_key="id", write_disposition="merge")
61
+ def fundingsources(
62
+ updated_at=dlt.sources.incremental("updated_at"),
63
+ ) -> Iterator[TDataItems]:
64
+ """Fetch all funding sources for the organization."""
65
+ if not organization_id:
66
+ raise ValueError("organization_id is required for fundingsources")
67
+
68
+ url = f"{BASE_URL}/organizations/{organization_id}/fundingsources"
69
+ yield from fetch_snapchat_data(
70
+ api, url, "fundingsources", "fundingsource", start_date, end_date
71
+ )
72
+
73
+ @dlt.resource(primary_key="id", write_disposition="merge")
74
+ def billingcenters(
75
+ updated_at=dlt.sources.incremental("updated_at"),
76
+ ) -> Iterator[TDataItems]:
77
+ """Fetch all billing centers for the organization."""
78
+ if not organization_id:
79
+ raise ValueError("organization_id is required for billingcenters")
80
+
81
+ url = f"{BASE_URL}/organizations/{organization_id}/billingcenters"
82
+ yield from fetch_snapchat_data(
83
+ api, url, "billingcenters", "billingcenter", start_date, end_date
84
+ )
85
+
86
+ @dlt.resource(primary_key="id", write_disposition="merge")
87
+ def adaccounts(
88
+ updated_at=dlt.sources.incremental("updated_at"),
89
+ ) -> Iterator[TDataItems]:
90
+ """Fetch all ad accounts for the organization."""
91
+ if not organization_id:
92
+ raise ValueError("organization_id is required for adaccounts")
93
+
94
+ url = f"{BASE_URL}/organizations/{organization_id}/adaccounts"
95
+ yield from fetch_snapchat_data(
96
+ api, url, "adaccounts", "adaccount", start_date, end_date
97
+ )
98
+
99
+ @dlt.resource(primary_key="id", write_disposition="merge")
100
+ def invoices(
101
+ updated_at=dlt.sources.incremental("updated_at"),
102
+ ) -> Iterator[TDataItems]:
103
+ """Fetch all invoices for a specific ad account or all ad accounts.
104
+
105
+ If ad_account_id is provided, fetch invoices only for that account.
106
+ If ad_account_id is None, fetch all ad accounts first and then get invoices for each.
107
+ """
108
+ # If specific ad_account_id provided, fetch only that account's invoices
109
+ if ad_account_id:
110
+ url = f"{BASE_URL}/adaccounts/{ad_account_id}/invoices"
111
+ yield from fetch_snapchat_data(
112
+ api, url, "invoices", "invoice", start_date, end_date
113
+ )
114
+ else:
115
+ # Otherwise, fetch all ad accounts first
116
+ if not organization_id:
117
+ raise ValueError(
118
+ "organization_id is required to fetch invoices for all ad accounts"
119
+ )
120
+
121
+ accounts_url = f"{BASE_URL}/organizations/{organization_id}/adaccounts"
122
+ accounts_data = list(
123
+ fetch_snapchat_data(
124
+ api,
125
+ accounts_url,
126
+ "adaccounts",
127
+ "adaccount",
128
+ start_date,
129
+ end_date,
130
+ )
131
+ )
132
+
133
+ # Then fetch invoices for each ad account
134
+ for account in accounts_data:
135
+ account_id = account.get("id")
136
+ if account_id:
137
+ invoices_url = f"{BASE_URL}/adaccounts/{account_id}/invoices"
138
+ yield from fetch_snapchat_data(
139
+ api,
140
+ invoices_url,
141
+ "invoices",
142
+ "invoice",
143
+ start_date,
144
+ end_date,
145
+ )
146
+
147
+ @dlt.resource(write_disposition="replace")
148
+ def transactions() -> Iterator[TDataItems]:
149
+ """Fetch all transactions for the organization."""
150
+ if not organization_id:
151
+ raise ValueError("organization_id is required for transactions")
152
+
153
+ url = f"{BASE_URL}/organizations/{organization_id}/transactions"
154
+
155
+ # Build query parameters for API-side filtering
156
+ params = {}
157
+ if start_date:
158
+ from dlt.common.time import ensure_pendulum_datetime
159
+
160
+ params["start_time"] = ensure_pendulum_datetime(start_date).format(
161
+ "YYYY-MM-DDTHH:mm:ss"
162
+ )
163
+
164
+ if end_date:
165
+ from dlt.common.time import ensure_pendulum_datetime
166
+
167
+ params["end_time"] = ensure_pendulum_datetime(end_date).format(
168
+ "YYYY-MM-DDTHH:mm:ss"
169
+ )
170
+
171
+ yield from fetch_snapchat_data_with_params(
172
+ api, url, "transactions", "transaction", params
173
+ )
174
+
175
+ @dlt.resource(write_disposition="replace")
176
+ def members() -> Iterator[TDataItems]:
177
+ """Fetch all members of the organization."""
178
+ if not organization_id:
179
+ raise ValueError("organization_id is required for members")
180
+
181
+ url = f"{BASE_URL}/organizations/{organization_id}/members"
182
+ # Members API doesn't return updated_at in response, so we can't filter by date
183
+ yield from fetch_snapchat_data(api, url, "members", "member", None, None)
184
+
185
+ @dlt.resource(write_disposition="replace")
186
+ def roles() -> Iterator[TDataItems]:
187
+ """Fetch all roles for the organization with pagination."""
188
+ if not organization_id:
189
+ raise ValueError("organization_id is required for roles")
190
+
191
+ url = f"{BASE_URL}/organizations/{organization_id}/roles"
192
+ client = create_client()
193
+ headers = api.get_headers()
194
+
195
+ for result in paginate(client, headers, url, page_size=1000):
196
+ items_data = result.get("roles", [])
197
+
198
+ for item in items_data:
199
+ if item.get("sub_request_status", "").upper() == "SUCCESS":
200
+ data = item.get("role", {})
201
+ if data:
202
+ yield data
203
+
204
+ @dlt.resource(primary_key="id", write_disposition="merge", max_table_nesting=0)
205
+ def campaigns(
206
+ updated_at=dlt.sources.incremental("updated_at"),
207
+ ) -> Iterator[TDataItems]:
208
+ """Fetch all campaigns for a specific ad account or all ad accounts.
209
+
210
+ If ad_account_id is provided, fetch campaigns only for that account.
211
+ If ad_account_id is None, fetch all ad accounts first and then get campaigns for each.
212
+ """
213
+ yield from fetch_with_paginate_account_id(
214
+ api=api,
215
+ ad_account_id=ad_account_id,
216
+ organization_id=organization_id,
217
+ base_url=BASE_URL,
218
+ resource_name="campaigns",
219
+ item_key="campaign",
220
+ start_date=start_date,
221
+ end_date=end_date,
222
+ )
223
+
224
+ @dlt.resource(primary_key="id", write_disposition="merge", max_table_nesting=0)
225
+ def adsquads(
226
+ updated_at=dlt.sources.incremental("updated_at"),
227
+ ) -> Iterator[TDataItems]:
228
+ """Fetch all ad squads for a specific ad account or all ad accounts.
229
+
230
+ If ad_account_id is provided, fetch ad squads only for that account.
231
+ If ad_account_id is None, fetch all ad accounts first and then get ad squads for each.
232
+ """
233
+ yield from fetch_with_paginate_account_id(
234
+ api=api,
235
+ ad_account_id=ad_account_id,
236
+ organization_id=organization_id,
237
+ base_url=BASE_URL,
238
+ resource_name="adsquads",
239
+ item_key="adsquad",
240
+ start_date=start_date,
241
+ end_date=end_date,
242
+ )
243
+
244
+ @dlt.resource(primary_key="id", write_disposition="merge", max_table_nesting=0)
245
+ def ads(
246
+ updated_at=dlt.sources.incremental("updated_at"),
247
+ ) -> Iterator[TDataItems]:
248
+ """Fetch all ads for a specific ad account or all ad accounts.
249
+
250
+ If ad_account_id is provided, fetch ads only for that account.
251
+ If ad_account_id is None, fetch all ad accounts first and then get ads for each.
252
+ """
253
+ yield from fetch_with_paginate_account_id(
254
+ api=api,
255
+ ad_account_id=ad_account_id,
256
+ organization_id=organization_id,
257
+ base_url=BASE_URL,
258
+ resource_name="ads",
259
+ item_key="ad",
260
+ start_date=start_date,
261
+ end_date=end_date,
262
+ )
263
+
264
+ @dlt.resource(primary_key="id", write_disposition="merge")
265
+ def event_details(
266
+ updated_at=dlt.sources.incremental("updated_at"),
267
+ ) -> Iterator[TDataItems]:
268
+ """Fetch all event details for a specific ad account or all ad accounts.
269
+
270
+ If ad_account_id is provided, fetch event details only for that account.
271
+ If ad_account_id is None, fetch all ad accounts first and then get event details for each.
272
+ """
273
+ yield from fetch_account_id_resource(
274
+ api=api,
275
+ ad_account_id=ad_account_id,
276
+ organization_id=organization_id,
277
+ base_url=BASE_URL,
278
+ resource_name="event_details",
279
+ item_key="event_detail",
280
+ start_date=start_date,
281
+ end_date=end_date,
282
+ )
283
+
284
+ @dlt.resource(primary_key="id", write_disposition="merge", max_table_nesting=0)
285
+ def creatives(
286
+ updated_at=dlt.sources.incremental("updated_at"),
287
+ ) -> Iterator[TDataItems]:
288
+ """Fetch all creatives for a specific ad account or all ad accounts.
289
+
290
+ If ad_account_id is provided, fetch creatives only for that account.
291
+ If ad_account_id is None, fetch all ad accounts first and then get creatives for each.
292
+ """
293
+ yield from fetch_with_paginate_account_id(
294
+ api=api,
295
+ ad_account_id=ad_account_id,
296
+ organization_id=organization_id,
297
+ base_url=BASE_URL,
298
+ resource_name="creatives",
299
+ item_key="creative",
300
+ start_date=start_date,
301
+ end_date=end_date,
302
+ )
303
+
304
+ @dlt.resource(primary_key="id", write_disposition="merge", max_table_nesting=0)
305
+ def segments(
306
+ updated_at=dlt.sources.incremental("updated_at"),
307
+ ) -> Iterator[TDataItems]:
308
+ """Fetch all audience segments for a specific ad account or all ad accounts.
309
+
310
+ If ad_account_id is provided, fetch segments only for that account.
311
+ If ad_account_id is None, fetch all ad accounts first and then get segments for each.
312
+ """
313
+ yield from fetch_account_id_resource(
314
+ api=api,
315
+ ad_account_id=ad_account_id,
316
+ organization_id=organization_id,
317
+ base_url=BASE_URL,
318
+ resource_name="segments",
319
+ item_key="segment",
320
+ start_date=start_date,
321
+ end_date=end_date,
322
+ )
323
+
324
+ def _build_stats_params(granularity: str, fields: str) -> dict:
325
+ """Build common stats parameters."""
326
+ params = {
327
+ "granularity": granularity,
328
+ "fields": fields,
329
+ }
330
+
331
+ # Add date range for DAY/HOUR granularity
332
+ if granularity in ["DAY", "HOUR"] and (start_date or end_date):
333
+ from dlt.common.time import ensure_pendulum_datetime
334
+
335
+ if start_date:
336
+ start_dt = ensure_pendulum_datetime(start_date)
337
+ params["start_time"] = start_dt.format("YYYY-MM-DDTHH:mm:ss.000")
338
+ if end_date:
339
+ end_dt = ensure_pendulum_datetime(end_date)
340
+ # For both HOUR and DAY granularity, use ceiling to round up to next hour if needed
341
+ if end_dt != end_dt.start_of("hour"):
342
+ end_dt = end_dt.add(hours=1).start_of("hour")
343
+ params["end_time"] = end_dt.format("YYYY-MM-DDTHH:mm:ss.000")
344
+
345
+ # Add optional parameters from stats_config
346
+ if stats_config:
347
+ optional_params = [
348
+ "breakdown",
349
+ "dimension",
350
+ "pivot",
351
+ "swipe_up_attribution_window",
352
+ "view_attribution_window",
353
+ "action_report_time",
354
+ "conversion_source_types",
355
+ "omit_empty",
356
+ "position_stats",
357
+ "test",
358
+ ]
359
+
360
+ for param in optional_params:
361
+ if param in stats_config:
362
+ params[param] = stats_config[param]
363
+
364
+ return params
365
+
366
+ @dlt.resource(write_disposition="replace", max_table_nesting=0)
367
+ def campaigns_stats() -> Iterator[TDataItems]:
368
+ """Fetch stats for all campaigns.
369
+
370
+ First fetches all campaigns, then fetches stats for each campaign.
371
+ """
372
+ if not stats_config:
373
+ raise ValueError("stats_config is required for campaigns_stats resource")
374
+
375
+ granularity = stats_config.get("granularity", "DAY")
376
+ fields = stats_config.get("fields", "impressions,spend")
377
+
378
+ params = _build_stats_params(granularity, fields)
379
+
380
+ yield from fetch_entity_stats(
381
+ api=api,
382
+ entity_type="campaign",
383
+ ad_account_id=ad_account_id,
384
+ organization_id=organization_id,
385
+ base_url=BASE_URL,
386
+ params=params,
387
+ granularity=granularity,
388
+ start_date=start_date,
389
+ end_date=end_date,
390
+ )
391
+
392
+ @dlt.resource(write_disposition="replace", max_table_nesting=0)
393
+ def ad_accounts_stats() -> Iterator[TDataItems]:
394
+ """Fetch stats for all ad accounts.
395
+
396
+ Fetches stats for each ad account directly.
397
+ """
398
+ if not stats_config:
399
+ raise ValueError("stats_config is required for ad_accounts_stats resource")
400
+
401
+ granularity = stats_config.get("granularity", "DAY")
402
+ fields = stats_config.get("fields", "impressions,spend")
403
+
404
+ params = _build_stats_params(granularity, fields)
405
+
406
+ yield from fetch_entity_stats(
407
+ api=api,
408
+ entity_type="adaccount",
409
+ ad_account_id=ad_account_id,
410
+ organization_id=organization_id,
411
+ base_url=BASE_URL,
412
+ params=params,
413
+ granularity=granularity,
414
+ start_date=start_date,
415
+ end_date=end_date,
416
+ )
417
+
418
+ @dlt.resource(write_disposition="replace", max_table_nesting=0)
419
+ def ads_stats() -> Iterator[TDataItems]:
420
+ """Fetch stats for all ads.
421
+
422
+ First fetches all ads, then fetches stats for each ad.
423
+ """
424
+ if not stats_config:
425
+ raise ValueError("stats_config is required for ads_stats resource")
426
+
427
+ granularity = stats_config.get("granularity", "DAY")
428
+ fields = stats_config.get("fields", "impressions,spend")
429
+
430
+ params = _build_stats_params(granularity, fields)
431
+
432
+ yield from fetch_entity_stats(
433
+ api=api,
434
+ entity_type="ad",
435
+ ad_account_id=ad_account_id,
436
+ organization_id=organization_id,
437
+ base_url=BASE_URL,
438
+ params=params,
439
+ granularity=granularity,
440
+ start_date=start_date,
441
+ end_date=end_date,
442
+ )
443
+
444
+ @dlt.resource(write_disposition="replace", max_table_nesting=0)
445
+ def ad_squads_stats() -> Iterator[TDataItems]:
446
+ """Fetch stats for all ad squads.
447
+
448
+ First fetches all ad squads, then fetches stats for each ad squad.
449
+ """
450
+ if not stats_config:
451
+ raise ValueError("stats_config is required for ad_squads_stats resource")
452
+
453
+ granularity = stats_config.get("granularity", "DAY")
454
+ fields = stats_config.get("fields", "impressions,spend")
455
+
456
+ params = _build_stats_params(granularity, fields)
457
+
458
+ yield from fetch_entity_stats(
459
+ api=api,
460
+ entity_type="adsquad",
461
+ ad_account_id=ad_account_id,
462
+ organization_id=organization_id,
463
+ base_url=BASE_URL,
464
+ params=params,
465
+ granularity=granularity,
466
+ start_date=start_date,
467
+ end_date=end_date,
468
+ )
469
+
470
+ return (
471
+ organizations,
472
+ fundingsources,
473
+ billingcenters,
474
+ adaccounts,
475
+ invoices,
476
+ transactions,
477
+ members,
478
+ roles,
479
+ campaigns,
480
+ adsquads,
481
+ ads,
482
+ event_details,
483
+ creatives,
484
+ segments,
485
+ campaigns_stats,
486
+ ad_accounts_stats,
487
+ ads_stats,
488
+ ad_squads_stats,
489
+ )
@@ -0,0 +1,72 @@
1
+ import requests
2
+ from dlt.sources.helpers.requests import Client
3
+
4
+
5
+ def retry_on_limit(
6
+ response: requests.Response | None, exception: BaseException | None
7
+ ) -> bool:
8
+ if response is None:
9
+ return False
10
+ return response.status_code == 429
11
+
12
+
13
+ def create_client() -> requests.Session:
14
+ return Client(
15
+ raise_for_status=False,
16
+ retry_condition=retry_on_limit,
17
+ request_max_attempts=12,
18
+ request_backoff_factor=2,
19
+ ).session
20
+
21
+
22
+ class SnapchatAdsAPI:
23
+ """Helper class for Snapchat Ads API authentication and requests."""
24
+
25
+ TOKEN_URL = "https://accounts.snapchat.com/login/oauth2/access_token"
26
+
27
+ def __init__(self, refresh_token: str, client_id: str, client_secret: str):
28
+ self.refresh_token = refresh_token
29
+ self.client_id = client_id
30
+ self.client_secret = client_secret
31
+ self._access_token = None
32
+
33
+ def get_access_token(self) -> str:
34
+ """
35
+ Refresh the access token using the refresh token.
36
+
37
+ Returns:
38
+ str: The access token
39
+ """
40
+ if self._access_token:
41
+ return self._access_token
42
+
43
+ client = create_client()
44
+ response = client.post(
45
+ self.TOKEN_URL,
46
+ data={
47
+ "refresh_token": self.refresh_token,
48
+ "client_id": self.client_id,
49
+ "client_secret": self.client_secret,
50
+ "grant_type": "refresh_token",
51
+ },
52
+ )
53
+
54
+ if response.status_code != 200:
55
+ raise ValueError(
56
+ f"Failed to refresh access token: {response.status_code} - {response.text}"
57
+ )
58
+
59
+ result = response.json()
60
+ self._access_token = result.get("access_token")
61
+
62
+ if not self._access_token:
63
+ raise ValueError(f"No access token in response: {result}")
64
+
65
+ return self._access_token
66
+
67
+ def get_headers(self) -> dict:
68
+ access_token = self.get_access_token()
69
+ return {
70
+ "Authorization": f"Bearer {access_token}",
71
+ "Content-Type": "application/json",
72
+ }