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.
Files changed (146) hide show
  1. ingestr/conftest.py +72 -0
  2. ingestr/main.py +134 -87
  3. ingestr/src/adjust/__init__.py +4 -4
  4. ingestr/src/adjust/adjust_helpers.py +7 -3
  5. ingestr/src/airtable/__init__.py +3 -2
  6. ingestr/src/allium/__init__.py +128 -0
  7. ingestr/src/anthropic/__init__.py +277 -0
  8. ingestr/src/anthropic/helpers.py +525 -0
  9. ingestr/src/applovin/__init__.py +262 -0
  10. ingestr/src/applovin_max/__init__.py +117 -0
  11. ingestr/src/appsflyer/__init__.py +325 -0
  12. ingestr/src/appsflyer/client.py +49 -45
  13. ingestr/src/appstore/__init__.py +1 -0
  14. ingestr/src/arrow/__init__.py +9 -1
  15. ingestr/src/asana_source/__init__.py +1 -1
  16. ingestr/src/attio/__init__.py +102 -0
  17. ingestr/src/attio/helpers.py +65 -0
  18. ingestr/src/blob.py +38 -11
  19. ingestr/src/buildinfo.py +1 -0
  20. ingestr/src/chess/__init__.py +1 -1
  21. ingestr/src/clickup/__init__.py +85 -0
  22. ingestr/src/clickup/helpers.py +47 -0
  23. ingestr/src/collector/spinner.py +43 -0
  24. ingestr/src/couchbase_source/__init__.py +118 -0
  25. ingestr/src/couchbase_source/helpers.py +135 -0
  26. ingestr/src/cursor/__init__.py +83 -0
  27. ingestr/src/cursor/helpers.py +188 -0
  28. ingestr/src/destinations.py +520 -33
  29. ingestr/src/docebo/__init__.py +589 -0
  30. ingestr/src/docebo/client.py +435 -0
  31. ingestr/src/docebo/helpers.py +97 -0
  32. ingestr/src/elasticsearch/__init__.py +80 -0
  33. ingestr/src/elasticsearch/helpers.py +138 -0
  34. ingestr/src/errors.py +8 -0
  35. ingestr/src/facebook_ads/__init__.py +47 -28
  36. ingestr/src/facebook_ads/helpers.py +59 -37
  37. ingestr/src/facebook_ads/settings.py +2 -0
  38. ingestr/src/facebook_ads/utils.py +39 -0
  39. ingestr/src/factory.py +116 -2
  40. ingestr/src/filesystem/__init__.py +8 -3
  41. ingestr/src/filters.py +46 -3
  42. ingestr/src/fluxx/__init__.py +9906 -0
  43. ingestr/src/fluxx/helpers.py +209 -0
  44. ingestr/src/frankfurter/__init__.py +157 -0
  45. ingestr/src/frankfurter/helpers.py +48 -0
  46. ingestr/src/freshdesk/__init__.py +89 -0
  47. ingestr/src/freshdesk/freshdesk_client.py +137 -0
  48. ingestr/src/freshdesk/settings.py +9 -0
  49. ingestr/src/fundraiseup/__init__.py +95 -0
  50. ingestr/src/fundraiseup/client.py +81 -0
  51. ingestr/src/github/__init__.py +41 -6
  52. ingestr/src/github/helpers.py +5 -5
  53. ingestr/src/google_analytics/__init__.py +22 -4
  54. ingestr/src/google_analytics/helpers.py +124 -6
  55. ingestr/src/google_sheets/__init__.py +4 -4
  56. ingestr/src/google_sheets/helpers/data_processing.py +2 -2
  57. ingestr/src/hostaway/__init__.py +302 -0
  58. ingestr/src/hostaway/client.py +288 -0
  59. ingestr/src/http/__init__.py +35 -0
  60. ingestr/src/http/readers.py +114 -0
  61. ingestr/src/http_client.py +24 -0
  62. ingestr/src/hubspot/__init__.py +66 -23
  63. ingestr/src/hubspot/helpers.py +52 -22
  64. ingestr/src/hubspot/settings.py +14 -7
  65. ingestr/src/influxdb/__init__.py +46 -0
  66. ingestr/src/influxdb/client.py +34 -0
  67. ingestr/src/intercom/__init__.py +142 -0
  68. ingestr/src/intercom/helpers.py +674 -0
  69. ingestr/src/intercom/settings.py +279 -0
  70. ingestr/src/isoc_pulse/__init__.py +159 -0
  71. ingestr/src/jira_source/__init__.py +340 -0
  72. ingestr/src/jira_source/helpers.py +439 -0
  73. ingestr/src/jira_source/settings.py +170 -0
  74. ingestr/src/kafka/__init__.py +4 -1
  75. ingestr/src/kinesis/__init__.py +139 -0
  76. ingestr/src/kinesis/helpers.py +82 -0
  77. ingestr/src/klaviyo/{_init_.py → __init__.py} +5 -6
  78. ingestr/src/linear/__init__.py +634 -0
  79. ingestr/src/linear/helpers.py +111 -0
  80. ingestr/src/linkedin_ads/helpers.py +0 -1
  81. ingestr/src/loader.py +69 -0
  82. ingestr/src/mailchimp/__init__.py +126 -0
  83. ingestr/src/mailchimp/helpers.py +226 -0
  84. ingestr/src/mailchimp/settings.py +164 -0
  85. ingestr/src/masking.py +344 -0
  86. ingestr/src/mixpanel/__init__.py +62 -0
  87. ingestr/src/mixpanel/client.py +99 -0
  88. ingestr/src/monday/__init__.py +246 -0
  89. ingestr/src/monday/helpers.py +392 -0
  90. ingestr/src/monday/settings.py +328 -0
  91. ingestr/src/mongodb/__init__.py +72 -8
  92. ingestr/src/mongodb/helpers.py +915 -38
  93. ingestr/src/partition.py +32 -0
  94. ingestr/src/personio/__init__.py +331 -0
  95. ingestr/src/personio/helpers.py +86 -0
  96. ingestr/src/phantombuster/__init__.py +65 -0
  97. ingestr/src/phantombuster/client.py +87 -0
  98. ingestr/src/pinterest/__init__.py +82 -0
  99. ingestr/src/pipedrive/__init__.py +198 -0
  100. ingestr/src/pipedrive/helpers/__init__.py +23 -0
  101. ingestr/src/pipedrive/helpers/custom_fields_munger.py +102 -0
  102. ingestr/src/pipedrive/helpers/pages.py +115 -0
  103. ingestr/src/pipedrive/settings.py +27 -0
  104. ingestr/src/pipedrive/typing.py +3 -0
  105. ingestr/src/plusvibeai/__init__.py +335 -0
  106. ingestr/src/plusvibeai/helpers.py +544 -0
  107. ingestr/src/plusvibeai/settings.py +252 -0
  108. ingestr/src/quickbooks/__init__.py +117 -0
  109. ingestr/src/resource.py +40 -0
  110. ingestr/src/revenuecat/__init__.py +83 -0
  111. ingestr/src/revenuecat/helpers.py +237 -0
  112. ingestr/src/salesforce/__init__.py +156 -0
  113. ingestr/src/salesforce/helpers.py +64 -0
  114. ingestr/src/shopify/__init__.py +1 -17
  115. ingestr/src/smartsheets/__init__.py +82 -0
  116. ingestr/src/snapchat_ads/__init__.py +489 -0
  117. ingestr/src/snapchat_ads/client.py +72 -0
  118. ingestr/src/snapchat_ads/helpers.py +535 -0
  119. ingestr/src/socrata_source/__init__.py +83 -0
  120. ingestr/src/socrata_source/helpers.py +85 -0
  121. ingestr/src/socrata_source/settings.py +8 -0
  122. ingestr/src/solidgate/__init__.py +219 -0
  123. ingestr/src/solidgate/helpers.py +154 -0
  124. ingestr/src/sources.py +3132 -212
  125. ingestr/src/stripe_analytics/__init__.py +49 -21
  126. ingestr/src/stripe_analytics/helpers.py +286 -1
  127. ingestr/src/stripe_analytics/settings.py +62 -10
  128. ingestr/src/telemetry/event.py +10 -9
  129. ingestr/src/tiktok_ads/__init__.py +12 -6
  130. ingestr/src/tiktok_ads/tiktok_helpers.py +0 -1
  131. ingestr/src/trustpilot/__init__.py +48 -0
  132. ingestr/src/trustpilot/client.py +48 -0
  133. ingestr/src/version.py +6 -1
  134. ingestr/src/wise/__init__.py +68 -0
  135. ingestr/src/wise/client.py +63 -0
  136. ingestr/src/zoom/__init__.py +99 -0
  137. ingestr/src/zoom/helpers.py +102 -0
  138. ingestr/tests/unit/test_smartsheets.py +133 -0
  139. ingestr-0.14.104.dist-info/METADATA +563 -0
  140. ingestr-0.14.104.dist-info/RECORD +203 -0
  141. ingestr/src/appsflyer/_init_.py +0 -24
  142. ingestr-0.13.2.dist-info/METADATA +0 -302
  143. ingestr-0.13.2.dist-info/RECORD +0 -107
  144. {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/WHEEL +0 -0
  145. {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/entry_points.txt +0 -0
  146. {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()