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,218 @@
|
|
|
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
|
+
"""Slack source helpers."""
|
|
16
|
+
|
|
17
|
+
from typing import Any, Generator, Iterable, List, Optional
|
|
18
|
+
from urllib.parse import urljoin
|
|
19
|
+
|
|
20
|
+
import pendulum
|
|
21
|
+
from dlt.common.time import ensure_pendulum_datetime
|
|
22
|
+
from dlt.common.typing import Dict, TAnyDateTime, TDataItem
|
|
23
|
+
from dlt.sources.helpers import requests
|
|
24
|
+
from jsonpath_ng.ext import parse # type: ignore
|
|
25
|
+
|
|
26
|
+
from .settings import MAX_PAGE_SIZE, SLACK_API_URL
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SlackApiException(Exception):
|
|
30
|
+
"""Slack api exception."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PaidOnlyException(SlackApiException):
|
|
34
|
+
"""Slack api exception."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def extract_jsonpath(
|
|
38
|
+
expression: str,
|
|
39
|
+
json_data: TDataItem,
|
|
40
|
+
) -> Generator[Any, None, None]:
|
|
41
|
+
"""Extract records from an input based on a JSONPath expression."""
|
|
42
|
+
if not expression:
|
|
43
|
+
yield json_data
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
jsonpath = parse(expression)
|
|
47
|
+
|
|
48
|
+
for match in jsonpath.find(json_data):
|
|
49
|
+
yield match.value
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def update_jsonpath(expression: str, json_data: TDataItem, value: Any) -> Any:
|
|
53
|
+
"""Update a record in an input based on a JSONPath expression."""
|
|
54
|
+
jsonpath = parse(expression)
|
|
55
|
+
return jsonpath.update_or_create(json_data, value)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def ensure_dt_type(dt: TAnyDateTime, to_ts: bool = False) -> Any:
|
|
59
|
+
"""Converts a datetime to a pendulum datetime or timestamp.
|
|
60
|
+
Args:
|
|
61
|
+
dt: The datetime to convert.
|
|
62
|
+
to_ts: Whether to convert to a timestamp or not.
|
|
63
|
+
Returns:
|
|
64
|
+
Any: The converted datetime or timestamp.
|
|
65
|
+
"""
|
|
66
|
+
if dt is None:
|
|
67
|
+
return None
|
|
68
|
+
out_dt = ensure_pendulum_datetime(dt)
|
|
69
|
+
if to_ts:
|
|
70
|
+
return out_dt.timestamp()
|
|
71
|
+
return out_dt
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SlackAPI:
|
|
75
|
+
"""
|
|
76
|
+
A Slack API client that can be used to get pages of data from Slack.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
access_token: str,
|
|
82
|
+
page_size: int = MAX_PAGE_SIZE,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Args:
|
|
86
|
+
access_token: The private app password to the app on your shop.
|
|
87
|
+
page_size: The max number of items to fetch per page. Defaults to 1000.
|
|
88
|
+
"""
|
|
89
|
+
self.access_token = access_token
|
|
90
|
+
self.page_size = page_size
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def headers(self) -> Dict[str, str]:
|
|
94
|
+
"""Generate the headers to use for the request."""
|
|
95
|
+
return {"Authorization": f"Bearer {self.access_token}"}
|
|
96
|
+
|
|
97
|
+
def parameters(
|
|
98
|
+
self, params: Optional[Dict[str, Any]] = None, next_cursor: str = None
|
|
99
|
+
) -> Dict[str, str]:
|
|
100
|
+
"""
|
|
101
|
+
Generate the query parameters to use for the request.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
params: The query parameters to include in the request.
|
|
105
|
+
next_cursor: The cursor to use to get the next page of results.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The query parameters to use for the request.
|
|
109
|
+
"""
|
|
110
|
+
params = params or {}
|
|
111
|
+
params["limit"] = self.page_size
|
|
112
|
+
if next_cursor:
|
|
113
|
+
params["cursor"] = next_cursor
|
|
114
|
+
return params
|
|
115
|
+
|
|
116
|
+
def url(self, resource: str) -> str:
|
|
117
|
+
"""
|
|
118
|
+
Generate the URL to use for the request.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
resource: The resource to get pages for (e.g. conversations.list).
|
|
122
|
+
"""
|
|
123
|
+
return urljoin(SLACK_API_URL, resource)
|
|
124
|
+
|
|
125
|
+
def _get_next_cursor(self, response: Dict[str, Any]) -> Any:
|
|
126
|
+
"""
|
|
127
|
+
Get the next cursor from the response.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
response: The response from the Slack API.
|
|
131
|
+
"""
|
|
132
|
+
cursor_jsonpath = "$.response_metadata.next_cursor"
|
|
133
|
+
return next(extract_jsonpath(cursor_jsonpath, response), None)
|
|
134
|
+
|
|
135
|
+
def _convert_datetime_fields(
|
|
136
|
+
self, item: Dict[str, Any], datetime_fields: List[str]
|
|
137
|
+
) -> Dict[str, Any]:
|
|
138
|
+
"""Convert timestamp fields in the item to pendulum datetime objects.
|
|
139
|
+
|
|
140
|
+
The item is modified in place.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
item: The item to convert
|
|
144
|
+
datetime_fields: List of fields to convert to pendulum datetime objects.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
The same data item (for convenience)
|
|
148
|
+
"""
|
|
149
|
+
if not datetime_fields:
|
|
150
|
+
return item
|
|
151
|
+
|
|
152
|
+
for field in datetime_fields:
|
|
153
|
+
if timestamp := next(extract_jsonpath(field, item), None):
|
|
154
|
+
if isinstance(timestamp, str):
|
|
155
|
+
timestamp = float(timestamp)
|
|
156
|
+
if timestamp > 1e10:
|
|
157
|
+
timestamp = timestamp / 1000
|
|
158
|
+
pendulum_dt = pendulum.from_timestamp(timestamp)
|
|
159
|
+
item = update_jsonpath(field, item, pendulum_dt)
|
|
160
|
+
return item
|
|
161
|
+
|
|
162
|
+
def get_pages(
|
|
163
|
+
self,
|
|
164
|
+
resource: str,
|
|
165
|
+
response_path: str = None,
|
|
166
|
+
params: Dict[str, Any] = None,
|
|
167
|
+
datetime_fields: List[str] = None,
|
|
168
|
+
context: Dict[str, Any] = None,
|
|
169
|
+
) -> Iterable[TDataItem]:
|
|
170
|
+
"""Get all pages from slack using requests.
|
|
171
|
+
Iterates through all pages and yield each page items.\
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
resource: The resource to get pages for (e.g. conversations.list).
|
|
175
|
+
response_path: The path to the list of items in the response JSON.
|
|
176
|
+
params: Query params to include in the request.
|
|
177
|
+
datetime_fields: List of fields to convert to pendulum datetime objects.
|
|
178
|
+
context: Additional context to add to each item.
|
|
179
|
+
|
|
180
|
+
Yields:
|
|
181
|
+
List of data items from the page
|
|
182
|
+
"""
|
|
183
|
+
has_next_page = True
|
|
184
|
+
next_cursor = None
|
|
185
|
+
|
|
186
|
+
# Iterate through all pages
|
|
187
|
+
while has_next_page:
|
|
188
|
+
# Make the request
|
|
189
|
+
response = requests.get(
|
|
190
|
+
url=self.url(resource),
|
|
191
|
+
headers=self.headers,
|
|
192
|
+
params=self.parameters(params or {}, next_cursor),
|
|
193
|
+
)
|
|
194
|
+
json_response = response.json()
|
|
195
|
+
|
|
196
|
+
# Stop if there was an error
|
|
197
|
+
if not json_response.get("ok"):
|
|
198
|
+
has_next_page = False
|
|
199
|
+
error = json_response.get("error")
|
|
200
|
+
if error == "paid_only":
|
|
201
|
+
raise PaidOnlyException(
|
|
202
|
+
"This resource is just available on paid accounts."
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
raise SlackApiException(error)
|
|
206
|
+
|
|
207
|
+
# Yield the page converting datetime fields
|
|
208
|
+
output = []
|
|
209
|
+
for item in extract_jsonpath(response_path, json_response):
|
|
210
|
+
item = self._convert_datetime_fields(item, datetime_fields)
|
|
211
|
+
item.update(context or {})
|
|
212
|
+
output.append(item)
|
|
213
|
+
yield output
|
|
214
|
+
|
|
215
|
+
# Get the next cursor
|
|
216
|
+
next_cursor = self._get_next_cursor(json_response)
|
|
217
|
+
if not next_cursor:
|
|
218
|
+
has_next_page = False
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
"""Slack source settings and constants"""
|
|
16
|
+
|
|
17
|
+
from dlt.common import pendulum
|
|
18
|
+
|
|
19
|
+
DEFAULT_START_DATE = pendulum.datetime(year=2000, month=1, day=1)
|
|
20
|
+
|
|
21
|
+
SLACK_API_URL = "https://slack.com/api/"
|
|
22
|
+
|
|
23
|
+
MAX_PAGE_SIZE = 1000
|
|
24
|
+
|
|
25
|
+
MSG_DATETIME_FIELDS = [
|
|
26
|
+
"ts",
|
|
27
|
+
"thread_ts",
|
|
28
|
+
"latest_reply",
|
|
29
|
+
"blocks.thread_ts",
|
|
30
|
+
"blocks.latest_reply",
|
|
31
|
+
"attachment.thread_ts",
|
|
32
|
+
"attachment.latest_reply",
|
|
33
|
+
"edited.ts",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
DEFAULT_DATETIME_FIELDS = ["updated", "created"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from typing import Iterable
|
|
2
|
+
|
|
3
|
+
import dlt
|
|
4
|
+
import smartsheet # type: ignore
|
|
5
|
+
from dlt.extract import DltResource
|
|
6
|
+
from smartsheet.models.enums import ColumnType # type: ignore
|
|
7
|
+
from smartsheet.models.sheet import Sheet # type: ignore
|
|
8
|
+
|
|
9
|
+
TYPE_MAPPING = {
|
|
10
|
+
ColumnType.TEXT_NUMBER: "text",
|
|
11
|
+
ColumnType.DATE: "date",
|
|
12
|
+
ColumnType.DATETIME: "timestamp",
|
|
13
|
+
ColumnType.CONTACT_LIST: "text",
|
|
14
|
+
ColumnType.CHECKBOX: "bool",
|
|
15
|
+
ColumnType.PICKLIST: "text",
|
|
16
|
+
ColumnType.DURATION: "text",
|
|
17
|
+
ColumnType.PREDECESSOR: "text",
|
|
18
|
+
ColumnType.ABSTRACT_DATETIME: "timestamp",
|
|
19
|
+
ColumnType.MULTI_CONTACT_LIST: "text",
|
|
20
|
+
ColumnType.MULTI_PICKLIST: "text",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dlt.source
|
|
25
|
+
def smartsheet_source(
|
|
26
|
+
access_token: str,
|
|
27
|
+
sheet_id: str,
|
|
28
|
+
) -> Iterable[DltResource]:
|
|
29
|
+
"""
|
|
30
|
+
A DLT source for Smartsheet.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
access_token: The Smartsheet API access token.
|
|
34
|
+
sheet_id: The ID of the sheet to load.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
An iterable of DLT resources.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Initialize Smartsheet client
|
|
41
|
+
smartsheet_client = smartsheet.Smartsheet(access_token)
|
|
42
|
+
smartsheet_client.errors_as_exceptions(True)
|
|
43
|
+
|
|
44
|
+
# The SDK expects sheet_id to be an int
|
|
45
|
+
sheet_id_int = int(sheet_id)
|
|
46
|
+
# Sanitize the sheet name to be a valid resource name
|
|
47
|
+
# We get objectValue to ensure `name` attribute is populated for the sheet
|
|
48
|
+
sheet_details = smartsheet_client.Sheets.get_sheet(
|
|
49
|
+
sheet_id_int, include=["objectValue"]
|
|
50
|
+
)
|
|
51
|
+
sheet_name = sheet_details.name
|
|
52
|
+
resource_name = f"sheet_{sheet_name.replace(' ', '_').lower()}"
|
|
53
|
+
sheet = smartsheet_client.Sheets.get_sheet(sheet_id_int)
|
|
54
|
+
|
|
55
|
+
yield dlt.resource(
|
|
56
|
+
_get_sheet_data(sheet),
|
|
57
|
+
name=resource_name,
|
|
58
|
+
columns=_generate_type_hints(sheet),
|
|
59
|
+
write_disposition="replace",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _get_sheet_data(sheet: Sheet):
|
|
64
|
+
"""Helper function to get all rows from a sheet."""
|
|
65
|
+
|
|
66
|
+
column_titles = [col.title for col in sheet.columns]
|
|
67
|
+
for row in sheet.rows:
|
|
68
|
+
row_data = {"_row_id": row.id}
|
|
69
|
+
for i, cell in enumerate(row.cells):
|
|
70
|
+
row_data[column_titles[i]] = cell.value
|
|
71
|
+
yield row_data
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _generate_type_hints(sheet: Sheet):
|
|
75
|
+
return {
|
|
76
|
+
col.title: {
|
|
77
|
+
"data_type": TYPE_MAPPING.get(col.type.value),
|
|
78
|
+
"nullable": True,
|
|
79
|
+
}
|
|
80
|
+
for col in sheet.columns
|
|
81
|
+
if col.type.value in TYPE_MAPPING
|
|
82
|
+
}
|