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.
- ingestr/conftest.py +72 -0
- ingestr/main.py +134 -87
- ingestr/src/adjust/__init__.py +4 -4
- ingestr/src/adjust/adjust_helpers.py +7 -3
- ingestr/src/airtable/__init__.py +3 -2
- ingestr/src/allium/__init__.py +128 -0
- ingestr/src/anthropic/__init__.py +277 -0
- ingestr/src/anthropic/helpers.py +525 -0
- ingestr/src/applovin/__init__.py +262 -0
- ingestr/src/applovin_max/__init__.py +117 -0
- ingestr/src/appsflyer/__init__.py +325 -0
- ingestr/src/appsflyer/client.py +49 -45
- ingestr/src/appstore/__init__.py +1 -0
- ingestr/src/arrow/__init__.py +9 -1
- ingestr/src/asana_source/__init__.py +1 -1
- ingestr/src/attio/__init__.py +102 -0
- ingestr/src/attio/helpers.py +65 -0
- ingestr/src/blob.py +38 -11
- ingestr/src/buildinfo.py +1 -0
- ingestr/src/chess/__init__.py +1 -1
- ingestr/src/clickup/__init__.py +85 -0
- ingestr/src/clickup/helpers.py +47 -0
- ingestr/src/collector/spinner.py +43 -0
- ingestr/src/couchbase_source/__init__.py +118 -0
- ingestr/src/couchbase_source/helpers.py +135 -0
- ingestr/src/cursor/__init__.py +83 -0
- ingestr/src/cursor/helpers.py +188 -0
- ingestr/src/destinations.py +520 -33
- ingestr/src/docebo/__init__.py +589 -0
- ingestr/src/docebo/client.py +435 -0
- ingestr/src/docebo/helpers.py +97 -0
- ingestr/src/elasticsearch/__init__.py +80 -0
- ingestr/src/elasticsearch/helpers.py +138 -0
- ingestr/src/errors.py +8 -0
- ingestr/src/facebook_ads/__init__.py +47 -28
- ingestr/src/facebook_ads/helpers.py +59 -37
- ingestr/src/facebook_ads/settings.py +2 -0
- ingestr/src/facebook_ads/utils.py +39 -0
- ingestr/src/factory.py +116 -2
- ingestr/src/filesystem/__init__.py +8 -3
- ingestr/src/filters.py +46 -3
- ingestr/src/fluxx/__init__.py +9906 -0
- ingestr/src/fluxx/helpers.py +209 -0
- ingestr/src/frankfurter/__init__.py +157 -0
- ingestr/src/frankfurter/helpers.py +48 -0
- ingestr/src/freshdesk/__init__.py +89 -0
- ingestr/src/freshdesk/freshdesk_client.py +137 -0
- ingestr/src/freshdesk/settings.py +9 -0
- ingestr/src/fundraiseup/__init__.py +95 -0
- ingestr/src/fundraiseup/client.py +81 -0
- ingestr/src/github/__init__.py +41 -6
- ingestr/src/github/helpers.py +5 -5
- ingestr/src/google_analytics/__init__.py +22 -4
- ingestr/src/google_analytics/helpers.py +124 -6
- ingestr/src/google_sheets/__init__.py +4 -4
- ingestr/src/google_sheets/helpers/data_processing.py +2 -2
- ingestr/src/hostaway/__init__.py +302 -0
- ingestr/src/hostaway/client.py +288 -0
- ingestr/src/http/__init__.py +35 -0
- ingestr/src/http/readers.py +114 -0
- ingestr/src/http_client.py +24 -0
- ingestr/src/hubspot/__init__.py +66 -23
- ingestr/src/hubspot/helpers.py +52 -22
- ingestr/src/hubspot/settings.py +14 -7
- ingestr/src/influxdb/__init__.py +46 -0
- ingestr/src/influxdb/client.py +34 -0
- ingestr/src/intercom/__init__.py +142 -0
- ingestr/src/intercom/helpers.py +674 -0
- ingestr/src/intercom/settings.py +279 -0
- ingestr/src/isoc_pulse/__init__.py +159 -0
- ingestr/src/jira_source/__init__.py +340 -0
- ingestr/src/jira_source/helpers.py +439 -0
- ingestr/src/jira_source/settings.py +170 -0
- ingestr/src/kafka/__init__.py +4 -1
- ingestr/src/kinesis/__init__.py +139 -0
- ingestr/src/kinesis/helpers.py +82 -0
- ingestr/src/klaviyo/{_init_.py → __init__.py} +5 -6
- ingestr/src/linear/__init__.py +634 -0
- ingestr/src/linear/helpers.py +111 -0
- ingestr/src/linkedin_ads/helpers.py +0 -1
- ingestr/src/loader.py +69 -0
- ingestr/src/mailchimp/__init__.py +126 -0
- ingestr/src/mailchimp/helpers.py +226 -0
- ingestr/src/mailchimp/settings.py +164 -0
- ingestr/src/masking.py +344 -0
- ingestr/src/mixpanel/__init__.py +62 -0
- ingestr/src/mixpanel/client.py +99 -0
- ingestr/src/monday/__init__.py +246 -0
- ingestr/src/monday/helpers.py +392 -0
- ingestr/src/monday/settings.py +328 -0
- ingestr/src/mongodb/__init__.py +72 -8
- ingestr/src/mongodb/helpers.py +915 -38
- ingestr/src/partition.py +32 -0
- ingestr/src/personio/__init__.py +331 -0
- ingestr/src/personio/helpers.py +86 -0
- ingestr/src/phantombuster/__init__.py +65 -0
- ingestr/src/phantombuster/client.py +87 -0
- ingestr/src/pinterest/__init__.py +82 -0
- ingestr/src/pipedrive/__init__.py +198 -0
- ingestr/src/pipedrive/helpers/__init__.py +23 -0
- ingestr/src/pipedrive/helpers/custom_fields_munger.py +102 -0
- ingestr/src/pipedrive/helpers/pages.py +115 -0
- ingestr/src/pipedrive/settings.py +27 -0
- ingestr/src/pipedrive/typing.py +3 -0
- ingestr/src/plusvibeai/__init__.py +335 -0
- ingestr/src/plusvibeai/helpers.py +544 -0
- ingestr/src/plusvibeai/settings.py +252 -0
- ingestr/src/quickbooks/__init__.py +117 -0
- ingestr/src/resource.py +40 -0
- ingestr/src/revenuecat/__init__.py +83 -0
- ingestr/src/revenuecat/helpers.py +237 -0
- ingestr/src/salesforce/__init__.py +156 -0
- ingestr/src/salesforce/helpers.py +64 -0
- ingestr/src/shopify/__init__.py +1 -17
- ingestr/src/smartsheets/__init__.py +82 -0
- ingestr/src/snapchat_ads/__init__.py +489 -0
- ingestr/src/snapchat_ads/client.py +72 -0
- ingestr/src/snapchat_ads/helpers.py +535 -0
- ingestr/src/socrata_source/__init__.py +83 -0
- ingestr/src/socrata_source/helpers.py +85 -0
- ingestr/src/socrata_source/settings.py +8 -0
- ingestr/src/solidgate/__init__.py +219 -0
- ingestr/src/solidgate/helpers.py +154 -0
- ingestr/src/sources.py +3132 -212
- ingestr/src/stripe_analytics/__init__.py +49 -21
- ingestr/src/stripe_analytics/helpers.py +286 -1
- ingestr/src/stripe_analytics/settings.py +62 -10
- ingestr/src/telemetry/event.py +10 -9
- ingestr/src/tiktok_ads/__init__.py +12 -6
- ingestr/src/tiktok_ads/tiktok_helpers.py +0 -1
- ingestr/src/trustpilot/__init__.py +48 -0
- ingestr/src/trustpilot/client.py +48 -0
- ingestr/src/version.py +6 -1
- ingestr/src/wise/__init__.py +68 -0
- ingestr/src/wise/client.py +63 -0
- ingestr/src/zoom/__init__.py +99 -0
- ingestr/src/zoom/helpers.py +102 -0
- ingestr/tests/unit/test_smartsheets.py +133 -0
- ingestr-0.14.104.dist-info/METADATA +563 -0
- ingestr-0.14.104.dist-info/RECORD +203 -0
- ingestr/src/appsflyer/_init_.py +0 -24
- ingestr-0.13.2.dist-info/METADATA +0 -302
- ingestr-0.13.2.dist-info/RECORD +0 -107
- {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/WHEEL +0 -0
- {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/entry_points.txt +0 -0
- {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
|
+
}
|