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