ingestr 0.13.2__py3-none-any.whl → 0.14.104__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.
- ingestr/conftest.py +72 -0
- ingestr/main.py +134 -87
- ingestr/src/adjust/__init__.py +4 -4
- ingestr/src/adjust/adjust_helpers.py +7 -3
- ingestr/src/airtable/__init__.py +3 -2
- ingestr/src/allium/__init__.py +128 -0
- ingestr/src/anthropic/__init__.py +277 -0
- ingestr/src/anthropic/helpers.py +525 -0
- ingestr/src/applovin/__init__.py +262 -0
- ingestr/src/applovin_max/__init__.py +117 -0
- ingestr/src/appsflyer/__init__.py +325 -0
- ingestr/src/appsflyer/client.py +49 -45
- ingestr/src/appstore/__init__.py +1 -0
- ingestr/src/arrow/__init__.py +9 -1
- ingestr/src/asana_source/__init__.py +1 -1
- ingestr/src/attio/__init__.py +102 -0
- ingestr/src/attio/helpers.py +65 -0
- ingestr/src/blob.py +38 -11
- ingestr/src/buildinfo.py +1 -0
- ingestr/src/chess/__init__.py +1 -1
- ingestr/src/clickup/__init__.py +85 -0
- ingestr/src/clickup/helpers.py +47 -0
- ingestr/src/collector/spinner.py +43 -0
- ingestr/src/couchbase_source/__init__.py +118 -0
- ingestr/src/couchbase_source/helpers.py +135 -0
- ingestr/src/cursor/__init__.py +83 -0
- ingestr/src/cursor/helpers.py +188 -0
- ingestr/src/destinations.py +520 -33
- ingestr/src/docebo/__init__.py +589 -0
- ingestr/src/docebo/client.py +435 -0
- ingestr/src/docebo/helpers.py +97 -0
- ingestr/src/elasticsearch/__init__.py +80 -0
- ingestr/src/elasticsearch/helpers.py +138 -0
- ingestr/src/errors.py +8 -0
- ingestr/src/facebook_ads/__init__.py +47 -28
- ingestr/src/facebook_ads/helpers.py +59 -37
- ingestr/src/facebook_ads/settings.py +2 -0
- ingestr/src/facebook_ads/utils.py +39 -0
- ingestr/src/factory.py +116 -2
- ingestr/src/filesystem/__init__.py +8 -3
- ingestr/src/filters.py +46 -3
- ingestr/src/fluxx/__init__.py +9906 -0
- ingestr/src/fluxx/helpers.py +209 -0
- ingestr/src/frankfurter/__init__.py +157 -0
- ingestr/src/frankfurter/helpers.py +48 -0
- ingestr/src/freshdesk/__init__.py +89 -0
- ingestr/src/freshdesk/freshdesk_client.py +137 -0
- ingestr/src/freshdesk/settings.py +9 -0
- ingestr/src/fundraiseup/__init__.py +95 -0
- ingestr/src/fundraiseup/client.py +81 -0
- ingestr/src/github/__init__.py +41 -6
- ingestr/src/github/helpers.py +5 -5
- ingestr/src/google_analytics/__init__.py +22 -4
- ingestr/src/google_analytics/helpers.py +124 -6
- ingestr/src/google_sheets/__init__.py +4 -4
- ingestr/src/google_sheets/helpers/data_processing.py +2 -2
- ingestr/src/hostaway/__init__.py +302 -0
- ingestr/src/hostaway/client.py +288 -0
- ingestr/src/http/__init__.py +35 -0
- ingestr/src/http/readers.py +114 -0
- ingestr/src/http_client.py +24 -0
- ingestr/src/hubspot/__init__.py +66 -23
- ingestr/src/hubspot/helpers.py +52 -22
- ingestr/src/hubspot/settings.py +14 -7
- ingestr/src/influxdb/__init__.py +46 -0
- ingestr/src/influxdb/client.py +34 -0
- ingestr/src/intercom/__init__.py +142 -0
- ingestr/src/intercom/helpers.py +674 -0
- ingestr/src/intercom/settings.py +279 -0
- ingestr/src/isoc_pulse/__init__.py +159 -0
- ingestr/src/jira_source/__init__.py +340 -0
- ingestr/src/jira_source/helpers.py +439 -0
- ingestr/src/jira_source/settings.py +170 -0
- ingestr/src/kafka/__init__.py +4 -1
- ingestr/src/kinesis/__init__.py +139 -0
- ingestr/src/kinesis/helpers.py +82 -0
- ingestr/src/klaviyo/{_init_.py → __init__.py} +5 -6
- ingestr/src/linear/__init__.py +634 -0
- ingestr/src/linear/helpers.py +111 -0
- ingestr/src/linkedin_ads/helpers.py +0 -1
- ingestr/src/loader.py +69 -0
- ingestr/src/mailchimp/__init__.py +126 -0
- ingestr/src/mailchimp/helpers.py +226 -0
- ingestr/src/mailchimp/settings.py +164 -0
- ingestr/src/masking.py +344 -0
- ingestr/src/mixpanel/__init__.py +62 -0
- ingestr/src/mixpanel/client.py +99 -0
- ingestr/src/monday/__init__.py +246 -0
- ingestr/src/monday/helpers.py +392 -0
- ingestr/src/monday/settings.py +328 -0
- ingestr/src/mongodb/__init__.py +72 -8
- ingestr/src/mongodb/helpers.py +915 -38
- ingestr/src/partition.py +32 -0
- ingestr/src/personio/__init__.py +331 -0
- ingestr/src/personio/helpers.py +86 -0
- ingestr/src/phantombuster/__init__.py +65 -0
- ingestr/src/phantombuster/client.py +87 -0
- ingestr/src/pinterest/__init__.py +82 -0
- ingestr/src/pipedrive/__init__.py +198 -0
- ingestr/src/pipedrive/helpers/__init__.py +23 -0
- ingestr/src/pipedrive/helpers/custom_fields_munger.py +102 -0
- ingestr/src/pipedrive/helpers/pages.py +115 -0
- ingestr/src/pipedrive/settings.py +27 -0
- ingestr/src/pipedrive/typing.py +3 -0
- ingestr/src/plusvibeai/__init__.py +335 -0
- ingestr/src/plusvibeai/helpers.py +544 -0
- ingestr/src/plusvibeai/settings.py +252 -0
- ingestr/src/quickbooks/__init__.py +117 -0
- ingestr/src/resource.py +40 -0
- ingestr/src/revenuecat/__init__.py +83 -0
- ingestr/src/revenuecat/helpers.py +237 -0
- ingestr/src/salesforce/__init__.py +156 -0
- ingestr/src/salesforce/helpers.py +64 -0
- ingestr/src/shopify/__init__.py +1 -17
- ingestr/src/smartsheets/__init__.py +82 -0
- ingestr/src/snapchat_ads/__init__.py +489 -0
- ingestr/src/snapchat_ads/client.py +72 -0
- ingestr/src/snapchat_ads/helpers.py +535 -0
- ingestr/src/socrata_source/__init__.py +83 -0
- ingestr/src/socrata_source/helpers.py +85 -0
- ingestr/src/socrata_source/settings.py +8 -0
- ingestr/src/solidgate/__init__.py +219 -0
- ingestr/src/solidgate/helpers.py +154 -0
- ingestr/src/sources.py +3132 -212
- ingestr/src/stripe_analytics/__init__.py +49 -21
- ingestr/src/stripe_analytics/helpers.py +286 -1
- ingestr/src/stripe_analytics/settings.py +62 -10
- ingestr/src/telemetry/event.py +10 -9
- ingestr/src/tiktok_ads/__init__.py +12 -6
- ingestr/src/tiktok_ads/tiktok_helpers.py +0 -1
- ingestr/src/trustpilot/__init__.py +48 -0
- ingestr/src/trustpilot/client.py +48 -0
- ingestr/src/version.py +6 -1
- ingestr/src/wise/__init__.py +68 -0
- ingestr/src/wise/client.py +63 -0
- ingestr/src/zoom/__init__.py +99 -0
- ingestr/src/zoom/helpers.py +102 -0
- ingestr/tests/unit/test_smartsheets.py +133 -0
- ingestr-0.14.104.dist-info/METADATA +563 -0
- ingestr-0.14.104.dist-info/RECORD +203 -0
- ingestr/src/appsflyer/_init_.py +0 -24
- ingestr-0.13.2.dist-info/METADATA +0 -302
- ingestr-0.13.2.dist-info/RECORD +0 -107
- {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/WHEEL +0 -0
- {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/entry_points.txt +0 -0
- {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""PlusVibeAI source settings and constants"""
|
|
2
|
+
|
|
3
|
+
# Default start date for PlusVibeAI API requests
|
|
4
|
+
DEFAULT_START_DATE = "2020-01-01"
|
|
5
|
+
|
|
6
|
+
# PlusVibeAI API request timeout in seconds
|
|
7
|
+
REQUEST_TIMEOUT = 300
|
|
8
|
+
|
|
9
|
+
# Default page size for paginated requests
|
|
10
|
+
DEFAULT_PAGE_SIZE = 100
|
|
11
|
+
|
|
12
|
+
# Maximum page size (adjust based on API limits)
|
|
13
|
+
MAX_PAGE_SIZE = 1000
|
|
14
|
+
|
|
15
|
+
# Base API path for PlusVibeAI
|
|
16
|
+
API_BASE_PATH = "/api/v1"
|
|
17
|
+
|
|
18
|
+
# Campaign fields to retrieve from PlusVibeAI API
|
|
19
|
+
CAMPAIGN_FIELDS = (
|
|
20
|
+
# Basic Information
|
|
21
|
+
"id",
|
|
22
|
+
"camp_name",
|
|
23
|
+
"parent_camp_id",
|
|
24
|
+
"campaign_type",
|
|
25
|
+
"organization_id",
|
|
26
|
+
"workspace_id",
|
|
27
|
+
"status",
|
|
28
|
+
# Timestamps
|
|
29
|
+
"created_at",
|
|
30
|
+
"modified_at",
|
|
31
|
+
"last_lead_sent",
|
|
32
|
+
"last_paused_at_bounced",
|
|
33
|
+
# Campaign Configuration
|
|
34
|
+
"tags",
|
|
35
|
+
"template_id",
|
|
36
|
+
"email_accounts",
|
|
37
|
+
"daily_limit",
|
|
38
|
+
"interval_limit_in_min",
|
|
39
|
+
"send_priority",
|
|
40
|
+
"send_as_txt",
|
|
41
|
+
# Tracking & Settings
|
|
42
|
+
"is_emailopened_tracking",
|
|
43
|
+
"is_unsubscribed_link",
|
|
44
|
+
"exclude_ooo",
|
|
45
|
+
"is_acc_based_sending",
|
|
46
|
+
"send_risky_email",
|
|
47
|
+
"unsub_blocklist",
|
|
48
|
+
"other_email_acc",
|
|
49
|
+
"is_esp_match",
|
|
50
|
+
"stop_on_lead_replied",
|
|
51
|
+
# Bounce Settings
|
|
52
|
+
"is_pause_on_bouncerate",
|
|
53
|
+
"bounce_rate_limit",
|
|
54
|
+
"is_paused_at_bounced",
|
|
55
|
+
# Schedule
|
|
56
|
+
"schedule",
|
|
57
|
+
"first_wait_time",
|
|
58
|
+
"camp_st_date",
|
|
59
|
+
"camp_end_date",
|
|
60
|
+
# Events & Sequences
|
|
61
|
+
"events",
|
|
62
|
+
"sequences",
|
|
63
|
+
"sequence_steps",
|
|
64
|
+
"camp_emails",
|
|
65
|
+
# Lead Statistics
|
|
66
|
+
"lead_count",
|
|
67
|
+
"completed_lead_count",
|
|
68
|
+
"lead_contacted_count",
|
|
69
|
+
# Email Performance Metrics
|
|
70
|
+
"sent_count",
|
|
71
|
+
"opened_count",
|
|
72
|
+
"unique_opened_count",
|
|
73
|
+
"replied_count",
|
|
74
|
+
"bounced_count",
|
|
75
|
+
"unsubscribed_count",
|
|
76
|
+
# Reply Classification
|
|
77
|
+
"positive_reply_count",
|
|
78
|
+
"negative_reply_count",
|
|
79
|
+
"neutral_reply_count",
|
|
80
|
+
# Daily & Business Metrics
|
|
81
|
+
"email_sent_today",
|
|
82
|
+
"opportunity_val",
|
|
83
|
+
"open_rate",
|
|
84
|
+
"replied_rate",
|
|
85
|
+
# Custom Data
|
|
86
|
+
"custom_fields",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Lead fields to retrieve from PlusVibeAI API
|
|
90
|
+
LEAD_FIELDS = (
|
|
91
|
+
# Basic Information
|
|
92
|
+
"_id",
|
|
93
|
+
"organization_id",
|
|
94
|
+
"campaign_id",
|
|
95
|
+
"workspace_id",
|
|
96
|
+
# Lead Status & Progress
|
|
97
|
+
"is_completed",
|
|
98
|
+
"current_step",
|
|
99
|
+
"status",
|
|
100
|
+
"label",
|
|
101
|
+
# Email Account Info
|
|
102
|
+
"email_account_id",
|
|
103
|
+
"email_acc_name",
|
|
104
|
+
# Campaign Info
|
|
105
|
+
"camp_name",
|
|
106
|
+
# Timestamps
|
|
107
|
+
"created_at",
|
|
108
|
+
"modified_at",
|
|
109
|
+
"last_sent_at",
|
|
110
|
+
# Email Engagement Metrics
|
|
111
|
+
"sent_step",
|
|
112
|
+
"replied_count",
|
|
113
|
+
"opened_count",
|
|
114
|
+
# Email Verification
|
|
115
|
+
"is_mx",
|
|
116
|
+
"mx",
|
|
117
|
+
# Contact Information
|
|
118
|
+
"email",
|
|
119
|
+
"first_name",
|
|
120
|
+
"last_name",
|
|
121
|
+
"phone_number",
|
|
122
|
+
# Address Information
|
|
123
|
+
"address_line",
|
|
124
|
+
"city",
|
|
125
|
+
"state",
|
|
126
|
+
"country",
|
|
127
|
+
"country_code",
|
|
128
|
+
# Professional Information
|
|
129
|
+
"job_title",
|
|
130
|
+
"department",
|
|
131
|
+
"company_name",
|
|
132
|
+
"company_website",
|
|
133
|
+
"industry",
|
|
134
|
+
# Social Media
|
|
135
|
+
"linkedin_person_url",
|
|
136
|
+
"linkedin_company_url",
|
|
137
|
+
# Workflow
|
|
138
|
+
"total_steps",
|
|
139
|
+
# Bounce Information
|
|
140
|
+
"bounce_msg",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Email Account fields to retrieve from PlusVibeAI API
|
|
144
|
+
EMAIL_ACCOUNT_FIELDS = (
|
|
145
|
+
# Basic Information
|
|
146
|
+
"_id",
|
|
147
|
+
"email",
|
|
148
|
+
"status",
|
|
149
|
+
"warmup_status",
|
|
150
|
+
# Timestamps
|
|
151
|
+
"timestamp_created",
|
|
152
|
+
"timestamp_updated",
|
|
153
|
+
# Payload - nested object containing all configuration
|
|
154
|
+
"payload",
|
|
155
|
+
# Payload sub-fields (for reference, stored in payload object):
|
|
156
|
+
# - name (first_name, last_name)
|
|
157
|
+
# - warmup (limit, warmup_custom_words, warmup_signature, advanced, increment, reply_rate)
|
|
158
|
+
# - imap_host, imap_port
|
|
159
|
+
# - smtp_host, smtp_port
|
|
160
|
+
# - daily_limit, sending_gap
|
|
161
|
+
# - reply_to, custom_domain, signature
|
|
162
|
+
# - tags, cmps
|
|
163
|
+
# - analytics (health_scores, reply_rates, daily_counters)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Email fields to retrieve from PlusVibeAI API
|
|
167
|
+
EMAIL_FIELDS = (
|
|
168
|
+
# Basic Information
|
|
169
|
+
"id",
|
|
170
|
+
"message_id",
|
|
171
|
+
"is_unread",
|
|
172
|
+
# Lead Information
|
|
173
|
+
"lead",
|
|
174
|
+
"lead_id",
|
|
175
|
+
"campaign_id",
|
|
176
|
+
# From Address
|
|
177
|
+
"from_address_email",
|
|
178
|
+
"from_address_json",
|
|
179
|
+
# Subject & Content
|
|
180
|
+
"subject",
|
|
181
|
+
"content_preview",
|
|
182
|
+
"body",
|
|
183
|
+
# Headers & Metadata
|
|
184
|
+
"headers",
|
|
185
|
+
"label",
|
|
186
|
+
"thread_id",
|
|
187
|
+
"eaccount",
|
|
188
|
+
# To/CC/BCC Addresses
|
|
189
|
+
"to_address_email_list",
|
|
190
|
+
"to_address_json",
|
|
191
|
+
"cc_address_email_list",
|
|
192
|
+
"cc_address_json",
|
|
193
|
+
"bcc_address_email_list",
|
|
194
|
+
# Timestamps
|
|
195
|
+
"timestamp_created",
|
|
196
|
+
"source_modified_at",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Blocklist fields to retrieve from PlusVibeAI API
|
|
200
|
+
BLOCKLIST_FIELDS = (
|
|
201
|
+
# Basic Information
|
|
202
|
+
"_id",
|
|
203
|
+
"workspace_id",
|
|
204
|
+
"value",
|
|
205
|
+
"created_by_label",
|
|
206
|
+
# Timestamps
|
|
207
|
+
"created_at",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Webhook fields to retrieve from PlusVibeAI API
|
|
211
|
+
WEBHOOK_FIELDS = (
|
|
212
|
+
# Basic Information
|
|
213
|
+
"_id",
|
|
214
|
+
"workspace_id",
|
|
215
|
+
"org_id",
|
|
216
|
+
"url",
|
|
217
|
+
"name",
|
|
218
|
+
"secret",
|
|
219
|
+
# Configuration
|
|
220
|
+
"camp_ids",
|
|
221
|
+
"evt_types",
|
|
222
|
+
"status",
|
|
223
|
+
"integration_type",
|
|
224
|
+
# Settings
|
|
225
|
+
"ignore_ooo",
|
|
226
|
+
"ignore_automatic",
|
|
227
|
+
# Timestamps
|
|
228
|
+
"created_at",
|
|
229
|
+
"modified_at",
|
|
230
|
+
"last_run",
|
|
231
|
+
# Response Data
|
|
232
|
+
"last_resp",
|
|
233
|
+
"last_recv_resp",
|
|
234
|
+
# User Information
|
|
235
|
+
"created_by",
|
|
236
|
+
"modified_by",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Tag fields to retrieve from PlusVibeAI API
|
|
240
|
+
TAG_FIELDS = (
|
|
241
|
+
# Basic Information
|
|
242
|
+
"_id",
|
|
243
|
+
"workspace_id",
|
|
244
|
+
"org_id",
|
|
245
|
+
"name",
|
|
246
|
+
"color",
|
|
247
|
+
"description",
|
|
248
|
+
"status",
|
|
249
|
+
# Timestamps
|
|
250
|
+
"created_at",
|
|
251
|
+
"modified_at",
|
|
252
|
+
)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""QuickBooks source built on top of python-quickbooks."""
|
|
2
|
+
|
|
3
|
+
from typing import Iterable, Iterator, List, Optional
|
|
4
|
+
|
|
5
|
+
import dlt
|
|
6
|
+
import pendulum
|
|
7
|
+
from dlt.common.time import ensure_pendulum_datetime
|
|
8
|
+
from dlt.common.typing import TDataItem
|
|
9
|
+
from dlt.sources import DltResource
|
|
10
|
+
from intuitlib.client import AuthClient # type: ignore
|
|
11
|
+
|
|
12
|
+
from quickbooks import QuickBooks # type: ignore
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dlt.source(name="quickbooks", max_table_nesting=0)
|
|
16
|
+
def quickbooks_source(
|
|
17
|
+
company_id: str,
|
|
18
|
+
start_date: pendulum.DateTime,
|
|
19
|
+
object: str,
|
|
20
|
+
end_date: pendulum.DateTime | None,
|
|
21
|
+
client_id: str,
|
|
22
|
+
client_secret: str,
|
|
23
|
+
refresh_token: str,
|
|
24
|
+
environment: str = "production",
|
|
25
|
+
minor_version: Optional[str] = None,
|
|
26
|
+
) -> Iterable[DltResource]:
|
|
27
|
+
"""Create dlt resources for QuickBooks objects.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
company_id: str
|
|
32
|
+
QuickBooks company id (realm id).
|
|
33
|
+
client_id: str
|
|
34
|
+
OAuth client id.
|
|
35
|
+
client_secret: str
|
|
36
|
+
OAuth client secret.
|
|
37
|
+
refresh_token: str
|
|
38
|
+
OAuth refresh token.
|
|
39
|
+
access_token: Optional[str]
|
|
40
|
+
Optional access token. If not provided the library will refresh using the
|
|
41
|
+
provided refresh token.
|
|
42
|
+
environment: str
|
|
43
|
+
Either ``"production"`` or ``"sandbox"``.
|
|
44
|
+
minor_version: Optional[int]
|
|
45
|
+
QuickBooks API minor version if needed.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
auth_client = AuthClient(
|
|
49
|
+
client_id=client_id,
|
|
50
|
+
client_secret=client_secret,
|
|
51
|
+
environment=environment,
|
|
52
|
+
# redirect_uri is not used since we authenticate using refresh token which skips the step of redirect callback.
|
|
53
|
+
# as redirect_uri is required param, we are passing empty string.
|
|
54
|
+
redirect_uri="",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# https://help.developer.intuit.com/s/article/Validity-of-Refresh-Token
|
|
58
|
+
client = QuickBooks(
|
|
59
|
+
auth_client=auth_client,
|
|
60
|
+
refresh_token=refresh_token,
|
|
61
|
+
company_id=company_id,
|
|
62
|
+
minorversion=minor_version,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def fetch_object(
|
|
66
|
+
obj_name: str,
|
|
67
|
+
updated_at: dlt.sources.incremental[str] = dlt.sources.incremental(
|
|
68
|
+
"lastupdatedtime",
|
|
69
|
+
initial_value=start_date, # type: ignore
|
|
70
|
+
end_value=end_date, # type: ignore
|
|
71
|
+
range_start="closed",
|
|
72
|
+
range_end="closed",
|
|
73
|
+
allow_external_schedulers=True,
|
|
74
|
+
),
|
|
75
|
+
) -> Iterator[List[TDataItem]]:
|
|
76
|
+
start_pos = 1
|
|
77
|
+
|
|
78
|
+
end_dt = updated_at.end_value or pendulum.now(tz="UTC")
|
|
79
|
+
start_dt = ensure_pendulum_datetime(str(updated_at.last_value)).in_tz("UTC")
|
|
80
|
+
|
|
81
|
+
start_str = start_dt.isoformat()
|
|
82
|
+
end_str = end_dt.isoformat()
|
|
83
|
+
|
|
84
|
+
where_clause = f"WHERE MetaData.LastUpdatedTime >= '{start_str}' AND MetaData.LastUpdatedTime < '{end_str}'"
|
|
85
|
+
while True:
|
|
86
|
+
query = (
|
|
87
|
+
f"SELECT * FROM {obj_name} {where_clause} "
|
|
88
|
+
f"ORDERBY MetaData.LastUpdatedTime ASC STARTPOSITION {start_pos} MAXRESULTS 1000"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
result = client.query(query)
|
|
92
|
+
|
|
93
|
+
items = result.get("QueryResponse", {}).get(obj_name.capitalize(), [])
|
|
94
|
+
if not items:
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
for item in items:
|
|
98
|
+
if item.get("MetaData") and item["MetaData"].get("LastUpdatedTime"):
|
|
99
|
+
item["lastupdatedtime"] = ensure_pendulum_datetime(
|
|
100
|
+
item["MetaData"]["LastUpdatedTime"]
|
|
101
|
+
)
|
|
102
|
+
item["id"] = item["Id"]
|
|
103
|
+
del item["Id"]
|
|
104
|
+
|
|
105
|
+
yield item
|
|
106
|
+
|
|
107
|
+
if len(items) < 1000:
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
start_pos += 1000
|
|
111
|
+
|
|
112
|
+
yield dlt.resource(
|
|
113
|
+
fetch_object,
|
|
114
|
+
name=object.lower(),
|
|
115
|
+
write_disposition="merge",
|
|
116
|
+
primary_key="id",
|
|
117
|
+
)(object)
|
ingestr/src/resource.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
from dlt.sources import DltResource, DltSource
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def for_each(
|
|
7
|
+
source: DltSource | DltResource, ex: Callable[[DltResource], None | DltResource]
|
|
8
|
+
):
|
|
9
|
+
"""
|
|
10
|
+
Apply a function to each resource in a source.
|
|
11
|
+
"""
|
|
12
|
+
if hasattr(source, "selected_resources") and source.selected_resources:
|
|
13
|
+
resource_names = list(source.selected_resources.keys())
|
|
14
|
+
for res in resource_names:
|
|
15
|
+
ex(source.resources[res]) # type: ignore[union-attr]
|
|
16
|
+
else:
|
|
17
|
+
ex(source) # type: ignore[arg-type]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TypeHintMap:
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.handled_typehints = False
|
|
23
|
+
|
|
24
|
+
def type_hint_map(self, item):
|
|
25
|
+
if self.handled_typehints:
|
|
26
|
+
return item
|
|
27
|
+
|
|
28
|
+
array_cols = []
|
|
29
|
+
for col in item:
|
|
30
|
+
if isinstance(item[col], (list, tuple)):
|
|
31
|
+
array_cols.append(col)
|
|
32
|
+
if array_cols:
|
|
33
|
+
import dlt
|
|
34
|
+
|
|
35
|
+
source = dlt.current.source()
|
|
36
|
+
columns = [{"name": col, "data_type": "json"} for col in array_cols]
|
|
37
|
+
for_each(source, lambda x: x.apply_hints(columns=columns))
|
|
38
|
+
|
|
39
|
+
self.handled_typehints = True
|
|
40
|
+
return item
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from typing import Any, Dict, Iterable, Iterator
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
import dlt
|
|
5
|
+
|
|
6
|
+
from .helpers import (
|
|
7
|
+
_make_request,
|
|
8
|
+
_paginate,
|
|
9
|
+
convert_timestamps_to_iso,
|
|
10
|
+
create_project_resource,
|
|
11
|
+
process_customer_with_nested_resources_async,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dlt.source(name="revenuecat", max_table_nesting=0)
|
|
16
|
+
def revenuecat_source(
|
|
17
|
+
api_key: str,
|
|
18
|
+
project_id: str = None,
|
|
19
|
+
) -> Iterable[dlt.sources.DltResource]:
|
|
20
|
+
"""
|
|
21
|
+
RevenueCat source for extracting data from RevenueCat API v2.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
api_key: RevenueCat API v2 secret key with Bearer token format
|
|
25
|
+
project_id: RevenueCat project ID (required for customers, products, entitlements, offerings, subscriptions, purchases)
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Iterable of DLT resources for customers, products, entitlements, offerings, purchases, subscriptions, and projects
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@dlt.resource(name="projects", primary_key="id", write_disposition="merge")
|
|
32
|
+
def projects() -> Iterator[Dict[str, Any]]:
|
|
33
|
+
"""Get list of projects."""
|
|
34
|
+
# Get projects list
|
|
35
|
+
data = _make_request(api_key, "/projects")
|
|
36
|
+
if "items" in data:
|
|
37
|
+
for project in data["items"]:
|
|
38
|
+
project = convert_timestamps_to_iso(project, ["created_at"])
|
|
39
|
+
yield project
|
|
40
|
+
|
|
41
|
+
@dlt.resource(
|
|
42
|
+
name="customer_ids",
|
|
43
|
+
write_disposition="replace",
|
|
44
|
+
selected=False,
|
|
45
|
+
parallelized=True,
|
|
46
|
+
)
|
|
47
|
+
def customer_ids():
|
|
48
|
+
if project_id is None:
|
|
49
|
+
raise ValueError("project_id is required for customers resource")
|
|
50
|
+
|
|
51
|
+
yield _paginate(api_key, f"/projects/{project_id}/customers")
|
|
52
|
+
|
|
53
|
+
@dlt.transformer(
|
|
54
|
+
data_from=customer_ids, write_disposition="replace", parallelized=True
|
|
55
|
+
)
|
|
56
|
+
async def customers(customers) -> Iterator[Dict[str, Any]]:
|
|
57
|
+
async with aiohttp.ClientSession() as session:
|
|
58
|
+
for customer in customers:
|
|
59
|
+
yield await process_customer_with_nested_resources_async(
|
|
60
|
+
session, api_key, project_id, customer
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Create project-dependent resources dynamically
|
|
64
|
+
project_resources = []
|
|
65
|
+
resource_names = ["products", "entitlements", "offerings"]
|
|
66
|
+
|
|
67
|
+
for resource_name in resource_names:
|
|
68
|
+
|
|
69
|
+
@dlt.resource(name=resource_name, primary_key="id", write_disposition="merge")
|
|
70
|
+
def create_resource(resource_name=resource_name) -> Iterator[Dict[str, Any]]:
|
|
71
|
+
"""Get list of project resource."""
|
|
72
|
+
yield from create_project_resource(resource_name, api_key, project_id)
|
|
73
|
+
|
|
74
|
+
# Set the function name for better identification
|
|
75
|
+
create_resource.__name__ = resource_name
|
|
76
|
+
project_resources.append(create_resource)
|
|
77
|
+
|
|
78
|
+
return [
|
|
79
|
+
projects,
|
|
80
|
+
customer_ids,
|
|
81
|
+
customers,
|
|
82
|
+
*project_resources,
|
|
83
|
+
]
|