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,80 @@
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
+ """Stripe analytics source settings and constants"""
16
+
17
+ # the most popular endpoints
18
+ # Full list of the Stripe API endpoints you can find here: https://stripe.com/docs/api.
19
+ ENDPOINTS = {
20
+ "account": "Account",
21
+ "applepaydomain": "ApplePayDomain",
22
+ "apple_pay_domain": "ApplePayDomain",
23
+ "applicationfee": "ApplicationFee",
24
+ "application_fee": "ApplicationFee",
25
+ "checkoutsession": "CheckoutSession",
26
+ "checkout_session": "CheckoutSession",
27
+ "coupon": "Coupon",
28
+ "charge": "Charge",
29
+ "customer": "Customer",
30
+ "dispute": "Dispute",
31
+ "paymentintent": "PaymentIntent",
32
+ "payment_intent": "PaymentIntent",
33
+ "paymentlink": "PaymentLink",
34
+ "payment_link": "PaymentLink",
35
+ "paymentmethod": "PaymentMethod",
36
+ "payment_method": "PaymentMethod",
37
+ "paymentmethoddomain": "PaymentMethodDomain",
38
+ "payment_method_domain": "PaymentMethodDomain",
39
+ "payout": "Payout",
40
+ "plan": "Plan",
41
+ "price": "Price",
42
+ "product": "Product",
43
+ "promotioncode": "PromotionCode",
44
+ "promotion_code": "PromotionCode",
45
+ "quote": "Quote",
46
+ "refund": "Refund",
47
+ "review": "Review",
48
+ "setupattempt": "SetupAttempt",
49
+ "setup_attempt": "SetupAttempt",
50
+ "setupintent": "SetupIntent",
51
+ "setup_intent": "SetupIntent",
52
+ "shippingrate": "ShippingRate",
53
+ "shipping_rate": "ShippingRate",
54
+ "subscription": "Subscription",
55
+ "subscriptionitem": "SubscriptionItem",
56
+ "subscription_item": "SubscriptionItem",
57
+ "subscriptionschedule": "SubscriptionSchedule",
58
+ "subscription_schedule": "SubscriptionSchedule",
59
+ "transfer": "Transfer",
60
+ "taxcode": "TaxCode",
61
+ "tax_code": "TaxCode",
62
+ "taxid": "TaxId",
63
+ "tax_id": "TaxId",
64
+ "taxrate": "TaxRate",
65
+ "tax_rate": "TaxRate",
66
+ "topup": "Topup",
67
+ "top_up": "Topup",
68
+ "webhookendpoint": "WebhookEndpoint",
69
+ "webhook_endpoint": "WebhookEndpoint",
70
+ "invoice": "Invoice",
71
+ "invoiceitem": "InvoiceItem",
72
+ "invoice_item": "InvoiceItem",
73
+ "invoicelineitem": "InvoiceLineItem",
74
+ "invoice_line_item": "InvoiceLineItem",
75
+ "balancetransaction": "BalanceTransaction",
76
+ "balance_transaction": "BalanceTransaction",
77
+ "creditnote": "CreditNote",
78
+ "credit_note": "CreditNote",
79
+ "event": "Event",
80
+ }
@@ -0,0 +1,15 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class TableDefinition:
6
+ dataset: str
7
+ table: str
8
+
9
+
10
+ def table_string_to_dataclass(table: str) -> TableDefinition:
11
+ table_fields = table.split(".", 1)
12
+ if len(table_fields) != 2:
13
+ raise ValueError("Table name must be in the format <schema>.<table>")
14
+
15
+ return TableDefinition(dataset=table_fields[0], table=table_fields[1])
@@ -0,0 +1,14 @@
1
+ {
2
+ "type": "service_account",
3
+ "project_id": "my-project",
4
+ "private_key_id": "some-private-key-id",
5
+ "private_key": "-----BEGIN PRIVATE KEY-----some-private-key-----END PRIVATE KEY-----\n",
6
+ "client_email": "someuser@someproject.iam.gserviceaccount.com",
7
+ "client_id": "1234567890",
8
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
9
+ "token_uri": "https://oauth2.googleapis.com/token",
10
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
11
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/",
12
+ "universe_domain": "googleapis.com"
13
+ }
14
+
@@ -0,0 +1,150 @@
1
+ from typing import Iterable
2
+
3
+ import dlt
4
+ import pendulum
5
+ from dlt.common.time import ensure_pendulum_datetime
6
+ from dlt.common.typing import TDataItem
7
+ from dlt.sources import DltResource
8
+
9
+ from .tiktok_helpers import TikTokAPI
10
+
11
+ KNOWN_TYPE_HINTS = {
12
+ "spend": {"data_type": "decimal"},
13
+ "billed_cost": {"data_type": "decimal"},
14
+ "cash_spend": {"data_type": "decimal"},
15
+ "voucher_spend": {"data_type": "decimal"},
16
+ "cpc": {"data_type": "decimal"},
17
+ "cpm": {"data_type": "decimal"},
18
+ "impressions": {"data_type": "bigint"},
19
+ "gross_impressions": {"data_type": "bigint"},
20
+ "clicks": {"data_type": "bigint"},
21
+ "ctr": {"data_type": "decimal"},
22
+ "reach": {"data_type": "bigint"},
23
+ "cost_per_1000_reached": {"data_type": "decimal"},
24
+ "frequency": {"data_type": "decimal"},
25
+ "conversion": {"data_type": "bigint"},
26
+ "cost_per_conversion": {"data_type": "decimal"},
27
+ "conversion_rate": {"data_type": "decimal"},
28
+ "conversion_rate_v2": {"data_type": "decimal"},
29
+ "real_time_conversion": {"data_type": "bigint"},
30
+ "real_time_cost_per_conversion": {"data_type": "decimal"},
31
+ "real_time_conversion_rate": {"data_type": "decimal"},
32
+ "real_time_conversion_rate_v2": {"data_type": "decimal"},
33
+ "result": {"data_type": "bigint"},
34
+ "cost_per_result": {"data_type": "decimal"},
35
+ "result_rate": {"data_type": "decimal"},
36
+ "real_time_result": {"data_type": "bigint"},
37
+ "real_time_cost_per_result": {"data_type": "decimal"},
38
+ "real_time_result_rate": {"data_type": "decimal"},
39
+ "secondary_goal_result": {"data_type": "bigint"},
40
+ "cost_per_secondary_goal_result": {"data_type": "decimal"},
41
+ "secondary_goal_result_rate": {"data_type": "decimal"},
42
+ }
43
+
44
+
45
+ def find_intervals(
46
+ current_date: pendulum.DateTime,
47
+ end_date: pendulum.DateTime,
48
+ interval_days: int,
49
+ ):
50
+ intervals = []
51
+ while current_date <= end_date:
52
+ interval_end = min(current_date.add(days=interval_days), end_date)
53
+ intervals.append((current_date, interval_end))
54
+ current_date = interval_end.add(days=1)
55
+
56
+ return intervals
57
+
58
+
59
+ @dlt.source(max_table_nesting=0)
60
+ def tiktok_source(
61
+ start_date: pendulum.DateTime,
62
+ end_date: pendulum.DateTime,
63
+ access_token: str,
64
+ advertiser_ids: list[str],
65
+ timezone: str,
66
+ page_size: int,
67
+ filtering_param: bool,
68
+ filter_name: str,
69
+ filter_value: list[int],
70
+ dimensions: list[str],
71
+ metrics: list[str],
72
+ ) -> DltResource:
73
+ tiktok_api = TikTokAPI(
74
+ access_token=access_token,
75
+ timezone=timezone,
76
+ page_size=page_size,
77
+ filtering_param=filtering_param,
78
+ filter_name=filter_name,
79
+ filter_value=filter_value,
80
+ )
81
+ incremental_loading_param = ""
82
+ is_incremental = False
83
+ interval_days = 365
84
+
85
+ if "stat_time_day" in dimensions:
86
+ incremental_loading_param = "stat_time_day"
87
+ is_incremental = True
88
+ interval_days = 30
89
+
90
+ if "stat_time_hour" in dimensions:
91
+ incremental_loading_param = "stat_time_hour"
92
+ is_incremental = True
93
+ interval_days = 0
94
+
95
+ type_hints = {
96
+ "advertiser_id": {"data_type": "text"},
97
+ }
98
+ for dimension in dimensions:
99
+ if dimension in KNOWN_TYPE_HINTS:
100
+ type_hints[dimension] = KNOWN_TYPE_HINTS[dimension]
101
+ for metric in metrics:
102
+ if metric in KNOWN_TYPE_HINTS:
103
+ type_hints[metric] = KNOWN_TYPE_HINTS[metric]
104
+
105
+ @dlt.resource(
106
+ write_disposition="merge",
107
+ primary_key=dimensions + ["advertiser_id"],
108
+ columns=type_hints,
109
+ parallelized=True,
110
+ )
111
+ def custom_reports(
112
+ datetime=(
113
+ dlt.sources.incremental(
114
+ incremental_loading_param,
115
+ initial_value=start_date,
116
+ end_value=end_date,
117
+ range_end="closed",
118
+ range_start="closed",
119
+ )
120
+ if is_incremental
121
+ else None
122
+ ),
123
+ ) -> Iterable[TDataItem]:
124
+ start_date_tz_adjusted = start_date.in_tz(timezone)
125
+ end_date_tz_adjusted = end_date.in_tz(timezone)
126
+
127
+ if datetime is not None:
128
+ start_date_tz_adjusted = ensure_pendulum_datetime(
129
+ datetime.last_value
130
+ ).in_tz(timezone)
131
+ end_date_tz_adjusted = ensure_pendulum_datetime(datetime.end_value).in_tz(
132
+ timezone
133
+ )
134
+
135
+ list_of_interval = find_intervals(
136
+ current_date=start_date_tz_adjusted,
137
+ end_date=end_date_tz_adjusted,
138
+ interval_days=interval_days,
139
+ )
140
+
141
+ for start, end in list_of_interval:
142
+ yield tiktok_api.fetch_pages(
143
+ advertiser_ids=advertiser_ids,
144
+ start_time=start,
145
+ end_time=end,
146
+ dimensions=dimensions,
147
+ metrics=metrics,
148
+ )
149
+
150
+ return custom_reports
@@ -0,0 +1,130 @@
1
+ import json
2
+
3
+ import requests
4
+ from dlt.common.time import ensure_pendulum_datetime
5
+ from dlt.sources.helpers.requests import Client
6
+
7
+ BASE_URL = "https://business-api.tiktok.com/open_api/v1.3/report/integrated/get/"
8
+
9
+
10
+ def retry_on_limit(
11
+ response: requests.Response | None, exception: BaseException | None
12
+ ) -> bool:
13
+ if response is None:
14
+ return False
15
+ return response.status_code == 429
16
+
17
+
18
+ def create_client() -> requests.Session:
19
+ return Client(
20
+ raise_for_status=False,
21
+ retry_condition=retry_on_limit,
22
+ request_max_attempts=12,
23
+ request_backoff_factor=2,
24
+ ).session
25
+
26
+
27
+ def flat_structure(items, timezone="UTC"):
28
+ for item in items:
29
+ if "dimensions" in item:
30
+ for key, value in item["dimensions"].items():
31
+ if key == "stat_time_day":
32
+ item["stat_time_day"] = ensure_pendulum_datetime(value).in_tz(
33
+ timezone
34
+ )
35
+ elif key == "stat_time_hour":
36
+ item["stat_time_hour"] = ensure_pendulum_datetime(value).in_tz(
37
+ timezone
38
+ )
39
+ else:
40
+ item[key] = value
41
+ del item["dimensions"]
42
+
43
+ for key, value in item["metrics"].items():
44
+ item[key] = value
45
+ del item["metrics"]
46
+
47
+ return items
48
+
49
+
50
+ class TikTokAPI:
51
+ def __init__(
52
+ self,
53
+ access_token,
54
+ timezone,
55
+ page_size,
56
+ filtering_param,
57
+ filter_name,
58
+ filter_value,
59
+ ):
60
+ self.headers = {
61
+ "Access-Token": access_token,
62
+ }
63
+ self.timezone = timezone
64
+ self.page_size = page_size
65
+ self.filtering_param = filtering_param
66
+ self.filter_name = filter_name
67
+ self.filter_value = filter_value
68
+
69
+ def fetch_pages(
70
+ self, advertiser_ids: list[str], start_time, end_time, dimensions, metrics
71
+ ):
72
+ data_level_mapping = {
73
+ "advertiser_id": "AUCTION_ADVERTISER",
74
+ "campaign_id": "AUCTION_CAMPAIGN",
75
+ "adgroup_id": "AUCTION_ADGROUP",
76
+ }
77
+
78
+ data_level = "AUCTION_AD"
79
+ for id_dimension in dimensions:
80
+ if id_dimension in data_level_mapping:
81
+ data_level = data_level_mapping[id_dimension]
82
+ break
83
+
84
+ current_page = 1
85
+ start_time = ensure_pendulum_datetime(start_time).to_date_string()
86
+ end_time = ensure_pendulum_datetime(end_time).to_date_string()
87
+
88
+ filtering = [
89
+ {
90
+ "field_name": self.filter_name,
91
+ "filter_type": "IN",
92
+ "filter_value": json.dumps(self.filter_value),
93
+ }
94
+ ]
95
+ params = {
96
+ "advertiser_ids": json.dumps(advertiser_ids),
97
+ "report_type": "BASIC",
98
+ "data_level": data_level,
99
+ "start_date": start_time,
100
+ "end_date": end_time,
101
+ "page_size": self.page_size,
102
+ "dimensions": json.dumps(dimensions),
103
+ "metrics": json.dumps(metrics),
104
+ }
105
+
106
+ if self.filtering_param:
107
+ params["filtering"] = json.dumps(filtering)
108
+ client = create_client()
109
+ while True:
110
+ params["page"] = current_page
111
+ response = client.get(url=BASE_URL, headers=self.headers, params=params)
112
+
113
+ result = response.json()
114
+ if result.get("message") != "OK":
115
+ raise ValueError(result.get("message", ""))
116
+
117
+ result_data = result.get("data", {})
118
+ items = result_data.get("list", [])
119
+
120
+ flat_structure(items=items, timezone=self.timezone)
121
+
122
+ yield items
123
+
124
+ page_info = result_data.get("page_info", {})
125
+ total_pages = page_info.get("total_page", 1)
126
+
127
+ if current_page >= total_pages:
128
+ break
129
+
130
+ current_page += 1
omniload/src/time.py ADDED
@@ -0,0 +1,11 @@
1
+ import datetime
2
+ from typing import Optional
3
+
4
+
5
+ def isotime(dt: Optional[datetime.datetime]) -> Optional[str]:
6
+ """
7
+ Converts a datetime object to an iso 8601 formatted string.
8
+ """
9
+ if dt is None:
10
+ return None
11
+ return dt.isoformat()
@@ -0,0 +1,48 @@
1
+ """Trustpilot source for ingesting reviews."""
2
+
3
+ from typing import Any, Dict, Generator, Iterable
4
+
5
+ import dlt
6
+ import pendulum
7
+ from dlt.sources import DltResource
8
+
9
+ from .client import TrustpilotClient
10
+
11
+
12
+ @dlt.source()
13
+ def trustpilot_source(
14
+ business_unit_id: str,
15
+ start_date: str,
16
+ end_date: str | None,
17
+ api_key: str,
18
+ per_page: int = 1000,
19
+ ) -> Iterable[DltResource]:
20
+ """Return resources for Trustpilot."""
21
+
22
+ client = TrustpilotClient(api_key=api_key)
23
+
24
+ @dlt.resource(name="reviews", write_disposition="merge", primary_key="id")
25
+ def reviews(
26
+ dateTime=(
27
+ dlt.sources.incremental(
28
+ "updated_at",
29
+ initial_value=start_date,
30
+ end_value=end_date,
31
+ range_start="closed",
32
+ range_end="closed",
33
+ )
34
+ ),
35
+ ) -> Generator[Dict[str, Any], None, None]:
36
+ if end_date is None:
37
+ end_dt = pendulum.now(tz="UTC").isoformat()
38
+ else:
39
+ end_dt = dateTime.end_value
40
+ start_dt = dateTime.last_value
41
+ yield from client.paginated_reviews(
42
+ business_unit_id=business_unit_id,
43
+ per_page=per_page,
44
+ updated_since=start_dt,
45
+ end_date=end_dt,
46
+ )
47
+
48
+ yield reviews
@@ -0,0 +1,48 @@
1
+ """Simple Trustpilot API client."""
2
+
3
+ from typing import Any, Dict, Iterable
4
+
5
+ import pendulum
6
+ from dlt.sources.helpers import requests
7
+
8
+
9
+ class TrustpilotClient:
10
+ """Client for the Trustpilot public API."""
11
+
12
+ def __init__(self, api_key: str) -> None:
13
+ self.api_key = api_key
14
+ self.base_url = "https://api.trustpilot.com/v1"
15
+
16
+ def _get(self, endpoint: str, params: Dict[str, Any]) -> Dict[str, Any]:
17
+ params = dict(params)
18
+ params["apikey"] = self.api_key
19
+ response = requests.get(f"{self.base_url}{endpoint}", params=params)
20
+ response.raise_for_status()
21
+ return response.json()
22
+
23
+ def paginated_reviews(
24
+ self,
25
+ business_unit_id: str,
26
+ updated_since: str,
27
+ end_date: str,
28
+ per_page: int = 1000,
29
+ ) -> Iterable[Dict[str, Any]]:
30
+ page = 1
31
+ while True:
32
+ params: Dict[str, Any] = {"perPage": per_page, "page": page}
33
+ if updated_since:
34
+ params["updatedSince"] = updated_since
35
+ data = self._get(f"/business-units/{business_unit_id}/reviews", params)
36
+ reviews = data.get("reviews", data)
37
+ if not reviews:
38
+ break
39
+ for review in reviews:
40
+ end_date_dt = pendulum.parse(end_date)
41
+ review["updated_at"] = review["updatedAt"]
42
+ review_dt = pendulum.parse(review["updated_at"])
43
+ if review_dt > end_date_dt: # type: ignore
44
+ continue
45
+ yield review
46
+ if len(reviews) < per_page:
47
+ break
48
+ page += 1
@@ -0,0 +1,6 @@
1
+ try:
2
+ from omniload.src import buildinfo # type: ignore[import-not-found,attr-defined]
3
+
4
+ __version__ = buildinfo.version.lstrip("v")
5
+ except ImportError:
6
+ __version__ = "0.0.0-dev"
@@ -0,0 +1,68 @@
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
+
8
+ from .client import WiseClient
9
+
10
+
11
+ @dlt.source(max_table_nesting=0)
12
+ def wise_source(
13
+ api_key: str,
14
+ start_date: pendulum.DateTime,
15
+ end_date: pendulum.DateTime | None = None,
16
+ ) -> Iterable[DltResource]:
17
+ client = WiseClient(api_key)
18
+
19
+ # List of all profiles belonging to user.
20
+ @dlt.resource(write_disposition="merge", name="profiles", primary_key="id")
21
+ def profiles() -> Iterable[TDataItem]:
22
+ yield from client.fetch_profiles()
23
+
24
+ # List transfers for a profile.
25
+ @dlt.resource(write_disposition="merge", name="transfers", primary_key="id")
26
+ def transfers(
27
+ profiles=profiles,
28
+ datetime=dlt.sources.incremental(
29
+ "created",
30
+ initial_value=start_date,
31
+ end_value=end_date,
32
+ range_end="closed",
33
+ range_start="closed",
34
+ ),
35
+ ):
36
+ if datetime.end_value is None:
37
+ end_dt = pendulum.now(tz="UTC")
38
+ else:
39
+ end_dt = datetime.end_value
40
+
41
+ start_dt = datetime.last_value
42
+
43
+ for profile in profiles:
44
+ yield from client.fetch_transfers(profile["id"], start_dt, end_dt)
45
+
46
+ # Retrieve the user's multi-currency account balance accounts. It returns all balance accounts the profile has.
47
+ @dlt.resource(write_disposition="merge", name="balances", primary_key="id")
48
+ def balances(
49
+ profiles=profiles,
50
+ datetime=dlt.sources.incremental(
51
+ "modificationTime",
52
+ initial_value=start_date,
53
+ end_value=end_date,
54
+ range_end="closed",
55
+ range_start="closed",
56
+ ),
57
+ ) -> Iterable[TDataItem]:
58
+ if datetime.end_value is None:
59
+ end_dt = pendulum.now(tz="UTC")
60
+ else:
61
+ end_dt = datetime.end_value
62
+
63
+ start_dt = datetime.last_value
64
+
65
+ for profile in profiles:
66
+ yield from client.fetch_balances(profile["id"], start_dt, end_dt)
67
+
68
+ return profiles, transfers, balances
@@ -0,0 +1,63 @@
1
+ from typing import Iterable
2
+
3
+ import pendulum
4
+ from dlt.sources.helpers.requests import Client
5
+
6
+
7
+ class WiseClient:
8
+ BASE_URL = "https://api.transferwise.com"
9
+
10
+ def __init__(self, api_key: str) -> None:
11
+ self.session = Client(raise_for_status=False).session
12
+ self.session.headers.update({"Authorization": f"Bearer {api_key}"})
13
+
14
+ # https://docs.wise.com/api-docs/api-reference/profile#list-profiles
15
+ def fetch_profiles(self) -> Iterable[dict]:
16
+ url = f"{self.BASE_URL}/v2/profiles"
17
+ resp = self.session.get(url)
18
+ resp.raise_for_status()
19
+ for profile in resp.json():
20
+ yield profile
21
+
22
+ # https://docs.wise.com/api-docs/api-reference/transfer#list-transfers
23
+ def fetch_transfers(
24
+ self, profile_id: str, start_time=pendulum.DateTime, end_time=pendulum.DateTime
25
+ ):
26
+ offset = 0
27
+
28
+ while True:
29
+ data = self.session.get(
30
+ f"{self.BASE_URL}/v1/transfers",
31
+ params={
32
+ "profile": profile_id,
33
+ "createdDateStart": start_time.to_date_string(),
34
+ "createdDateEnd": end_time.to_date_string(),
35
+ "limit": 100,
36
+ "offset": offset,
37
+ },
38
+ )
39
+ response_data = data.json()
40
+
41
+ if not response_data or len(response_data) == 0:
42
+ break
43
+
44
+ for transfer in response_data:
45
+ transfer["created"] = pendulum.parse(transfer["created"])
46
+
47
+ yield transfer
48
+ offset += 100
49
+
50
+ # https://docs.wise.com/api-docs/api-reference/balance#list
51
+ def fetch_balances(
52
+ self, profile_id: str, start_time=pendulum.DateTime, end_time=pendulum.DateTime
53
+ ) -> Iterable[dict]:
54
+ url = f"{self.BASE_URL}/v4/profiles/{profile_id}/balances"
55
+ resp = self.session.get(url, params={"types": "STANDARD,SAVINGS"})
56
+ resp.raise_for_status()
57
+ for balance in resp.json():
58
+ balance["modificationTime"] = pendulum.parse(balance["modificationTime"])
59
+ if (
60
+ balance["modificationTime"] > start_time
61
+ and balance["modificationTime"] < end_time
62
+ ):
63
+ yield balance