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,252 @@
|
|
|
1
|
+
"""PlusVibeAI source settings and constants"""
|
|
2
|
+
|
|
3
|
+
# Default start date for PlusVibeAI API requests
|
|
4
|
+
DEFAULT_START_DATE = "2020-01-01"
|
|
5
|
+
|
|
6
|
+
# PlusVibeAI API request timeout in seconds
|
|
7
|
+
REQUEST_TIMEOUT = 300
|
|
8
|
+
|
|
9
|
+
# Default page size for paginated requests
|
|
10
|
+
DEFAULT_PAGE_SIZE = 100
|
|
11
|
+
|
|
12
|
+
# Maximum page size (adjust based on API limits)
|
|
13
|
+
MAX_PAGE_SIZE = 1000
|
|
14
|
+
|
|
15
|
+
# Base API path for PlusVibeAI
|
|
16
|
+
API_BASE_PATH = "/api/v1"
|
|
17
|
+
|
|
18
|
+
# Campaign fields to retrieve from PlusVibeAI API
|
|
19
|
+
CAMPAIGN_FIELDS = (
|
|
20
|
+
# Basic Information
|
|
21
|
+
"id",
|
|
22
|
+
"camp_name",
|
|
23
|
+
"parent_camp_id",
|
|
24
|
+
"campaign_type",
|
|
25
|
+
"organization_id",
|
|
26
|
+
"workspace_id",
|
|
27
|
+
"status",
|
|
28
|
+
# Timestamps
|
|
29
|
+
"created_at",
|
|
30
|
+
"modified_at",
|
|
31
|
+
"last_lead_sent",
|
|
32
|
+
"last_paused_at_bounced",
|
|
33
|
+
# Campaign Configuration
|
|
34
|
+
"tags",
|
|
35
|
+
"template_id",
|
|
36
|
+
"email_accounts",
|
|
37
|
+
"daily_limit",
|
|
38
|
+
"interval_limit_in_min",
|
|
39
|
+
"send_priority",
|
|
40
|
+
"send_as_txt",
|
|
41
|
+
# Tracking & Settings
|
|
42
|
+
"is_emailopened_tracking",
|
|
43
|
+
"is_unsubscribed_link",
|
|
44
|
+
"exclude_ooo",
|
|
45
|
+
"is_acc_based_sending",
|
|
46
|
+
"send_risky_email",
|
|
47
|
+
"unsub_blocklist",
|
|
48
|
+
"other_email_acc",
|
|
49
|
+
"is_esp_match",
|
|
50
|
+
"stop_on_lead_replied",
|
|
51
|
+
# Bounce Settings
|
|
52
|
+
"is_pause_on_bouncerate",
|
|
53
|
+
"bounce_rate_limit",
|
|
54
|
+
"is_paused_at_bounced",
|
|
55
|
+
# Schedule
|
|
56
|
+
"schedule",
|
|
57
|
+
"first_wait_time",
|
|
58
|
+
"camp_st_date",
|
|
59
|
+
"camp_end_date",
|
|
60
|
+
# Events & Sequences
|
|
61
|
+
"events",
|
|
62
|
+
"sequences",
|
|
63
|
+
"sequence_steps",
|
|
64
|
+
"camp_emails",
|
|
65
|
+
# Lead Statistics
|
|
66
|
+
"lead_count",
|
|
67
|
+
"completed_lead_count",
|
|
68
|
+
"lead_contacted_count",
|
|
69
|
+
# Email Performance Metrics
|
|
70
|
+
"sent_count",
|
|
71
|
+
"opened_count",
|
|
72
|
+
"unique_opened_count",
|
|
73
|
+
"replied_count",
|
|
74
|
+
"bounced_count",
|
|
75
|
+
"unsubscribed_count",
|
|
76
|
+
# Reply Classification
|
|
77
|
+
"positive_reply_count",
|
|
78
|
+
"negative_reply_count",
|
|
79
|
+
"neutral_reply_count",
|
|
80
|
+
# Daily & Business Metrics
|
|
81
|
+
"email_sent_today",
|
|
82
|
+
"opportunity_val",
|
|
83
|
+
"open_rate",
|
|
84
|
+
"replied_rate",
|
|
85
|
+
# Custom Data
|
|
86
|
+
"custom_fields",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Lead fields to retrieve from PlusVibeAI API
|
|
90
|
+
LEAD_FIELDS = (
|
|
91
|
+
# Basic Information
|
|
92
|
+
"_id",
|
|
93
|
+
"organization_id",
|
|
94
|
+
"campaign_id",
|
|
95
|
+
"workspace_id",
|
|
96
|
+
# Lead Status & Progress
|
|
97
|
+
"is_completed",
|
|
98
|
+
"current_step",
|
|
99
|
+
"status",
|
|
100
|
+
"label",
|
|
101
|
+
# Email Account Info
|
|
102
|
+
"email_account_id",
|
|
103
|
+
"email_acc_name",
|
|
104
|
+
# Campaign Info
|
|
105
|
+
"camp_name",
|
|
106
|
+
# Timestamps
|
|
107
|
+
"created_at",
|
|
108
|
+
"modified_at",
|
|
109
|
+
"last_sent_at",
|
|
110
|
+
# Email Engagement Metrics
|
|
111
|
+
"sent_step",
|
|
112
|
+
"replied_count",
|
|
113
|
+
"opened_count",
|
|
114
|
+
# Email Verification
|
|
115
|
+
"is_mx",
|
|
116
|
+
"mx",
|
|
117
|
+
# Contact Information
|
|
118
|
+
"email",
|
|
119
|
+
"first_name",
|
|
120
|
+
"last_name",
|
|
121
|
+
"phone_number",
|
|
122
|
+
# Address Information
|
|
123
|
+
"address_line",
|
|
124
|
+
"city",
|
|
125
|
+
"state",
|
|
126
|
+
"country",
|
|
127
|
+
"country_code",
|
|
128
|
+
# Professional Information
|
|
129
|
+
"job_title",
|
|
130
|
+
"department",
|
|
131
|
+
"company_name",
|
|
132
|
+
"company_website",
|
|
133
|
+
"industry",
|
|
134
|
+
# Social Media
|
|
135
|
+
"linkedin_person_url",
|
|
136
|
+
"linkedin_company_url",
|
|
137
|
+
# Workflow
|
|
138
|
+
"total_steps",
|
|
139
|
+
# Bounce Information
|
|
140
|
+
"bounce_msg",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Email Account fields to retrieve from PlusVibeAI API
|
|
144
|
+
EMAIL_ACCOUNT_FIELDS = (
|
|
145
|
+
# Basic Information
|
|
146
|
+
"_id",
|
|
147
|
+
"email",
|
|
148
|
+
"status",
|
|
149
|
+
"warmup_status",
|
|
150
|
+
# Timestamps
|
|
151
|
+
"timestamp_created",
|
|
152
|
+
"timestamp_updated",
|
|
153
|
+
# Payload - nested object containing all configuration
|
|
154
|
+
"payload",
|
|
155
|
+
# Payload sub-fields (for reference, stored in payload object):
|
|
156
|
+
# - name (first_name, last_name)
|
|
157
|
+
# - warmup (limit, warmup_custom_words, warmup_signature, advanced, increment, reply_rate)
|
|
158
|
+
# - imap_host, imap_port
|
|
159
|
+
# - smtp_host, smtp_port
|
|
160
|
+
# - daily_limit, sending_gap
|
|
161
|
+
# - reply_to, custom_domain, signature
|
|
162
|
+
# - tags, cmps
|
|
163
|
+
# - analytics (health_scores, reply_rates, daily_counters)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Email fields to retrieve from PlusVibeAI API
|
|
167
|
+
EMAIL_FIELDS = (
|
|
168
|
+
# Basic Information
|
|
169
|
+
"id",
|
|
170
|
+
"message_id",
|
|
171
|
+
"is_unread",
|
|
172
|
+
# Lead Information
|
|
173
|
+
"lead",
|
|
174
|
+
"lead_id",
|
|
175
|
+
"campaign_id",
|
|
176
|
+
# From Address
|
|
177
|
+
"from_address_email",
|
|
178
|
+
"from_address_json",
|
|
179
|
+
# Subject & Content
|
|
180
|
+
"subject",
|
|
181
|
+
"content_preview",
|
|
182
|
+
"body",
|
|
183
|
+
# Headers & Metadata
|
|
184
|
+
"headers",
|
|
185
|
+
"label",
|
|
186
|
+
"thread_id",
|
|
187
|
+
"eaccount",
|
|
188
|
+
# To/CC/BCC Addresses
|
|
189
|
+
"to_address_email_list",
|
|
190
|
+
"to_address_json",
|
|
191
|
+
"cc_address_email_list",
|
|
192
|
+
"cc_address_json",
|
|
193
|
+
"bcc_address_email_list",
|
|
194
|
+
# Timestamps
|
|
195
|
+
"timestamp_created",
|
|
196
|
+
"source_modified_at",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Blocklist fields to retrieve from PlusVibeAI API
|
|
200
|
+
BLOCKLIST_FIELDS = (
|
|
201
|
+
# Basic Information
|
|
202
|
+
"_id",
|
|
203
|
+
"workspace_id",
|
|
204
|
+
"value",
|
|
205
|
+
"created_by_label",
|
|
206
|
+
# Timestamps
|
|
207
|
+
"created_at",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Webhook fields to retrieve from PlusVibeAI API
|
|
211
|
+
WEBHOOK_FIELDS = (
|
|
212
|
+
# Basic Information
|
|
213
|
+
"_id",
|
|
214
|
+
"workspace_id",
|
|
215
|
+
"org_id",
|
|
216
|
+
"url",
|
|
217
|
+
"name",
|
|
218
|
+
"secret",
|
|
219
|
+
# Configuration
|
|
220
|
+
"camp_ids",
|
|
221
|
+
"evt_types",
|
|
222
|
+
"status",
|
|
223
|
+
"integration_type",
|
|
224
|
+
# Settings
|
|
225
|
+
"ignore_ooo",
|
|
226
|
+
"ignore_automatic",
|
|
227
|
+
# Timestamps
|
|
228
|
+
"created_at",
|
|
229
|
+
"modified_at",
|
|
230
|
+
"last_run",
|
|
231
|
+
# Response Data
|
|
232
|
+
"last_resp",
|
|
233
|
+
"last_recv_resp",
|
|
234
|
+
# User Information
|
|
235
|
+
"created_by",
|
|
236
|
+
"modified_by",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Tag fields to retrieve from PlusVibeAI API
|
|
240
|
+
TAG_FIELDS = (
|
|
241
|
+
# Basic Information
|
|
242
|
+
"_id",
|
|
243
|
+
"workspace_id",
|
|
244
|
+
"org_id",
|
|
245
|
+
"name",
|
|
246
|
+
"color",
|
|
247
|
+
"description",
|
|
248
|
+
"status",
|
|
249
|
+
# Timestamps
|
|
250
|
+
"created_at",
|
|
251
|
+
"modified_at",
|
|
252
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Fetches Primer payments data."""
|
|
2
|
+
|
|
3
|
+
from typing import Iterable, Optional
|
|
4
|
+
|
|
5
|
+
import dlt
|
|
6
|
+
from dlt.common.time import ensure_pendulum_datetime
|
|
7
|
+
from dlt.common.typing import TAnyDateTime, TDataItem
|
|
8
|
+
from dlt.sources import DltResource
|
|
9
|
+
|
|
10
|
+
from .helpers import PrimerApi
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dlt.source(name="primer", max_table_nesting=0)
|
|
14
|
+
def primer_source(
|
|
15
|
+
api_key: str = dlt.secrets.value,
|
|
16
|
+
api_version: str = "2.4",
|
|
17
|
+
start_date: Optional[TAnyDateTime] = None,
|
|
18
|
+
end_date: Optional[TAnyDateTime] = None,
|
|
19
|
+
) -> Iterable[DltResource]:
|
|
20
|
+
client = PrimerApi(api_key, api_version)
|
|
21
|
+
|
|
22
|
+
start_date_obj = ensure_pendulum_datetime(start_date) if start_date else None
|
|
23
|
+
end_date_obj = ensure_pendulum_datetime(end_date) if end_date else None
|
|
24
|
+
|
|
25
|
+
@dlt.resource(selected=False)
|
|
26
|
+
def payment_ids() -> Iterable[TDataItem]:
|
|
27
|
+
"""List payment IDs from Primer API."""
|
|
28
|
+
ids = client.list_payment_ids(
|
|
29
|
+
start_date=start_date_obj,
|
|
30
|
+
end_date=end_date_obj,
|
|
31
|
+
)
|
|
32
|
+
for payment_id in ids:
|
|
33
|
+
yield {"id": payment_id}
|
|
34
|
+
|
|
35
|
+
@dlt.transformer(
|
|
36
|
+
data_from=payment_ids,
|
|
37
|
+
primary_key="id",
|
|
38
|
+
write_disposition="merge",
|
|
39
|
+
parallelized=True,
|
|
40
|
+
)
|
|
41
|
+
def payments(payment_id_item: TDataItem) -> TDataItem:
|
|
42
|
+
"""Fetch payment details in parallel."""
|
|
43
|
+
return client.get_payment(payment_id_item["id"])
|
|
44
|
+
|
|
45
|
+
return (payment_ids, payments)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Primer API client helpers"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from dlt.common.pendulum import pendulum
|
|
6
|
+
from dlt.sources.helpers import requests
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_date_params(
|
|
10
|
+
start_date: Optional[pendulum.DateTime] = None,
|
|
11
|
+
end_date: Optional[pendulum.DateTime] = None,
|
|
12
|
+
) -> Dict[str, str]:
|
|
13
|
+
params: Dict[str, str] = {}
|
|
14
|
+
if start_date:
|
|
15
|
+
params["from_date"] = start_date.start_of("day").to_iso8601_string()
|
|
16
|
+
if end_date:
|
|
17
|
+
params["to_date"] = end_date.add(days=1).start_of("day").to_iso8601_string()
|
|
18
|
+
return params
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PrimerApi:
|
|
22
|
+
"""
|
|
23
|
+
A Primer API client that handles pagination and data fetching.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
BASE_URL = "https://api.primer.io"
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
api_key: str,
|
|
31
|
+
api_version: str = "2.4",
|
|
32
|
+
) -> None:
|
|
33
|
+
self.api_key = api_key
|
|
34
|
+
self.api_version = api_version
|
|
35
|
+
|
|
36
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
37
|
+
return {
|
|
38
|
+
"X-API-KEY": self.api_key,
|
|
39
|
+
"X-API-VERSION": self.api_version,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def list_payment_ids(
|
|
43
|
+
self,
|
|
44
|
+
start_date: Optional[pendulum.DateTime] = None,
|
|
45
|
+
end_date: Optional[pendulum.DateTime] = None,
|
|
46
|
+
) -> List[str]:
|
|
47
|
+
"""List all payment IDs from the payments endpoint."""
|
|
48
|
+
url = f"{self.BASE_URL}/payments"
|
|
49
|
+
params: Dict[str, Any] = build_date_params(start_date, end_date)
|
|
50
|
+
|
|
51
|
+
payment_ids: List[str] = []
|
|
52
|
+
|
|
53
|
+
while True:
|
|
54
|
+
response = requests.get(url, params=params, headers=self._get_headers())
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
|
|
57
|
+
json_data = response.json()
|
|
58
|
+
data = json_data.get("data", [])
|
|
59
|
+
|
|
60
|
+
if len(data) == 0:
|
|
61
|
+
break
|
|
62
|
+
|
|
63
|
+
for item in data:
|
|
64
|
+
payment_ids.append(item["id"])
|
|
65
|
+
|
|
66
|
+
next_cursor = json_data.get("nextCursor")
|
|
67
|
+
if not next_cursor:
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
params["cursor"] = next_cursor
|
|
71
|
+
|
|
72
|
+
return payment_ids
|
|
73
|
+
|
|
74
|
+
def get_payment(self, payment_id: str) -> Dict[str, Any]:
|
|
75
|
+
"""Get payment by ID."""
|
|
76
|
+
url = f"{self.BASE_URL}/payments/{payment_id}"
|
|
77
|
+
response = requests.get(url, headers=self._get_headers())
|
|
78
|
+
response.raise_for_status()
|
|
79
|
+
return response.json()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""QuickBooks source built on top of python-quickbooks."""
|
|
2
|
+
|
|
3
|
+
from typing import Iterable, Iterator, List, Optional
|
|
4
|
+
|
|
5
|
+
import dlt
|
|
6
|
+
import pendulum
|
|
7
|
+
from dlt.common.time import ensure_pendulum_datetime
|
|
8
|
+
from dlt.common.typing import TDataItem
|
|
9
|
+
from dlt.sources import DltResource
|
|
10
|
+
from intuitlib.client import AuthClient # type: ignore
|
|
11
|
+
|
|
12
|
+
from quickbooks import QuickBooks # type: ignore
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dlt.source(name="quickbooks", max_table_nesting=0)
|
|
16
|
+
def quickbooks_source(
|
|
17
|
+
company_id: str,
|
|
18
|
+
start_date: pendulum.DateTime,
|
|
19
|
+
object: str,
|
|
20
|
+
end_date: pendulum.DateTime | None,
|
|
21
|
+
client_id: str,
|
|
22
|
+
client_secret: str,
|
|
23
|
+
refresh_token: str,
|
|
24
|
+
environment: str = "production",
|
|
25
|
+
minor_version: Optional[str] = None,
|
|
26
|
+
) -> Iterable[DltResource]:
|
|
27
|
+
"""Create dlt resources for QuickBooks objects.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
company_id: str
|
|
32
|
+
QuickBooks company id (realm id).
|
|
33
|
+
client_id: str
|
|
34
|
+
OAuth client id.
|
|
35
|
+
client_secret: str
|
|
36
|
+
OAuth client secret.
|
|
37
|
+
refresh_token: str
|
|
38
|
+
OAuth refresh token.
|
|
39
|
+
access_token: Optional[str]
|
|
40
|
+
Optional access token. If not provided the library will refresh using the
|
|
41
|
+
provided refresh token.
|
|
42
|
+
environment: str
|
|
43
|
+
Either ``"production"`` or ``"sandbox"``.
|
|
44
|
+
minor_version: Optional[int]
|
|
45
|
+
QuickBooks API minor version if needed.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
auth_client = AuthClient(
|
|
49
|
+
client_id=client_id,
|
|
50
|
+
client_secret=client_secret,
|
|
51
|
+
environment=environment,
|
|
52
|
+
# redirect_uri is not used since we authenticate using refresh token which skips the step of redirect callback.
|
|
53
|
+
# as redirect_uri is required param, we are passing empty string.
|
|
54
|
+
redirect_uri="",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# https://help.developer.intuit.com/s/article/Validity-of-Refresh-Token
|
|
58
|
+
client = QuickBooks(
|
|
59
|
+
auth_client=auth_client,
|
|
60
|
+
refresh_token=refresh_token,
|
|
61
|
+
company_id=company_id,
|
|
62
|
+
minorversion=minor_version,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def fetch_object(
|
|
66
|
+
obj_name: str,
|
|
67
|
+
updated_at: dlt.sources.incremental[str] = dlt.sources.incremental(
|
|
68
|
+
"lastupdatedtime",
|
|
69
|
+
initial_value=start_date, # type: ignore
|
|
70
|
+
end_value=end_date, # type: ignore
|
|
71
|
+
range_start="closed",
|
|
72
|
+
range_end="closed",
|
|
73
|
+
allow_external_schedulers=True,
|
|
74
|
+
),
|
|
75
|
+
) -> Iterator[List[TDataItem]]:
|
|
76
|
+
start_pos = 1
|
|
77
|
+
|
|
78
|
+
end_dt = updated_at.end_value or pendulum.now(tz="UTC")
|
|
79
|
+
start_dt = ensure_pendulum_datetime(str(updated_at.last_value)).in_tz("UTC")
|
|
80
|
+
|
|
81
|
+
start_str = start_dt.isoformat()
|
|
82
|
+
end_str = end_dt.isoformat()
|
|
83
|
+
|
|
84
|
+
where_clause = f"WHERE MetaData.LastUpdatedTime >= '{start_str}' AND MetaData.LastUpdatedTime < '{end_str}'"
|
|
85
|
+
while True:
|
|
86
|
+
query = (
|
|
87
|
+
f"SELECT * FROM {obj_name} {where_clause} "
|
|
88
|
+
f"ORDERBY MetaData.LastUpdatedTime ASC STARTPOSITION {start_pos} MAXRESULTS 1000"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
result = client.query(query)
|
|
92
|
+
|
|
93
|
+
items = result.get("QueryResponse", {}).get(obj_name.capitalize(), [])
|
|
94
|
+
if not items:
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
for item in items:
|
|
98
|
+
if item.get("MetaData") and item["MetaData"].get("LastUpdatedTime"):
|
|
99
|
+
item["lastupdatedtime"] = ensure_pendulum_datetime(
|
|
100
|
+
item["MetaData"]["LastUpdatedTime"]
|
|
101
|
+
)
|
|
102
|
+
item["id"] = item["Id"]
|
|
103
|
+
del item["Id"]
|
|
104
|
+
|
|
105
|
+
yield item
|
|
106
|
+
|
|
107
|
+
if len(items) < 1000:
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
start_pos += 1000
|
|
111
|
+
|
|
112
|
+
yield dlt.resource(
|
|
113
|
+
fetch_object,
|
|
114
|
+
name=object.lower(),
|
|
115
|
+
write_disposition="merge",
|
|
116
|
+
primary_key="id",
|
|
117
|
+
)(object)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from typing import Iterable
|
|
2
|
+
|
|
3
|
+
import dlt
|
|
4
|
+
import pendulum
|
|
5
|
+
from dlt.common.typing import TDataItem
|
|
6
|
+
from dlt.sources import DltResource
|
|
7
|
+
from pendulum import Date
|
|
8
|
+
|
|
9
|
+
from .helpers import (
|
|
10
|
+
BASE_URL,
|
|
11
|
+
LEVEL_ID_FIELDS,
|
|
12
|
+
RedditAdsAPI,
|
|
13
|
+
RedditAdsReportAPI,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dlt.source(max_table_nesting=0)
|
|
18
|
+
def reddit_ads_source(
|
|
19
|
+
access_token: str,
|
|
20
|
+
account_ids: list[str],
|
|
21
|
+
) -> list[DltResource]:
|
|
22
|
+
reddit_api = RedditAdsAPI(access_token=access_token)
|
|
23
|
+
|
|
24
|
+
@dlt.resource(write_disposition="replace", primary_key="id")
|
|
25
|
+
def accounts() -> Iterable[TDataItem]:
|
|
26
|
+
url = f"{BASE_URL}/accounts"
|
|
27
|
+
account_id_set = set(account_ids)
|
|
28
|
+
for page in reddit_api.fetch_pages(url):
|
|
29
|
+
filtered = [item for item in page if str(item.get("id")) in account_id_set]
|
|
30
|
+
if filtered:
|
|
31
|
+
yield filtered
|
|
32
|
+
|
|
33
|
+
@dlt.resource(write_disposition="replace", primary_key="id")
|
|
34
|
+
def campaigns() -> Iterable[TDataItem]:
|
|
35
|
+
for account_id in account_ids:
|
|
36
|
+
url = f"{BASE_URL}/accounts/{account_id}/campaigns"
|
|
37
|
+
for page in reddit_api.fetch_pages(url):
|
|
38
|
+
for item in page:
|
|
39
|
+
item["account_id"] = account_id
|
|
40
|
+
yield page
|
|
41
|
+
|
|
42
|
+
@dlt.resource(write_disposition="replace", primary_key="id")
|
|
43
|
+
def ad_groups() -> Iterable[TDataItem]:
|
|
44
|
+
for account_id in account_ids:
|
|
45
|
+
url = f"{BASE_URL}/accounts/{account_id}/ad_groups"
|
|
46
|
+
for page in reddit_api.fetch_pages(url):
|
|
47
|
+
for item in page:
|
|
48
|
+
item["account_id"] = account_id
|
|
49
|
+
yield page
|
|
50
|
+
|
|
51
|
+
@dlt.resource(write_disposition="replace", primary_key="id")
|
|
52
|
+
def ads() -> Iterable[TDataItem]:
|
|
53
|
+
for account_id in account_ids:
|
|
54
|
+
url = f"{BASE_URL}/accounts/{account_id}/ads"
|
|
55
|
+
for page in reddit_api.fetch_pages(url):
|
|
56
|
+
for item in page:
|
|
57
|
+
item["account_id"] = account_id
|
|
58
|
+
yield page
|
|
59
|
+
|
|
60
|
+
@dlt.resource(write_disposition="replace", primary_key="id")
|
|
61
|
+
def posts() -> Iterable[TDataItem]:
|
|
62
|
+
for account_id in account_ids:
|
|
63
|
+
url = f"{BASE_URL}/accounts/{account_id}/posts"
|
|
64
|
+
for page in reddit_api.fetch_pages(url):
|
|
65
|
+
for item in page:
|
|
66
|
+
item["account_id"] = account_id
|
|
67
|
+
yield page
|
|
68
|
+
|
|
69
|
+
@dlt.resource(write_disposition="replace", primary_key="id")
|
|
70
|
+
def custom_audiences() -> Iterable[TDataItem]:
|
|
71
|
+
for account_id in account_ids:
|
|
72
|
+
url = f"{BASE_URL}/accounts/{account_id}/custom_audiences"
|
|
73
|
+
for page in reddit_api.fetch_pages(url):
|
|
74
|
+
for item in page:
|
|
75
|
+
item["account_id"] = account_id
|
|
76
|
+
yield page
|
|
77
|
+
|
|
78
|
+
@dlt.resource(write_disposition="replace", primary_key="id")
|
|
79
|
+
def saved_audiences() -> Iterable[TDataItem]:
|
|
80
|
+
for account_id in account_ids:
|
|
81
|
+
url = f"{BASE_URL}/accounts/{account_id}/saved_audiences"
|
|
82
|
+
for page in reddit_api.fetch_pages(url):
|
|
83
|
+
for item in page:
|
|
84
|
+
item["account_id"] = account_id
|
|
85
|
+
yield page
|
|
86
|
+
|
|
87
|
+
@dlt.resource(write_disposition="replace", primary_key="id")
|
|
88
|
+
def pixels() -> Iterable[TDataItem]:
|
|
89
|
+
for account_id in account_ids:
|
|
90
|
+
url = f"{BASE_URL}/accounts/{account_id}/pixels"
|
|
91
|
+
for page in reddit_api.fetch_pages(url):
|
|
92
|
+
for item in page:
|
|
93
|
+
item["account_id"] = account_id
|
|
94
|
+
yield page
|
|
95
|
+
|
|
96
|
+
@dlt.resource(write_disposition="replace", primary_key="id")
|
|
97
|
+
def funding_instruments() -> Iterable[TDataItem]:
|
|
98
|
+
for account_id in account_ids:
|
|
99
|
+
url = f"{BASE_URL}/accounts/{account_id}/funding_instruments"
|
|
100
|
+
for page in reddit_api.fetch_pages(url):
|
|
101
|
+
for item in page:
|
|
102
|
+
item["account_id"] = account_id
|
|
103
|
+
yield page
|
|
104
|
+
|
|
105
|
+
return [
|
|
106
|
+
accounts,
|
|
107
|
+
campaigns,
|
|
108
|
+
ad_groups,
|
|
109
|
+
ads,
|
|
110
|
+
posts,
|
|
111
|
+
custom_audiences,
|
|
112
|
+
saved_audiences,
|
|
113
|
+
pixels,
|
|
114
|
+
funding_instruments,
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dlt.source(max_table_nesting=0)
|
|
119
|
+
def reddit_ads_analytics_source(
|
|
120
|
+
access_token: str,
|
|
121
|
+
account_ids: list[str],
|
|
122
|
+
level: str,
|
|
123
|
+
breakdowns: list[str],
|
|
124
|
+
metrics: list[str],
|
|
125
|
+
start_date: Date,
|
|
126
|
+
end_date: Date | None,
|
|
127
|
+
) -> DltResource:
|
|
128
|
+
level_id_field = LEVEL_ID_FIELDS.get(level, "account_id")
|
|
129
|
+
primary_key = [level_id_field] + breakdowns
|
|
130
|
+
|
|
131
|
+
has_date_breakdown = "date" in breakdowns
|
|
132
|
+
|
|
133
|
+
if has_date_breakdown:
|
|
134
|
+
|
|
135
|
+
@dlt.resource(write_disposition="merge", primary_key=primary_key)
|
|
136
|
+
def custom_reports(
|
|
137
|
+
dateTime=dlt.sources.incremental(
|
|
138
|
+
"date",
|
|
139
|
+
initial_value=start_date,
|
|
140
|
+
end_value=end_date,
|
|
141
|
+
range_start="closed",
|
|
142
|
+
range_end="closed",
|
|
143
|
+
),
|
|
144
|
+
) -> Iterable[TDataItem]:
|
|
145
|
+
report_api = RedditAdsReportAPI(
|
|
146
|
+
access_token=access_token,
|
|
147
|
+
account_ids=account_ids,
|
|
148
|
+
level=level,
|
|
149
|
+
breakdowns=breakdowns,
|
|
150
|
+
metrics=metrics,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
actual_end = (
|
|
154
|
+
dateTime.end_value
|
|
155
|
+
if dateTime.end_value is not None
|
|
156
|
+
else pendulum.now().date()
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
yield from report_api.fetch_report(
|
|
160
|
+
start_date=dateTime.last_value,
|
|
161
|
+
end_date=actual_end,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
else:
|
|
165
|
+
|
|
166
|
+
@dlt.resource(write_disposition="merge", primary_key=primary_key)
|
|
167
|
+
def custom_reports() -> Iterable[TDataItem]:
|
|
168
|
+
report_api = RedditAdsReportAPI(
|
|
169
|
+
access_token=access_token,
|
|
170
|
+
account_ids=account_ids,
|
|
171
|
+
level=level,
|
|
172
|
+
breakdowns=breakdowns,
|
|
173
|
+
metrics=metrics,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
actual_end = end_date if end_date is not None else pendulum.now().date()
|
|
177
|
+
|
|
178
|
+
yield from report_api.fetch_report(
|
|
179
|
+
start_date=start_date,
|
|
180
|
+
end_date=actual_end,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return custom_reports
|