omniload 0.0.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. omniload/conftest.py +72 -0
  2. omniload/main.py +810 -0
  3. omniload/src/.gitignore +10 -0
  4. omniload/src/adjust/__init__.py +108 -0
  5. omniload/src/adjust/adjust_helpers.py +122 -0
  6. omniload/src/airtable/__init__.py +84 -0
  7. omniload/src/allium/__init__.py +128 -0
  8. omniload/src/anthropic/__init__.py +277 -0
  9. omniload/src/anthropic/helpers.py +525 -0
  10. omniload/src/applovin/__init__.py +316 -0
  11. omniload/src/applovin_max/__init__.py +117 -0
  12. omniload/src/appsflyer/__init__.py +325 -0
  13. omniload/src/appsflyer/client.py +110 -0
  14. omniload/src/appstore/__init__.py +142 -0
  15. omniload/src/appstore/client.py +126 -0
  16. omniload/src/appstore/errors.py +15 -0
  17. omniload/src/appstore/models.py +117 -0
  18. omniload/src/appstore/resources.py +179 -0
  19. omniload/src/arrow/__init__.py +81 -0
  20. omniload/src/asana_source/__init__.py +281 -0
  21. omniload/src/asana_source/helpers.py +30 -0
  22. omniload/src/asana_source/settings.py +158 -0
  23. omniload/src/attio/__init__.py +102 -0
  24. omniload/src/attio/helpers.py +65 -0
  25. omniload/src/blob.py +95 -0
  26. omniload/src/bruin/__init__.py +76 -0
  27. omniload/src/chess/__init__.py +180 -0
  28. omniload/src/chess/helpers.py +35 -0
  29. omniload/src/chess/settings.py +18 -0
  30. omniload/src/clickup/__init__.py +85 -0
  31. omniload/src/clickup/helpers.py +47 -0
  32. omniload/src/collector/spinner.py +43 -0
  33. omniload/src/couchbase_source/__init__.py +118 -0
  34. omniload/src/couchbase_source/helpers.py +135 -0
  35. omniload/src/cursor/__init__.py +83 -0
  36. omniload/src/cursor/helpers.py +188 -0
  37. omniload/src/customer_io/__init__.py +486 -0
  38. omniload/src/customer_io/helpers.py +530 -0
  39. omniload/src/destinations.py +982 -0
  40. omniload/src/docebo/__init__.py +589 -0
  41. omniload/src/docebo/client.py +435 -0
  42. omniload/src/docebo/helpers.py +97 -0
  43. omniload/src/dune/__init__.py +104 -0
  44. omniload/src/dune/helpers.py +108 -0
  45. omniload/src/dynamodb/__init__.py +86 -0
  46. omniload/src/elasticsearch/__init__.py +80 -0
  47. omniload/src/elasticsearch/helpers.py +141 -0
  48. omniload/src/errors.py +26 -0
  49. omniload/src/facebook_ads/__init__.py +403 -0
  50. omniload/src/facebook_ads/exceptions.py +19 -0
  51. omniload/src/facebook_ads/helpers.py +296 -0
  52. omniload/src/facebook_ads/settings.py +224 -0
  53. omniload/src/facebook_ads/utils.py +53 -0
  54. omniload/src/factory.py +305 -0
  55. omniload/src/filesystem/__init__.py +133 -0
  56. omniload/src/filesystem/helpers.py +114 -0
  57. omniload/src/filesystem/readers.py +187 -0
  58. omniload/src/filters.py +62 -0
  59. omniload/src/fireflies/__init__.py +151 -0
  60. omniload/src/fireflies/helpers.py +753 -0
  61. omniload/src/fluxx/__init__.py +10013 -0
  62. omniload/src/fluxx/helpers.py +233 -0
  63. omniload/src/frankfurter/__init__.py +157 -0
  64. omniload/src/frankfurter/helpers.py +48 -0
  65. omniload/src/freshdesk/__init__.py +103 -0
  66. omniload/src/freshdesk/freshdesk_client.py +151 -0
  67. omniload/src/freshdesk/settings.py +23 -0
  68. omniload/src/fundraiseup/__init__.py +95 -0
  69. omniload/src/fundraiseup/client.py +81 -0
  70. omniload/src/github/__init__.py +202 -0
  71. omniload/src/github/helpers.py +207 -0
  72. omniload/src/github/queries.py +129 -0
  73. omniload/src/github/settings.py +24 -0
  74. omniload/src/google_ads/__init__.py +198 -0
  75. omniload/src/google_ads/field.py +17 -0
  76. omniload/src/google_ads/metrics.py +254 -0
  77. omniload/src/google_ads/predicates.py +37 -0
  78. omniload/src/google_ads/reports.py +411 -0
  79. omniload/src/google_ads/test_google_ads.py +184 -0
  80. omniload/src/google_analytics/__init__.py +144 -0
  81. omniload/src/google_analytics/helpers.py +312 -0
  82. omniload/src/google_sheets/README.md +95 -0
  83. omniload/src/google_sheets/__init__.py +166 -0
  84. omniload/src/google_sheets/helpers/__init__.py +15 -0
  85. omniload/src/google_sheets/helpers/api_calls.py +160 -0
  86. omniload/src/google_sheets/helpers/data_processing.py +316 -0
  87. omniload/src/gorgias/__init__.py +595 -0
  88. omniload/src/gorgias/helpers.py +166 -0
  89. omniload/src/hostaway/__init__.py +302 -0
  90. omniload/src/hostaway/client.py +288 -0
  91. omniload/src/http/__init__.py +38 -0
  92. omniload/src/http/readers.py +146 -0
  93. omniload/src/http_client.py +24 -0
  94. omniload/src/hubspot/__init__.py +800 -0
  95. omniload/src/hubspot/helpers.py +417 -0
  96. omniload/src/hubspot/settings.py +329 -0
  97. omniload/src/indeed/__init__.py +153 -0
  98. omniload/src/indeed/helpers.py +228 -0
  99. omniload/src/influxdb/__init__.py +46 -0
  100. omniload/src/influxdb/client.py +34 -0
  101. omniload/src/intercom/__init__.py +142 -0
  102. omniload/src/intercom/helpers.py +674 -0
  103. omniload/src/intercom/settings.py +279 -0
  104. omniload/src/isoc_pulse/__init__.py +159 -0
  105. omniload/src/jira_source/__init__.py +377 -0
  106. omniload/src/jira_source/helpers.py +510 -0
  107. omniload/src/jira_source/settings.py +184 -0
  108. omniload/src/kafka/__init__.py +120 -0
  109. omniload/src/kafka/helpers.py +241 -0
  110. omniload/src/kinesis/__init__.py +153 -0
  111. omniload/src/kinesis/helpers.py +96 -0
  112. omniload/src/klaviyo/__init__.py +237 -0
  113. omniload/src/klaviyo/client.py +212 -0
  114. omniload/src/klaviyo/helpers.py +19 -0
  115. omniload/src/linear/__init__.py +634 -0
  116. omniload/src/linear/helpers.py +111 -0
  117. omniload/src/linkedin_ads/__init__.py +266 -0
  118. omniload/src/linkedin_ads/dimension_time_enum.py +17 -0
  119. omniload/src/linkedin_ads/helpers.py +246 -0
  120. omniload/src/loader.py +69 -0
  121. omniload/src/mailchimp/__init__.py +126 -0
  122. omniload/src/mailchimp/helpers.py +226 -0
  123. omniload/src/mailchimp/settings.py +164 -0
  124. omniload/src/masking.py +344 -0
  125. omniload/src/mixpanel/__init__.py +62 -0
  126. omniload/src/mixpanel/client.py +104 -0
  127. omniload/src/monday/__init__.py +246 -0
  128. omniload/src/monday/helpers.py +392 -0
  129. omniload/src/monday/settings.py +325 -0
  130. omniload/src/mongodb/__init__.py +281 -0
  131. omniload/src/mongodb/helpers.py +975 -0
  132. omniload/src/notion/__init__.py +69 -0
  133. omniload/src/notion/helpers/__init__.py +14 -0
  134. omniload/src/notion/helpers/client.py +178 -0
  135. omniload/src/notion/helpers/database.py +92 -0
  136. omniload/src/notion/settings.py +17 -0
  137. omniload/src/partition.py +32 -0
  138. omniload/src/personio/__init__.py +345 -0
  139. omniload/src/personio/helpers.py +100 -0
  140. omniload/src/phantombuster/__init__.py +65 -0
  141. omniload/src/phantombuster/client.py +87 -0
  142. omniload/src/pinterest/__init__.py +82 -0
  143. omniload/src/pipedrive/__init__.py +212 -0
  144. omniload/src/pipedrive/helpers/__init__.py +37 -0
  145. omniload/src/pipedrive/helpers/custom_fields_munger.py +116 -0
  146. omniload/src/pipedrive/helpers/pages.py +129 -0
  147. omniload/src/pipedrive/settings.py +41 -0
  148. omniload/src/pipedrive/typing.py +17 -0
  149. omniload/src/plusvibeai/__init__.py +335 -0
  150. omniload/src/plusvibeai/helpers.py +544 -0
  151. omniload/src/plusvibeai/settings.py +252 -0
  152. omniload/src/primer/__init__.py +45 -0
  153. omniload/src/primer/helpers.py +79 -0
  154. omniload/src/quickbooks/__init__.py +117 -0
  155. omniload/src/reddit_ads/__init__.py +183 -0
  156. omniload/src/reddit_ads/helpers.py +232 -0
  157. omniload/src/resource.py +40 -0
  158. omniload/src/revenuecat/__init__.py +83 -0
  159. omniload/src/revenuecat/helpers.py +237 -0
  160. omniload/src/salesforce/__init__.py +170 -0
  161. omniload/src/salesforce/helpers.py +78 -0
  162. omniload/src/shopify/__init__.py +1953 -0
  163. omniload/src/shopify/exceptions.py +17 -0
  164. omniload/src/shopify/helpers.py +202 -0
  165. omniload/src/shopify/settings.py +19 -0
  166. omniload/src/slack/__init__.py +290 -0
  167. omniload/src/slack/helpers.py +218 -0
  168. omniload/src/slack/settings.py +36 -0
  169. omniload/src/smartsheets/__init__.py +82 -0
  170. omniload/src/snapchat_ads/__init__.py +455 -0
  171. omniload/src/snapchat_ads/client.py +72 -0
  172. omniload/src/snapchat_ads/helpers.py +630 -0
  173. omniload/src/snapchat_ads/settings.py +130 -0
  174. omniload/src/socrata_source/__init__.py +83 -0
  175. omniload/src/socrata_source/helpers.py +85 -0
  176. omniload/src/socrata_source/settings.py +8 -0
  177. omniload/src/solidgate/__init__.py +219 -0
  178. omniload/src/solidgate/helpers.py +154 -0
  179. omniload/src/sources.py +5408 -0
  180. omniload/src/sql_database/__init__.py +0 -0
  181. omniload/src/sql_database/callbacks.py +66 -0
  182. omniload/src/stripe_analytics/__init__.py +183 -0
  183. omniload/src/stripe_analytics/helpers.py +386 -0
  184. omniload/src/stripe_analytics/settings.py +80 -0
  185. omniload/src/table_definition.py +15 -0
  186. omniload/src/testdata/fakebqcredentials.json +14 -0
  187. omniload/src/tiktok_ads/__init__.py +150 -0
  188. omniload/src/tiktok_ads/tiktok_helpers.py +130 -0
  189. omniload/src/time.py +11 -0
  190. omniload/src/trustpilot/__init__.py +48 -0
  191. omniload/src/trustpilot/client.py +48 -0
  192. omniload/src/version.py +6 -0
  193. omniload/src/wise/__init__.py +68 -0
  194. omniload/src/wise/client.py +63 -0
  195. omniload/src/zendesk/__init__.py +480 -0
  196. omniload/src/zendesk/helpers/__init__.py +39 -0
  197. omniload/src/zendesk/helpers/api_helpers.py +119 -0
  198. omniload/src/zendesk/helpers/credentials.py +68 -0
  199. omniload/src/zendesk/helpers/talk_api.py +132 -0
  200. omniload/src/zendesk/settings.py +71 -0
  201. omniload/src/zoom/__init__.py +99 -0
  202. omniload/src/zoom/helpers.py +102 -0
  203. omniload/testdata/.gitignore +2 -0
  204. omniload/testdata/create_replace.csv +21 -0
  205. omniload/testdata/delete_insert_expected.csv +6 -0
  206. omniload/testdata/delete_insert_part1.csv +5 -0
  207. omniload/testdata/delete_insert_part2.csv +6 -0
  208. omniload/testdata/merge_expected.csv +5 -0
  209. omniload/testdata/merge_part1.csv +4 -0
  210. omniload/testdata/merge_part2.csv +5 -0
  211. omniload/tests/unit/test_smartsheets.py +133 -0
  212. omniload-0.0.0.dev0.dist-info/METADATA +439 -0
  213. omniload-0.0.0.dev0.dist-info/RECORD +218 -0
  214. omniload-0.0.0.dev0.dist-info/WHEEL +4 -0
  215. omniload-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  216. omniload-0.0.0.dev0.dist-info/licenses/LICENSE.Apache-2.0 +201 -0
  217. omniload-0.0.0.dev0.dist-info/licenses/LICENSE.md +21 -0
  218. omniload-0.0.0.dev0.dist-info/licenses/NOTICE +35 -0
@@ -0,0 +1,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