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.
- omniload/conftest.py +72 -0
- omniload/main.py +810 -0
- omniload/src/.gitignore +10 -0
- omniload/src/adjust/__init__.py +108 -0
- omniload/src/adjust/adjust_helpers.py +122 -0
- omniload/src/airtable/__init__.py +84 -0
- omniload/src/allium/__init__.py +128 -0
- omniload/src/anthropic/__init__.py +277 -0
- omniload/src/anthropic/helpers.py +525 -0
- omniload/src/applovin/__init__.py +316 -0
- omniload/src/applovin_max/__init__.py +117 -0
- omniload/src/appsflyer/__init__.py +325 -0
- omniload/src/appsflyer/client.py +110 -0
- omniload/src/appstore/__init__.py +142 -0
- omniload/src/appstore/client.py +126 -0
- omniload/src/appstore/errors.py +15 -0
- omniload/src/appstore/models.py +117 -0
- omniload/src/appstore/resources.py +179 -0
- omniload/src/arrow/__init__.py +81 -0
- omniload/src/asana_source/__init__.py +281 -0
- omniload/src/asana_source/helpers.py +30 -0
- omniload/src/asana_source/settings.py +158 -0
- omniload/src/attio/__init__.py +102 -0
- omniload/src/attio/helpers.py +65 -0
- omniload/src/blob.py +95 -0
- omniload/src/bruin/__init__.py +76 -0
- omniload/src/chess/__init__.py +180 -0
- omniload/src/chess/helpers.py +35 -0
- omniload/src/chess/settings.py +18 -0
- omniload/src/clickup/__init__.py +85 -0
- omniload/src/clickup/helpers.py +47 -0
- omniload/src/collector/spinner.py +43 -0
- omniload/src/couchbase_source/__init__.py +118 -0
- omniload/src/couchbase_source/helpers.py +135 -0
- omniload/src/cursor/__init__.py +83 -0
- omniload/src/cursor/helpers.py +188 -0
- omniload/src/customer_io/__init__.py +486 -0
- omniload/src/customer_io/helpers.py +530 -0
- omniload/src/destinations.py +982 -0
- omniload/src/docebo/__init__.py +589 -0
- omniload/src/docebo/client.py +435 -0
- omniload/src/docebo/helpers.py +97 -0
- omniload/src/dune/__init__.py +104 -0
- omniload/src/dune/helpers.py +108 -0
- omniload/src/dynamodb/__init__.py +86 -0
- omniload/src/elasticsearch/__init__.py +80 -0
- omniload/src/elasticsearch/helpers.py +141 -0
- omniload/src/errors.py +26 -0
- omniload/src/facebook_ads/__init__.py +403 -0
- omniload/src/facebook_ads/exceptions.py +19 -0
- omniload/src/facebook_ads/helpers.py +296 -0
- omniload/src/facebook_ads/settings.py +224 -0
- omniload/src/facebook_ads/utils.py +53 -0
- omniload/src/factory.py +305 -0
- omniload/src/filesystem/__init__.py +133 -0
- omniload/src/filesystem/helpers.py +114 -0
- omniload/src/filesystem/readers.py +187 -0
- omniload/src/filters.py +62 -0
- omniload/src/fireflies/__init__.py +151 -0
- omniload/src/fireflies/helpers.py +753 -0
- omniload/src/fluxx/__init__.py +10013 -0
- omniload/src/fluxx/helpers.py +233 -0
- omniload/src/frankfurter/__init__.py +157 -0
- omniload/src/frankfurter/helpers.py +48 -0
- omniload/src/freshdesk/__init__.py +103 -0
- omniload/src/freshdesk/freshdesk_client.py +151 -0
- omniload/src/freshdesk/settings.py +23 -0
- omniload/src/fundraiseup/__init__.py +95 -0
- omniload/src/fundraiseup/client.py +81 -0
- omniload/src/github/__init__.py +202 -0
- omniload/src/github/helpers.py +207 -0
- omniload/src/github/queries.py +129 -0
- omniload/src/github/settings.py +24 -0
- omniload/src/google_ads/__init__.py +198 -0
- omniload/src/google_ads/field.py +17 -0
- omniload/src/google_ads/metrics.py +254 -0
- omniload/src/google_ads/predicates.py +37 -0
- omniload/src/google_ads/reports.py +411 -0
- omniload/src/google_ads/test_google_ads.py +184 -0
- omniload/src/google_analytics/__init__.py +144 -0
- omniload/src/google_analytics/helpers.py +312 -0
- omniload/src/google_sheets/README.md +95 -0
- omniload/src/google_sheets/__init__.py +166 -0
- omniload/src/google_sheets/helpers/__init__.py +15 -0
- omniload/src/google_sheets/helpers/api_calls.py +160 -0
- omniload/src/google_sheets/helpers/data_processing.py +316 -0
- omniload/src/gorgias/__init__.py +595 -0
- omniload/src/gorgias/helpers.py +166 -0
- omniload/src/hostaway/__init__.py +302 -0
- omniload/src/hostaway/client.py +288 -0
- omniload/src/http/__init__.py +38 -0
- omniload/src/http/readers.py +146 -0
- omniload/src/http_client.py +24 -0
- omniload/src/hubspot/__init__.py +800 -0
- omniload/src/hubspot/helpers.py +417 -0
- omniload/src/hubspot/settings.py +329 -0
- omniload/src/indeed/__init__.py +153 -0
- omniload/src/indeed/helpers.py +228 -0
- omniload/src/influxdb/__init__.py +46 -0
- omniload/src/influxdb/client.py +34 -0
- omniload/src/intercom/__init__.py +142 -0
- omniload/src/intercom/helpers.py +674 -0
- omniload/src/intercom/settings.py +279 -0
- omniload/src/isoc_pulse/__init__.py +159 -0
- omniload/src/jira_source/__init__.py +377 -0
- omniload/src/jira_source/helpers.py +510 -0
- omniload/src/jira_source/settings.py +184 -0
- omniload/src/kafka/__init__.py +120 -0
- omniload/src/kafka/helpers.py +241 -0
- omniload/src/kinesis/__init__.py +153 -0
- omniload/src/kinesis/helpers.py +96 -0
- omniload/src/klaviyo/__init__.py +237 -0
- omniload/src/klaviyo/client.py +212 -0
- omniload/src/klaviyo/helpers.py +19 -0
- omniload/src/linear/__init__.py +634 -0
- omniload/src/linear/helpers.py +111 -0
- omniload/src/linkedin_ads/__init__.py +266 -0
- omniload/src/linkedin_ads/dimension_time_enum.py +17 -0
- omniload/src/linkedin_ads/helpers.py +246 -0
- omniload/src/loader.py +69 -0
- omniload/src/mailchimp/__init__.py +126 -0
- omniload/src/mailchimp/helpers.py +226 -0
- omniload/src/mailchimp/settings.py +164 -0
- omniload/src/masking.py +344 -0
- omniload/src/mixpanel/__init__.py +62 -0
- omniload/src/mixpanel/client.py +104 -0
- omniload/src/monday/__init__.py +246 -0
- omniload/src/monday/helpers.py +392 -0
- omniload/src/monday/settings.py +325 -0
- omniload/src/mongodb/__init__.py +281 -0
- omniload/src/mongodb/helpers.py +975 -0
- omniload/src/notion/__init__.py +69 -0
- omniload/src/notion/helpers/__init__.py +14 -0
- omniload/src/notion/helpers/client.py +178 -0
- omniload/src/notion/helpers/database.py +92 -0
- omniload/src/notion/settings.py +17 -0
- omniload/src/partition.py +32 -0
- omniload/src/personio/__init__.py +345 -0
- omniload/src/personio/helpers.py +100 -0
- omniload/src/phantombuster/__init__.py +65 -0
- omniload/src/phantombuster/client.py +87 -0
- omniload/src/pinterest/__init__.py +82 -0
- omniload/src/pipedrive/__init__.py +212 -0
- omniload/src/pipedrive/helpers/__init__.py +37 -0
- omniload/src/pipedrive/helpers/custom_fields_munger.py +116 -0
- omniload/src/pipedrive/helpers/pages.py +129 -0
- omniload/src/pipedrive/settings.py +41 -0
- omniload/src/pipedrive/typing.py +17 -0
- omniload/src/plusvibeai/__init__.py +335 -0
- omniload/src/plusvibeai/helpers.py +544 -0
- omniload/src/plusvibeai/settings.py +252 -0
- omniload/src/primer/__init__.py +45 -0
- omniload/src/primer/helpers.py +79 -0
- omniload/src/quickbooks/__init__.py +117 -0
- omniload/src/reddit_ads/__init__.py +183 -0
- omniload/src/reddit_ads/helpers.py +232 -0
- omniload/src/resource.py +40 -0
- omniload/src/revenuecat/__init__.py +83 -0
- omniload/src/revenuecat/helpers.py +237 -0
- omniload/src/salesforce/__init__.py +170 -0
- omniload/src/salesforce/helpers.py +78 -0
- omniload/src/shopify/__init__.py +1953 -0
- omniload/src/shopify/exceptions.py +17 -0
- omniload/src/shopify/helpers.py +202 -0
- omniload/src/shopify/settings.py +19 -0
- omniload/src/slack/__init__.py +290 -0
- omniload/src/slack/helpers.py +218 -0
- omniload/src/slack/settings.py +36 -0
- omniload/src/smartsheets/__init__.py +82 -0
- omniload/src/snapchat_ads/__init__.py +455 -0
- omniload/src/snapchat_ads/client.py +72 -0
- omniload/src/snapchat_ads/helpers.py +630 -0
- omniload/src/snapchat_ads/settings.py +130 -0
- omniload/src/socrata_source/__init__.py +83 -0
- omniload/src/socrata_source/helpers.py +85 -0
- omniload/src/socrata_source/settings.py +8 -0
- omniload/src/solidgate/__init__.py +219 -0
- omniload/src/solidgate/helpers.py +154 -0
- omniload/src/sources.py +5408 -0
- omniload/src/sql_database/__init__.py +0 -0
- omniload/src/sql_database/callbacks.py +66 -0
- omniload/src/stripe_analytics/__init__.py +183 -0
- omniload/src/stripe_analytics/helpers.py +386 -0
- omniload/src/stripe_analytics/settings.py +80 -0
- omniload/src/table_definition.py +15 -0
- omniload/src/testdata/fakebqcredentials.json +14 -0
- omniload/src/tiktok_ads/__init__.py +150 -0
- omniload/src/tiktok_ads/tiktok_helpers.py +130 -0
- omniload/src/time.py +11 -0
- omniload/src/trustpilot/__init__.py +48 -0
- omniload/src/trustpilot/client.py +48 -0
- omniload/src/version.py +6 -0
- omniload/src/wise/__init__.py +68 -0
- omniload/src/wise/client.py +63 -0
- omniload/src/zendesk/__init__.py +480 -0
- omniload/src/zendesk/helpers/__init__.py +39 -0
- omniload/src/zendesk/helpers/api_helpers.py +119 -0
- omniload/src/zendesk/helpers/credentials.py +68 -0
- omniload/src/zendesk/helpers/talk_api.py +132 -0
- omniload/src/zendesk/settings.py +71 -0
- omniload/src/zoom/__init__.py +99 -0
- omniload/src/zoom/helpers.py +102 -0
- omniload/testdata/.gitignore +2 -0
- omniload/testdata/create_replace.csv +21 -0
- omniload/testdata/delete_insert_expected.csv +6 -0
- omniload/testdata/delete_insert_part1.csv +5 -0
- omniload/testdata/delete_insert_part2.csv +6 -0
- omniload/testdata/merge_expected.csv +5 -0
- omniload/testdata/merge_part1.csv +4 -0
- omniload/testdata/merge_part2.csv +5 -0
- omniload/tests/unit/test_smartsheets.py +133 -0
- omniload-0.0.0.dev0.dist-info/METADATA +439 -0
- omniload-0.0.0.dev0.dist-info/RECORD +218 -0
- omniload-0.0.0.dev0.dist-info/WHEEL +4 -0
- omniload-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- omniload-0.0.0.dev0.dist-info/licenses/LICENSE.Apache-2.0 +201 -0
- omniload-0.0.0.dev0.dist-info/licenses/LICENSE.md +21 -0
- 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
|
+
}
|