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