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,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
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from typing import Any, Dict, Iterable, Sequence
|
|
2
|
+
|
|
3
|
+
import dlt
|
|
4
|
+
import pendulum
|
|
5
|
+
from dlt.common.typing import TAnyDateTime, TDataItem
|
|
6
|
+
from dlt.sources import DltResource
|
|
7
|
+
|
|
8
|
+
from .helpers import ZoomClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dlt.source(name="zoom", max_table_nesting=0)
|
|
12
|
+
def zoom_source(
|
|
13
|
+
client_id: str,
|
|
14
|
+
client_secret: str,
|
|
15
|
+
account_id: str,
|
|
16
|
+
start_date: pendulum.DateTime,
|
|
17
|
+
end_date: pendulum.DateTime | None = None,
|
|
18
|
+
) -> Sequence[DltResource]:
|
|
19
|
+
"""Create a Zoom source with meetings resource for all users in the account."""
|
|
20
|
+
client = ZoomClient(
|
|
21
|
+
client_id=client_id,
|
|
22
|
+
client_secret=client_secret,
|
|
23
|
+
account_id=account_id,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
@dlt.resource(write_disposition="merge", primary_key="id")
|
|
27
|
+
def meetings(
|
|
28
|
+
datetime: dlt.sources.incremental[TAnyDateTime] = dlt.sources.incremental(
|
|
29
|
+
"start_time",
|
|
30
|
+
initial_value=start_date.isoformat(),
|
|
31
|
+
end_value=end_date.isoformat() if end_date is not None else None,
|
|
32
|
+
range_start="closed",
|
|
33
|
+
range_end="closed",
|
|
34
|
+
),
|
|
35
|
+
) -> Iterable[TDataItem]:
|
|
36
|
+
if datetime.last_value:
|
|
37
|
+
start_dt = pendulum.parse(datetime.last_value)
|
|
38
|
+
else:
|
|
39
|
+
start_dt = pendulum.parse(start_date)
|
|
40
|
+
|
|
41
|
+
if end_date is None:
|
|
42
|
+
end_dt = pendulum.now("UTC")
|
|
43
|
+
else:
|
|
44
|
+
end_dt = pendulum.parse(datetime.end_value)
|
|
45
|
+
|
|
46
|
+
base_params: Dict[str, Any] = {
|
|
47
|
+
"type": "scheduled",
|
|
48
|
+
"page_size": 300,
|
|
49
|
+
"from": start_dt.to_date_string(),
|
|
50
|
+
"to": end_dt.to_date_string(),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for user in client.get_users():
|
|
54
|
+
user_id = user["id"]
|
|
55
|
+
yield from client.get_meetings(user_id, base_params)
|
|
56
|
+
|
|
57
|
+
@dlt.resource(write_disposition="merge", primary_key="id")
|
|
58
|
+
def users() -> Iterable[TDataItem]:
|
|
59
|
+
yield from client.get_users()
|
|
60
|
+
|
|
61
|
+
@dlt.resource(write_disposition="merge", primary_key="id")
|
|
62
|
+
def participants(
|
|
63
|
+
datetime: dlt.sources.incremental[TAnyDateTime] = dlt.sources.incremental(
|
|
64
|
+
"join_time",
|
|
65
|
+
initial_value=start_date.isoformat(),
|
|
66
|
+
end_value=end_date.isoformat() if end_date is not None else None,
|
|
67
|
+
range_start="closed",
|
|
68
|
+
range_end="closed",
|
|
69
|
+
),
|
|
70
|
+
) -> Iterable[TDataItem]:
|
|
71
|
+
if datetime.last_value:
|
|
72
|
+
start_dt = pendulum.parse(datetime.last_value)
|
|
73
|
+
else:
|
|
74
|
+
start_dt = pendulum.parse(start_date)
|
|
75
|
+
|
|
76
|
+
if end_date is None:
|
|
77
|
+
end_dt = pendulum.now("UTC")
|
|
78
|
+
else:
|
|
79
|
+
end_dt = pendulum.parse(datetime.end_value)
|
|
80
|
+
|
|
81
|
+
participant_params: Dict[str, Any] = {
|
|
82
|
+
"page_size": 300,
|
|
83
|
+
}
|
|
84
|
+
meeting_params = {
|
|
85
|
+
"type": "previous_meetings",
|
|
86
|
+
"page_size": 300,
|
|
87
|
+
}
|
|
88
|
+
for user in client.get_users():
|
|
89
|
+
user_id = user["id"]
|
|
90
|
+
for meeting in client.get_meetings(user_id=user_id, params=meeting_params):
|
|
91
|
+
meeting_id = meeting["id"]
|
|
92
|
+
yield from client.get_participants(
|
|
93
|
+
meeting_id=meeting_id,
|
|
94
|
+
params=participant_params,
|
|
95
|
+
start_date=start_dt,
|
|
96
|
+
end_date=end_dt,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return meetings, users, participants
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Any, Dict, Iterator, Optional
|
|
3
|
+
|
|
4
|
+
import pendulum
|
|
5
|
+
|
|
6
|
+
from ingestr.src.http_client import create_client
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ZoomClient:
|
|
10
|
+
"""Minimal Zoom API client supporting Server-to-Server OAuth."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
client_id: Optional[str] = None,
|
|
15
|
+
client_secret: Optional[str] = None,
|
|
16
|
+
account_id: Optional[str] = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
self.client_id = client_id
|
|
19
|
+
self.client_secret = client_secret
|
|
20
|
+
self.account_id = account_id
|
|
21
|
+
self.token_expires_at: float = 0
|
|
22
|
+
self.base_url = "https://api.zoom.us/v2"
|
|
23
|
+
self.session = create_client()
|
|
24
|
+
self._refresh_access_token()
|
|
25
|
+
|
|
26
|
+
def _refresh_access_token(self) -> None:
|
|
27
|
+
token_url = "https://zoom.us/oauth/token"
|
|
28
|
+
auth = (self.client_id, self.client_secret)
|
|
29
|
+
resp = self.session.post(
|
|
30
|
+
token_url,
|
|
31
|
+
params={"grant_type": "account_credentials", "account_id": self.account_id},
|
|
32
|
+
auth=auth,
|
|
33
|
+
)
|
|
34
|
+
resp.raise_for_status()
|
|
35
|
+
data = resp.json()
|
|
36
|
+
self.access_token = data.get("access_token")
|
|
37
|
+
self.token_expires_at = time.time() + data.get("expires_in", 3600)
|
|
38
|
+
|
|
39
|
+
def _ensure_token(self) -> None:
|
|
40
|
+
if self.access_token is None or self.token_expires_at <= time.time():
|
|
41
|
+
self._refresh_access_token()
|
|
42
|
+
|
|
43
|
+
def _headers(self) -> Dict[str, str]:
|
|
44
|
+
self._ensure_token()
|
|
45
|
+
return {
|
|
46
|
+
"Authorization": f"Bearer {self.access_token}",
|
|
47
|
+
"Accept": "application/json",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
def get_users(self) -> Iterator[Dict[str, Any]]:
|
|
51
|
+
url = f"{self.base_url}/users"
|
|
52
|
+
|
|
53
|
+
params = {"page_size": 1000}
|
|
54
|
+
while True:
|
|
55
|
+
response = self.session.get(url, headers=self._headers(), params=params)
|
|
56
|
+
response.raise_for_status()
|
|
57
|
+
data = response.json()
|
|
58
|
+
for user in data.get("users", []):
|
|
59
|
+
yield user
|
|
60
|
+
token = data.get("next_page_token")
|
|
61
|
+
if not token:
|
|
62
|
+
break
|
|
63
|
+
params["next_page_token"] = token
|
|
64
|
+
|
|
65
|
+
# https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meetings
|
|
66
|
+
def get_meetings(
|
|
67
|
+
self, user_id: str, params: Dict[str, Any]
|
|
68
|
+
) -> Iterator[Dict[str, Any]]:
|
|
69
|
+
url = f"{self.base_url}/users/{user_id}/meetings"
|
|
70
|
+
while True:
|
|
71
|
+
response = self.session.get(url, headers=self._headers(), params=params)
|
|
72
|
+
response.raise_for_status()
|
|
73
|
+
data = response.json()
|
|
74
|
+
for item in data.get("meetings", []):
|
|
75
|
+
item["zoom_user_id"] = user_id
|
|
76
|
+
yield item
|
|
77
|
+
token = data.get("next_page_token")
|
|
78
|
+
if not token:
|
|
79
|
+
break
|
|
80
|
+
params["next_page_token"] = token
|
|
81
|
+
|
|
82
|
+
# https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/reportMeetingParticipants
|
|
83
|
+
def get_participants(
|
|
84
|
+
self,
|
|
85
|
+
meeting_id: str,
|
|
86
|
+
params: Dict[str, Any],
|
|
87
|
+
start_date: pendulum.DateTime,
|
|
88
|
+
end_date: pendulum.DateTime,
|
|
89
|
+
) -> Iterator[Dict[str, Any]]:
|
|
90
|
+
url = f"{self.base_url}/report/meetings/{meeting_id}/participants"
|
|
91
|
+
while True:
|
|
92
|
+
response = self.session.get(url, headers=self._headers(), params=params)
|
|
93
|
+
response.raise_for_status()
|
|
94
|
+
data = response.json()
|
|
95
|
+
for item in data.get("participants", []):
|
|
96
|
+
join_time = pendulum.parse(item["join_time"])
|
|
97
|
+
if join_time >= start_date and join_time <= end_date:
|
|
98
|
+
yield item
|
|
99
|
+
token = data.get("next_page_token")
|
|
100
|
+
if not token:
|
|
101
|
+
break
|
|
102
|
+
params["next_page_token"] = token
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import unittest
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
import smartsheet # type: ignore
|
|
6
|
+
from smartsheet.models import Cell, Column, Row, Sheet # type: ignore
|
|
7
|
+
|
|
8
|
+
from ingestr.src.smartsheets import _get_sheet_data, smartsheet_source
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def pp(x):
|
|
12
|
+
print(x, file=sys.stderr)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestSmartsheetSource(unittest.TestCase):
|
|
16
|
+
@patch("ingestr.src.smartsheets.smartsheet.Smartsheet")
|
|
17
|
+
def test_smartsheet_source_success(self, mock_smartsheet_client):
|
|
18
|
+
# Mock Smartsheet client and its methods
|
|
19
|
+
mock_client_instance = mock_smartsheet_client.return_value
|
|
20
|
+
|
|
21
|
+
# Mock sheet details response
|
|
22
|
+
mock_sheet_details = Sheet(
|
|
23
|
+
{
|
|
24
|
+
"id": 123,
|
|
25
|
+
"name": "Test Sheet 1",
|
|
26
|
+
"columns": [
|
|
27
|
+
Column(
|
|
28
|
+
{"id": 1, "title": "Col A", "type": "TEXT_NUMBER", "index": 0}
|
|
29
|
+
),
|
|
30
|
+
Column(
|
|
31
|
+
{"id": 2, "title": "Col B", "type": "TEXT_NUMBER", "index": 1}
|
|
32
|
+
),
|
|
33
|
+
],
|
|
34
|
+
"rows": [
|
|
35
|
+
Row(
|
|
36
|
+
{
|
|
37
|
+
"id": 101,
|
|
38
|
+
"sheetId": 123,
|
|
39
|
+
"cells": [
|
|
40
|
+
Cell({"columnId": 1, "value": "r1c1"}),
|
|
41
|
+
Cell({"columnId": 2, "value": "r1c2"}),
|
|
42
|
+
],
|
|
43
|
+
}
|
|
44
|
+
),
|
|
45
|
+
Row(
|
|
46
|
+
{
|
|
47
|
+
"id": 102,
|
|
48
|
+
"sheetId": 123,
|
|
49
|
+
"cells": [
|
|
50
|
+
Cell({"columnId": 1, "value": "r2c1"}),
|
|
51
|
+
Cell({"columnId": 2, "value": "r2c2"}),
|
|
52
|
+
],
|
|
53
|
+
}
|
|
54
|
+
),
|
|
55
|
+
],
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
mock_client_instance.Sheets.get_sheet.return_value = mock_sheet_details
|
|
59
|
+
|
|
60
|
+
resource = smartsheet_source(access_token="test_token", sheet_id="123")
|
|
61
|
+
data = list(resource)
|
|
62
|
+
self.assertEqual(len(data), 2)
|
|
63
|
+
self.assertEqual(data[0], {"_row_id": 101, "Col A": "r1c1", "Col B": "r1c2"})
|
|
64
|
+
self.assertEqual(data[1], {"_row_id": 102, "Col A": "r2c1", "Col B": "r2c2"})
|
|
65
|
+
|
|
66
|
+
mock_smartsheet_client.assert_called_once_with("test_token")
|
|
67
|
+
mock_client_instance.Sheets.get_sheet.assert_any_call(
|
|
68
|
+
123, include=["objectValue"]
|
|
69
|
+
) # for resource name
|
|
70
|
+
mock_client_instance.Sheets.get_sheet.assert_any_call(
|
|
71
|
+
123
|
|
72
|
+
) # for _get_sheet_data
|
|
73
|
+
|
|
74
|
+
@patch("ingestr.src.smartsheets.smartsheet.Smartsheet")
|
|
75
|
+
def test_smartsheet_source_api_error(self, mock_smartsheet_client):
|
|
76
|
+
mock_client_instance = mock_smartsheet_client.return_value
|
|
77
|
+
mock_client_instance.Sheets.get_sheet.side_effect = (
|
|
78
|
+
smartsheet.exceptions.ApiError("API Error", 500)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
with self.assertRaises(smartsheet.exceptions.ApiError):
|
|
82
|
+
source = smartsheet_source(access_token="test_token", sheet_id="123")
|
|
83
|
+
# Consume the generator to trigger the API call
|
|
84
|
+
list(source)
|
|
85
|
+
|
|
86
|
+
def test_get_sheet_data(self):
|
|
87
|
+
mock_sheet = Sheet(
|
|
88
|
+
{
|
|
89
|
+
"id": 456,
|
|
90
|
+
"name": "Data Sheet",
|
|
91
|
+
"columns": [
|
|
92
|
+
Column(
|
|
93
|
+
{"id": 10, "title": "ID", "type": "TEXT_NUMBER", "index": 0}
|
|
94
|
+
),
|
|
95
|
+
Column(
|
|
96
|
+
{"id": 20, "title": "Value", "type": "TEXT_NUMBER", "index": 1}
|
|
97
|
+
),
|
|
98
|
+
],
|
|
99
|
+
"rows": [
|
|
100
|
+
Row(
|
|
101
|
+
{
|
|
102
|
+
"id": 201,
|
|
103
|
+
"sheetId": 456,
|
|
104
|
+
"cells": [
|
|
105
|
+
Cell({"columnId": 10, "value": 1}),
|
|
106
|
+
Cell({"columnId": 20, "value": "Alpha"}),
|
|
107
|
+
],
|
|
108
|
+
}
|
|
109
|
+
),
|
|
110
|
+
Row(
|
|
111
|
+
{
|
|
112
|
+
"id": 202,
|
|
113
|
+
"sheetId": 456,
|
|
114
|
+
"cells": [
|
|
115
|
+
Cell({"columnId": 10, "value": 2}),
|
|
116
|
+
Cell({"columnId": 20, "value": "Beta"}),
|
|
117
|
+
],
|
|
118
|
+
}
|
|
119
|
+
),
|
|
120
|
+
],
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
data_generator = _get_sheet_data(mock_sheet)
|
|
125
|
+
data = list(data_generator)
|
|
126
|
+
|
|
127
|
+
self.assertEqual(len(data), 2)
|
|
128
|
+
self.assertEqual(data[0], {"_row_id": 201, "ID": 1, "Value": "Alpha"})
|
|
129
|
+
self.assertEqual(data[1], {"_row_id": 202, "ID": 2, "Value": "Beta"})
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
unittest.main()
|