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,329 @@
1
+ # Copyright 2022-2025 ScaleVector
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Hubspot source settings and constants"""
16
+
17
+ from dlt.common import pendulum
18
+
19
+ STARTDATE = pendulum.datetime(year=2000, month=1, day=1)
20
+
21
+ CRM_CONTACTS_ENDPOINT = (
22
+ "/crm/v3/objects/contacts?associations=companies,deals,products,tickets,quotes"
23
+ )
24
+ CRM_COMPANIES_ENDPOINT = "/crm/v3/objects/companies?associations=products"
25
+ CRM_DEALS_ENDPOINT = (
26
+ "/crm/v3/objects/deals?associations=companies,contacts,products,tickets,quotes"
27
+ )
28
+ CRM_PRODUCTS_ENDPOINT = (
29
+ "/crm/v3/objects/products?associations=companies,contacts,deals,tickets,quotes"
30
+ )
31
+ CRM_TICKETS_ENDPOINT = (
32
+ "/crm/v3/objects/tickets?associations=companies,contacts,deals,products,quotes"
33
+ )
34
+ CRM_QUOTES_ENDPOINT = (
35
+ "/crm/v3/objects/quotes?associations=companies,contacts,deals,products,tickets"
36
+ )
37
+ CRM_CALLS_ENDPOINT = (
38
+ "/crm/v3/objects/calls?associations=contacts,companies,deals,products,quotes"
39
+ )
40
+ CRM_EMAILS_ENDPOINT = (
41
+ "/crm/v3/objects/emails?associations=contacts,companies,deals,products,quotes"
42
+ )
43
+ CRM_FEEDBACK_SUBMISSIONS_ENDPOINT = "/crm/v3/objects/feedback_submissions?associations=contacts,companies,deals,products,quotes"
44
+ CRM_LINE_ITEMS_ENDPOINT = (
45
+ "/crm/v3/objects/line_items?associations=contacts,companies,deals,products,quotes"
46
+ )
47
+ CRM_MEETINGS_ENDPOINT = (
48
+ "/crm/v3/objects/meetings?associations=contacts,companies,deals,products,quotes"
49
+ )
50
+ CRM_NOTES_ENDPOINT = (
51
+ "/crm/v3/objects/notes?associations=contacts,companies,deals,products,quotes"
52
+ )
53
+ CRM_TASKS_ENDPOINT = (
54
+ "/crm/v3/objects/tasks?associations=contacts,companies,deals,products,quotes"
55
+ )
56
+ CRM_CARTS_ENDPOINT = (
57
+ "/crm/v3/objects/carts?associations=contacts,companies,deals,products,quotes"
58
+ )
59
+ CRM_DISCOUNTS_ENDPOINT = "/crm/v3/objects/discounts?associations=contacts,line_items,companies,deals,products,quotes"
60
+ CRM_FEES_ENDPOINT = "/crm/v3/objects/fees?associations=contacts,line_items,companies,deals,products,quotes"
61
+ CRM_INVOICES_ENDPOINT = "/crm/v3/objects/invoices?associations=contacts,line_items,companies,fees,products,quotes"
62
+ CRM_COMMERCE_PAYMENTS_ENDPOINT = "/crm/v3/objects/commerce_payments?associations=contacts,companies,deals,quotes,invoices,products,fees"
63
+ CRM_TAXES_ENDPOINT = (
64
+ "/crm/v3/objects/taxes?associations=line_items,companies,deals,products,quotes,fees"
65
+ )
66
+ CRM_OWNERS_ENDPOINT = "/crm/v3/owners"
67
+ CRM_SCHEMAS_ENDPOINT = "/crm/v3/schemas"
68
+
69
+ CRM_OBJECT_ENDPOINTS = {
70
+ "contact": CRM_CONTACTS_ENDPOINT,
71
+ "company": CRM_COMPANIES_ENDPOINT,
72
+ "deal": CRM_DEALS_ENDPOINT,
73
+ "product": CRM_PRODUCTS_ENDPOINT,
74
+ "ticket": CRM_TICKETS_ENDPOINT,
75
+ "quote": CRM_QUOTES_ENDPOINT,
76
+ "call": CRM_CALLS_ENDPOINT,
77
+ "email": CRM_EMAILS_ENDPOINT,
78
+ "feedback_submission": CRM_FEEDBACK_SUBMISSIONS_ENDPOINT,
79
+ "line_item": CRM_LINE_ITEMS_ENDPOINT,
80
+ "meeting": CRM_MEETINGS_ENDPOINT,
81
+ "note": CRM_NOTES_ENDPOINT,
82
+ "task": CRM_TASKS_ENDPOINT,
83
+ "cart": CRM_CARTS_ENDPOINT,
84
+ "discount": CRM_DISCOUNTS_ENDPOINT,
85
+ "fee": CRM_FEES_ENDPOINT,
86
+ "invoice": CRM_INVOICES_ENDPOINT,
87
+ "commerce_payment": CRM_COMMERCE_PAYMENTS_ENDPOINT,
88
+ "tax": CRM_TAXES_ENDPOINT,
89
+ }
90
+
91
+ WEB_ANALYTICS_EVENTS_ENDPOINT = "/events/v3/events?objectType={objectType}&objectId={objectId}&occurredAfter={occurredAfter}&occurredBefore={occurredBefore}&sort=-occurredAt"
92
+
93
+ OBJECT_TYPE_SINGULAR = {
94
+ "companies": "company",
95
+ "contacts": "contact",
96
+ "deals": "deal",
97
+ "tickets": "ticket",
98
+ "products": "product",
99
+ "quotes": "quote",
100
+ "calls": "call",
101
+ "emails": "email",
102
+ "feedback_submissions": "feedback_submission",
103
+ "line_items": "line_item",
104
+ "meetings": "meeting",
105
+ "notes": "note",
106
+ "tasks": "task",
107
+ "carts": "cart",
108
+ "discounts": "discount",
109
+ "fees": "fee",
110
+ "invoices": "invoice",
111
+ "commerce_payments": "commerce_payment",
112
+ "taxes": "tax",
113
+ }
114
+
115
+ OBJECT_TYPE_PLURAL = {v: k for k, v in OBJECT_TYPE_SINGULAR.items()}
116
+
117
+ # Contacts use "lastmodifieddate"; all other CRM objects use "hs_lastmodifieddate"
118
+ LAST_MODIFIED_PROPERTY = {
119
+ "contact": "lastmodifieddate",
120
+ }
121
+ DEFAULT_LAST_MODIFIED_PROPERTY = "hs_lastmodifieddate"
122
+
123
+ DEFAULT_DEAL_PROPS = [
124
+ "amount",
125
+ "closedate",
126
+ "createdate",
127
+ "dealname",
128
+ "dealstage",
129
+ "hs_lastmodifieddate",
130
+ "hs_object_id",
131
+ "pipeline",
132
+ ]
133
+
134
+ DEFAULT_COMPANY_PROPS = [
135
+ "createdate",
136
+ "domain",
137
+ "hs_lastmodifieddate",
138
+ "hs_object_id",
139
+ "name",
140
+ ]
141
+
142
+ DEFAULT_CONTACT_PROPS = [
143
+ "createdate",
144
+ "email",
145
+ "firstname",
146
+ "hs_object_id",
147
+ "lastmodifieddate",
148
+ "lastname",
149
+ ]
150
+
151
+ DEFAULT_TICKET_PROPS = [
152
+ "createdate",
153
+ "content",
154
+ "hs_lastmodifieddate",
155
+ "hs_object_id",
156
+ "hs_pipeline",
157
+ "hs_pipeline_stage",
158
+ "hs_ticket_category",
159
+ "hs_ticket_priority",
160
+ "subject",
161
+ ]
162
+
163
+ DEFAULT_PRODUCT_PROPS = [
164
+ "createdate",
165
+ "description",
166
+ "hs_lastmodifieddate",
167
+ "hs_object_id",
168
+ "name",
169
+ "price",
170
+ ]
171
+
172
+ DEFAULT_QUOTE_PROPS = [
173
+ "hs_createdate",
174
+ "hs_expiration_date",
175
+ "hs_lastmodifieddate",
176
+ "hs_object_id",
177
+ "hs_public_url_key",
178
+ "hs_status",
179
+ "hs_title",
180
+ ]
181
+
182
+ DEFAULT_CALL_PROPS = [
183
+ "hs_call_body",
184
+ "hs_call_direction",
185
+ "hs_call_disposition",
186
+ "hs_call_duration",
187
+ "hs_call_from_number",
188
+ "hs_call_status",
189
+ "hs_call_title",
190
+ "hs_call_to_number",
191
+ "hs_lastmodifieddate",
192
+ "hs_timestamp",
193
+ ]
194
+
195
+ DEFAULT_EMAIL_PROPS = [
196
+ "hs_attachment_ids",
197
+ "hs_email_direction",
198
+ "hs_email_headers",
199
+ "hs_email_html",
200
+ "hs_email_status",
201
+ "hs_email_subject",
202
+ "hs_email_text",
203
+ "hs_timestamp",
204
+ "hs_lastmodifieddate",
205
+ "hubspot_owner_id",
206
+ ]
207
+
208
+ DEFAULT_FEEDBACK_SUBMISSION_PROPS = [
209
+ "hs_createdate",
210
+ "hs_lastmodifieddate",
211
+ "hs_object_id",
212
+ "hs_sentiment",
213
+ "hs_survey_channel",
214
+ ]
215
+
216
+ DEFAULT_LINE_ITEM_PROPS = [
217
+ "amount",
218
+ "description",
219
+ "hs_line_item_currency_code",
220
+ "hs_recurring_billing_end_date",
221
+ "hs_recurring_billing_start_date",
222
+ "hs_lastmodifieddate",
223
+ "hs_sku",
224
+ "name",
225
+ "price",
226
+ "quantity",
227
+ "recurringbillingfrequency",
228
+ ]
229
+
230
+ DEFAULT_MEETING_PROPS = [
231
+ "hs_internal_meeting_notes",
232
+ "hs_meeting_body",
233
+ "hs_meeting_end_time",
234
+ "hs_meeting_external_url",
235
+ "hs_meeting_location",
236
+ "hs_meeting_outcome",
237
+ "hs_meeting_start_time",
238
+ "hs_meeting_title",
239
+ "hs_timestamp",
240
+ "hs_lastmodifieddate",
241
+ "hubspot_owner_id",
242
+ ]
243
+
244
+ DEFAULT_NOTE_PROPS = [
245
+ "hs_attachment_ids",
246
+ "hs_note_body",
247
+ "hs_timestamp",
248
+ "hs_lastmodifieddate",
249
+ "hubspot_owner_id",
250
+ ]
251
+
252
+ DEFAULT_TASK_PROPS = [
253
+ "hs_task_body",
254
+ "hs_task_priority",
255
+ "hs_task_status",
256
+ "hs_task_subject",
257
+ "hs_task_type",
258
+ "hs_timestamp",
259
+ "hs_lastmodifieddate",
260
+ "hubspot_owner_id",
261
+ ]
262
+
263
+ DEFAULT_CART_PROPS = [
264
+ "hs_cart_discount",
265
+ "hs_cart_name",
266
+ "hs_cart_url",
267
+ "hs_createdate",
268
+ "hs_currency_code",
269
+ "hs_external_cart_id",
270
+ "hs_external_status",
271
+ "hs_lastmodifieddate",
272
+ "hs_object_id",
273
+ "hs_shipping_cost",
274
+ "hs_source_store",
275
+ "hs_tags",
276
+ "hs_tax",
277
+ "hs_total_price",
278
+ ]
279
+
280
+ DEFAULT_DISCOUNT_PROPS = [
281
+ "hs_duration",
282
+ "hs_label",
283
+ "hs_lastmodifieddate",
284
+ "hs_sort_order",
285
+ "hs_type",
286
+ "hs_value",
287
+ ]
288
+
289
+ DEFAULT_FEE_PROPS = [
290
+ "hs_label",
291
+ "hs_lastmodifieddate",
292
+ "hs_type",
293
+ "hs_value",
294
+ ]
295
+
296
+ DEFAULT_INVOICE_PROPS = [
297
+ "hs_currency",
298
+ "hs_due_date",
299
+ "hs_invoice_date",
300
+ "hs_lastmodifieddate",
301
+ "hs_tax_id",
302
+ ]
303
+
304
+ DEFAULT_COMMERCE_PAYMENT_PROPS = [
305
+ "hs_currency_code",
306
+ "hs_customer_email",
307
+ "hs_fees_amount",
308
+ "hs_initial_amount",
309
+ "hs_initiated_date",
310
+ "hs_internal_comment",
311
+ "hs_lastmodifieddate",
312
+ "hs_latest_status",
313
+ "hs_payment_method_type",
314
+ "hs_payout_date",
315
+ "hs_processor_type",
316
+ "hs_reference_number",
317
+ "hs_refunds_amount",
318
+ "hs_billing_address_city",
319
+ "hs_billing_address_country",
320
+ ]
321
+
322
+ DEFAULT_TAX_PROPS = [
323
+ "hs_label",
324
+ "hs_lastmodifieddate",
325
+ "hs_type",
326
+ "hs_value",
327
+ ]
328
+
329
+ ALL = ("ALL",)
@@ -0,0 +1,153 @@
1
+ from typing import Any, Dict, Iterable, Iterator
2
+
3
+ import dlt
4
+ import pendulum
5
+
6
+ from .helpers import (
7
+ _get_account,
8
+ _get_campaign_budget,
9
+ _get_campaign_details,
10
+ _get_campaign_jobs,
11
+ _get_campaign_properties,
12
+ _get_campaign_stats,
13
+ _get_oauth_token,
14
+ _get_traffic_report,
15
+ _paginate_campaigns,
16
+ )
17
+
18
+
19
+ @dlt.source(name="indeed", max_table_nesting=0)
20
+ def indeed_source(
21
+ client_id: str,
22
+ client_secret: str,
23
+ employer_id: str,
24
+ start_date: pendulum.DateTime,
25
+ end_date: pendulum.DateTime | None = None,
26
+ ) -> Iterable[dlt.sources.DltResource]:
27
+ token = _get_oauth_token(client_id, client_secret, employer_id)
28
+
29
+ @dlt.resource(name="campaigns", write_disposition="merge", primary_key="Id")
30
+ def campaigns() -> Iterator[Dict[str, Any]]:
31
+ for campaign in _paginate_campaigns(token):
32
+ yield campaign
33
+
34
+ @dlt.transformer(
35
+ name="campaign_details",
36
+ write_disposition="merge",
37
+ primary_key="campaignId",
38
+ data_from=campaigns,
39
+ parallelized=True,
40
+ )
41
+ def campaign_details(campaign: Dict[str, Any]) -> Iterator[Dict[str, Any]]:
42
+ details = _get_campaign_details(token, campaign["Id"])
43
+ yield details
44
+
45
+ @dlt.transformer(
46
+ name="campaign_budget",
47
+ write_disposition="merge",
48
+ primary_key="campaignId",
49
+ data_from=campaigns,
50
+ parallelized=True,
51
+ )
52
+ def campaign_budget(campaign: Dict[str, Any]) -> Iterator[Dict[str, Any]]:
53
+ budget = _get_campaign_budget(token, campaign["Id"])
54
+ if budget:
55
+ yield budget
56
+
57
+ @dlt.transformer(
58
+ name="campaign_jobs",
59
+ write_disposition="merge",
60
+ primary_key=["campaignId", "jobKey"],
61
+ data_from=campaigns,
62
+ parallelized=True,
63
+ )
64
+ def campaign_jobs(campaign: Dict[str, Any]) -> Iterator[Dict[str, Any]]:
65
+ for job in _get_campaign_jobs(token, campaign["Id"]):
66
+ yield job
67
+
68
+ @dlt.transformer(
69
+ name="campaign_properties",
70
+ write_disposition="merge",
71
+ primary_key="campaignId",
72
+ data_from=campaigns,
73
+ parallelized=True,
74
+ )
75
+ def campaign_properties(campaign: Dict[str, Any]) -> Iterator[Dict[str, Any]]:
76
+ props = _get_campaign_properties(token, campaign["Id"])
77
+ if props:
78
+ yield props
79
+
80
+ @dlt.transformer(
81
+ name="campaign_stats",
82
+ write_disposition="merge",
83
+ primary_key=["campaignId", "Date"],
84
+ merge_key="Date",
85
+ data_from=campaigns,
86
+ parallelized=True,
87
+ )
88
+ def campaign_stats(
89
+ campaign: Dict[str, Any],
90
+ date: dlt.sources.incremental[str] = dlt.sources.incremental(
91
+ "Date",
92
+ initial_value=start_date.to_date_string(),
93
+ end_value=end_date.to_date_string() if end_date else None,
94
+ range_start="closed",
95
+ range_end="closed",
96
+ ),
97
+ ) -> Iterator[Dict[str, Any]]:
98
+ current_start = date.last_value or start_date.to_date_string()
99
+ current_end = date.end_value or pendulum.now("UTC").to_date_string()
100
+
101
+ for stat in _get_campaign_stats(
102
+ token, campaign["Id"], current_start, current_end
103
+ ):
104
+ yield stat
105
+
106
+ @dlt.resource(
107
+ name="account",
108
+ write_disposition="merge",
109
+ primary_key=["employerId", "jobSourceId"],
110
+ )
111
+ def account() -> Iterator[Dict[str, Any]]:
112
+ data = _get_account(token)
113
+ employer_id = data.get("employerId")
114
+ contact = data.get("contact")
115
+ company = data.get("company")
116
+ email = data.get("email")
117
+
118
+ for job_source in data.get("jobSourceList", []):
119
+ yield {
120
+ "employerId": employer_id,
121
+ "contact": contact,
122
+ "company": company,
123
+ "email": email,
124
+ "jobSourceId": job_source.get("id"),
125
+ "jobSourceSiteName": job_source.get("siteName"),
126
+ }
127
+
128
+ @dlt.resource(name="traffic_stats", write_disposition="merge", merge_key="date")
129
+ def traffic_stats(
130
+ date: dlt.sources.incremental[str] = dlt.sources.incremental(
131
+ "date",
132
+ initial_value=start_date.to_date_string(),
133
+ end_value=end_date.to_date_string() if end_date else None,
134
+ range_start="closed",
135
+ range_end="closed",
136
+ ),
137
+ ) -> Iterator[Dict[str, Any]]:
138
+ current_start = date.last_value or start_date.to_date_string()
139
+ current_end = date.end_value or pendulum.now("UTC").to_date_string()
140
+
141
+ for row in _get_traffic_report(token, current_start, current_end):
142
+ yield row
143
+
144
+ return [
145
+ campaigns,
146
+ campaign_details,
147
+ campaign_budget,
148
+ campaign_jobs,
149
+ campaign_properties,
150
+ campaign_stats,
151
+ account,
152
+ traffic_stats,
153
+ ]
@@ -0,0 +1,228 @@
1
+ import csv
2
+ import io
3
+ import time
4
+ from datetime import datetime, timedelta
5
+ from typing import Any, Dict, Iterator, Optional
6
+
7
+ import requests
8
+
9
+ INDEED_TOKEN_URL = "https://apis.indeed.com/oauth/v2/tokens"
10
+ INDEED_API_BASE_URL = "https://apis.indeed.com/ads/v1"
11
+
12
+ DEFAULT_SCOPES = [
13
+ "employer.advertising.campaign.read",
14
+ "employer.advertising.campaign_report.read",
15
+ "employer.advertising.account.read",
16
+ ]
17
+
18
+
19
+ def _get_oauth_token(
20
+ client_id: str,
21
+ client_secret: str,
22
+ employer_id: str,
23
+ ) -> str:
24
+ response = requests.post(
25
+ INDEED_TOKEN_URL,
26
+ data={
27
+ "client_id": client_id,
28
+ "client_secret": client_secret,
29
+ "grant_type": "client_credentials",
30
+ "scope": " ".join(DEFAULT_SCOPES),
31
+ "employer": employer_id,
32
+ },
33
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
34
+ )
35
+ response.raise_for_status()
36
+ payload = response.json()
37
+
38
+ if "access_token" not in payload:
39
+ raise ValueError(f"Token response missing access_token: {payload}")
40
+
41
+ return payload["access_token"]
42
+
43
+
44
+ def _api_request(
45
+ token: str,
46
+ endpoint: str,
47
+ params: Optional[Dict[str, Any]] = None,
48
+ ) -> requests.Response:
49
+ url = f"{INDEED_API_BASE_URL}{endpoint}"
50
+ headers = {
51
+ "Authorization": f"Bearer {token}",
52
+ "Accept": "application/json",
53
+ }
54
+ response = requests.get(url, headers=headers, params=params)
55
+ try:
56
+ response.raise_for_status()
57
+ except requests.exceptions.HTTPError as e:
58
+ raise RuntimeError(f"{e} - Response body: {response.text}") from e
59
+ return response
60
+
61
+
62
+ def _paginate_campaigns(token: str) -> Iterator[Dict[str, Any]]:
63
+ cursor: Optional[str] = None
64
+ while True:
65
+ params: Dict[str, Any] = {"perPage": 500, "status": "ACTIVE"}
66
+ if cursor:
67
+ params["start"] = cursor
68
+ response = _api_request(token, "/campaigns", params=params)
69
+ data = response.json()
70
+
71
+ campaigns = data.get("data", {}).get("Campaigns", [])
72
+ for campaign in campaigns:
73
+ yield campaign
74
+
75
+ links = data.get("meta", {}).get("links", [])
76
+ next_link = next((link for link in links if link.get("rel") == "next"), None)
77
+
78
+ if not next_link:
79
+ break
80
+
81
+ href = next_link.get("href", "")
82
+ if "start=" in href:
83
+ cursor = href.split("start=")[1].split("&")[0]
84
+ else:
85
+ break
86
+
87
+
88
+ def _get_campaign_details(token: str, campaign_id: str) -> Dict[str, Any]:
89
+ response = _api_request(token, f"/campaigns/{campaign_id}")
90
+ data = response.json().get("data", {})
91
+ data["campaignId"] = campaign_id
92
+ return data
93
+
94
+
95
+ def _get_campaign_budget(token: str, campaign_id: str) -> Optional[Dict[str, Any]]:
96
+ try:
97
+ response = _api_request(token, f"/campaigns/{campaign_id}/budget")
98
+ data = response.json().get("data", {})
99
+ data["campaignId"] = campaign_id
100
+ return data
101
+ except requests.exceptions.HTTPError as e:
102
+ if e.response.status_code == 404:
103
+ return None
104
+ raise
105
+
106
+
107
+ def _get_campaign_jobs(token: str, campaign_id: str) -> Iterator[Dict[str, Any]]:
108
+ try:
109
+ response = _api_request(
110
+ token,
111
+ f"/campaigns/{campaign_id}/jobDetails",
112
+ )
113
+ data = response.json()
114
+
115
+ jobs = data.get("data", {}).get("entries", [])
116
+ for job in jobs:
117
+ job["campaignId"] = campaign_id
118
+ yield job
119
+ except requests.exceptions.HTTPError as e:
120
+ if e.response is not None and e.response.status_code == 404:
121
+ return
122
+ if e.response is not None and e.response.status_code == 429:
123
+ time.sleep(5)
124
+ yield from _get_campaign_jobs(token, campaign_id)
125
+ return
126
+ raise
127
+
128
+
129
+ def _get_campaign_properties(token: str, campaign_id: str) -> Optional[Dict[str, Any]]:
130
+ try:
131
+ response = _api_request(token, f"/campaigns/{campaign_id}/properties")
132
+ data = response.json().get("data", {})
133
+ data["campaignId"] = campaign_id
134
+ return data
135
+ except requests.exceptions.HTTPError as e:
136
+ if e.response.status_code == 404:
137
+ return None
138
+ raise
139
+
140
+
141
+ def _get_campaign_stats(
142
+ token: str, campaign_id: str, start_date: str, end_date: str
143
+ ) -> Iterator[Dict[str, Any]]:
144
+ try:
145
+ response = _api_request(
146
+ token,
147
+ f"/campaigns/{campaign_id}/stats",
148
+ params={"startDate": start_date, "endDate": end_date},
149
+ )
150
+ data = response.json().get("data", {})
151
+
152
+ stats = data.get("Stats", []) or data.get("entries", [])
153
+ for stat in stats:
154
+ stat["campaignId"] = campaign_id
155
+ yield stat
156
+ except requests.exceptions.HTTPError as e:
157
+ if e.response.status_code == 404:
158
+ return
159
+ raise
160
+
161
+
162
+ def _get_account(token: str) -> Dict[str, Any]:
163
+ response = _api_request(token, "/account")
164
+ return response.json().get("data", {})
165
+
166
+
167
+ def _get_traffic_report_for_day(
168
+ token: str, date: str, max_retries: int = 10, retry_delay: int = 5
169
+ ) -> Iterator[Dict[str, Any]]:
170
+ try:
171
+ start_dt = datetime.strptime(date, "%Y-%m-%d")
172
+ end_dt = start_dt + timedelta(days=1)
173
+ end_date_str = end_dt.strftime("%Y-%m-%d")
174
+
175
+ response = _api_request(
176
+ token,
177
+ "/stats",
178
+ params={"startDate": date, "endDate": end_date_str, "v": "8"},
179
+ )
180
+
181
+ if response.status_code == 202:
182
+ location = response.json().get("data", {}).get("location", "")
183
+ if not location:
184
+ raise ValueError(f"API returned 202 but no location for date {date}")
185
+
186
+ if location.startswith("/v1"):
187
+ location = location[3:]
188
+
189
+ for attempt in range(max_retries):
190
+ time.sleep(retry_delay)
191
+ report_response = _api_request(token, location)
192
+
193
+ if report_response.status_code == 200:
194
+ content_type = report_response.headers.get("Content-Type", "")
195
+ if "text/csv" in content_type or "application/csv" in content_type:
196
+ for row in _parse_csv(report_response.text):
197
+ row["date"] = date
198
+ yield row
199
+ return
200
+ return
201
+ except requests.exceptions.HTTPError as e:
202
+ body = e.response.text if e.response is not None else "N/A"
203
+ raise RuntimeError(f"{e} - Response body: {body}") from e
204
+
205
+
206
+ def _get_traffic_report(
207
+ token: str,
208
+ start_date: str,
209
+ end_date: str,
210
+ max_retries: int = 10,
211
+ retry_delay: int = 5,
212
+ ) -> Iterator[Dict[str, Any]]:
213
+ start = datetime.strptime(start_date, "%Y-%m-%d")
214
+ end = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
215
+
216
+ current = start
217
+ while current < end:
218
+ date_str = current.strftime("%Y-%m-%d")
219
+ yield from _get_traffic_report_for_day(
220
+ token, date_str, max_retries, retry_delay
221
+ )
222
+ current += timedelta(days=1)
223
+
224
+
225
+ def _parse_csv(csv_content: str) -> Iterator[Dict[str, Any]]:
226
+ reader = csv.DictReader(io.StringIO(csv_content))
227
+ for row in reader:
228
+ yield dict(row)