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.
Files changed (218) hide show
  1. omniload/conftest.py +72 -0
  2. omniload/main.py +810 -0
  3. omniload/src/.gitignore +10 -0
  4. omniload/src/adjust/__init__.py +108 -0
  5. omniload/src/adjust/adjust_helpers.py +122 -0
  6. omniload/src/airtable/__init__.py +84 -0
  7. omniload/src/allium/__init__.py +128 -0
  8. omniload/src/anthropic/__init__.py +277 -0
  9. omniload/src/anthropic/helpers.py +525 -0
  10. omniload/src/applovin/__init__.py +316 -0
  11. omniload/src/applovin_max/__init__.py +117 -0
  12. omniload/src/appsflyer/__init__.py +325 -0
  13. omniload/src/appsflyer/client.py +110 -0
  14. omniload/src/appstore/__init__.py +142 -0
  15. omniload/src/appstore/client.py +126 -0
  16. omniload/src/appstore/errors.py +15 -0
  17. omniload/src/appstore/models.py +117 -0
  18. omniload/src/appstore/resources.py +179 -0
  19. omniload/src/arrow/__init__.py +81 -0
  20. omniload/src/asana_source/__init__.py +281 -0
  21. omniload/src/asana_source/helpers.py +30 -0
  22. omniload/src/asana_source/settings.py +158 -0
  23. omniload/src/attio/__init__.py +102 -0
  24. omniload/src/attio/helpers.py +65 -0
  25. omniload/src/blob.py +95 -0
  26. omniload/src/bruin/__init__.py +76 -0
  27. omniload/src/chess/__init__.py +180 -0
  28. omniload/src/chess/helpers.py +35 -0
  29. omniload/src/chess/settings.py +18 -0
  30. omniload/src/clickup/__init__.py +85 -0
  31. omniload/src/clickup/helpers.py +47 -0
  32. omniload/src/collector/spinner.py +43 -0
  33. omniload/src/couchbase_source/__init__.py +118 -0
  34. omniload/src/couchbase_source/helpers.py +135 -0
  35. omniload/src/cursor/__init__.py +83 -0
  36. omniload/src/cursor/helpers.py +188 -0
  37. omniload/src/customer_io/__init__.py +486 -0
  38. omniload/src/customer_io/helpers.py +530 -0
  39. omniload/src/destinations.py +982 -0
  40. omniload/src/docebo/__init__.py +589 -0
  41. omniload/src/docebo/client.py +435 -0
  42. omniload/src/docebo/helpers.py +97 -0
  43. omniload/src/dune/__init__.py +104 -0
  44. omniload/src/dune/helpers.py +108 -0
  45. omniload/src/dynamodb/__init__.py +86 -0
  46. omniload/src/elasticsearch/__init__.py +80 -0
  47. omniload/src/elasticsearch/helpers.py +141 -0
  48. omniload/src/errors.py +26 -0
  49. omniload/src/facebook_ads/__init__.py +403 -0
  50. omniload/src/facebook_ads/exceptions.py +19 -0
  51. omniload/src/facebook_ads/helpers.py +296 -0
  52. omniload/src/facebook_ads/settings.py +224 -0
  53. omniload/src/facebook_ads/utils.py +53 -0
  54. omniload/src/factory.py +305 -0
  55. omniload/src/filesystem/__init__.py +133 -0
  56. omniload/src/filesystem/helpers.py +114 -0
  57. omniload/src/filesystem/readers.py +187 -0
  58. omniload/src/filters.py +62 -0
  59. omniload/src/fireflies/__init__.py +151 -0
  60. omniload/src/fireflies/helpers.py +753 -0
  61. omniload/src/fluxx/__init__.py +10013 -0
  62. omniload/src/fluxx/helpers.py +233 -0
  63. omniload/src/frankfurter/__init__.py +157 -0
  64. omniload/src/frankfurter/helpers.py +48 -0
  65. omniload/src/freshdesk/__init__.py +103 -0
  66. omniload/src/freshdesk/freshdesk_client.py +151 -0
  67. omniload/src/freshdesk/settings.py +23 -0
  68. omniload/src/fundraiseup/__init__.py +95 -0
  69. omniload/src/fundraiseup/client.py +81 -0
  70. omniload/src/github/__init__.py +202 -0
  71. omniload/src/github/helpers.py +207 -0
  72. omniload/src/github/queries.py +129 -0
  73. omniload/src/github/settings.py +24 -0
  74. omniload/src/google_ads/__init__.py +198 -0
  75. omniload/src/google_ads/field.py +17 -0
  76. omniload/src/google_ads/metrics.py +254 -0
  77. omniload/src/google_ads/predicates.py +37 -0
  78. omniload/src/google_ads/reports.py +411 -0
  79. omniload/src/google_ads/test_google_ads.py +184 -0
  80. omniload/src/google_analytics/__init__.py +144 -0
  81. omniload/src/google_analytics/helpers.py +312 -0
  82. omniload/src/google_sheets/README.md +95 -0
  83. omniload/src/google_sheets/__init__.py +166 -0
  84. omniload/src/google_sheets/helpers/__init__.py +15 -0
  85. omniload/src/google_sheets/helpers/api_calls.py +160 -0
  86. omniload/src/google_sheets/helpers/data_processing.py +316 -0
  87. omniload/src/gorgias/__init__.py +595 -0
  88. omniload/src/gorgias/helpers.py +166 -0
  89. omniload/src/hostaway/__init__.py +302 -0
  90. omniload/src/hostaway/client.py +288 -0
  91. omniload/src/http/__init__.py +38 -0
  92. omniload/src/http/readers.py +146 -0
  93. omniload/src/http_client.py +24 -0
  94. omniload/src/hubspot/__init__.py +800 -0
  95. omniload/src/hubspot/helpers.py +417 -0
  96. omniload/src/hubspot/settings.py +329 -0
  97. omniload/src/indeed/__init__.py +153 -0
  98. omniload/src/indeed/helpers.py +228 -0
  99. omniload/src/influxdb/__init__.py +46 -0
  100. omniload/src/influxdb/client.py +34 -0
  101. omniload/src/intercom/__init__.py +142 -0
  102. omniload/src/intercom/helpers.py +674 -0
  103. omniload/src/intercom/settings.py +279 -0
  104. omniload/src/isoc_pulse/__init__.py +159 -0
  105. omniload/src/jira_source/__init__.py +377 -0
  106. omniload/src/jira_source/helpers.py +510 -0
  107. omniload/src/jira_source/settings.py +184 -0
  108. omniload/src/kafka/__init__.py +120 -0
  109. omniload/src/kafka/helpers.py +241 -0
  110. omniload/src/kinesis/__init__.py +153 -0
  111. omniload/src/kinesis/helpers.py +96 -0
  112. omniload/src/klaviyo/__init__.py +237 -0
  113. omniload/src/klaviyo/client.py +212 -0
  114. omniload/src/klaviyo/helpers.py +19 -0
  115. omniload/src/linear/__init__.py +634 -0
  116. omniload/src/linear/helpers.py +111 -0
  117. omniload/src/linkedin_ads/__init__.py +266 -0
  118. omniload/src/linkedin_ads/dimension_time_enum.py +17 -0
  119. omniload/src/linkedin_ads/helpers.py +246 -0
  120. omniload/src/loader.py +69 -0
  121. omniload/src/mailchimp/__init__.py +126 -0
  122. omniload/src/mailchimp/helpers.py +226 -0
  123. omniload/src/mailchimp/settings.py +164 -0
  124. omniload/src/masking.py +344 -0
  125. omniload/src/mixpanel/__init__.py +62 -0
  126. omniload/src/mixpanel/client.py +104 -0
  127. omniload/src/monday/__init__.py +246 -0
  128. omniload/src/monday/helpers.py +392 -0
  129. omniload/src/monday/settings.py +325 -0
  130. omniload/src/mongodb/__init__.py +281 -0
  131. omniload/src/mongodb/helpers.py +975 -0
  132. omniload/src/notion/__init__.py +69 -0
  133. omniload/src/notion/helpers/__init__.py +14 -0
  134. omniload/src/notion/helpers/client.py +178 -0
  135. omniload/src/notion/helpers/database.py +92 -0
  136. omniload/src/notion/settings.py +17 -0
  137. omniload/src/partition.py +32 -0
  138. omniload/src/personio/__init__.py +345 -0
  139. omniload/src/personio/helpers.py +100 -0
  140. omniload/src/phantombuster/__init__.py +65 -0
  141. omniload/src/phantombuster/client.py +87 -0
  142. omniload/src/pinterest/__init__.py +82 -0
  143. omniload/src/pipedrive/__init__.py +212 -0
  144. omniload/src/pipedrive/helpers/__init__.py +37 -0
  145. omniload/src/pipedrive/helpers/custom_fields_munger.py +116 -0
  146. omniload/src/pipedrive/helpers/pages.py +129 -0
  147. omniload/src/pipedrive/settings.py +41 -0
  148. omniload/src/pipedrive/typing.py +17 -0
  149. omniload/src/plusvibeai/__init__.py +335 -0
  150. omniload/src/plusvibeai/helpers.py +544 -0
  151. omniload/src/plusvibeai/settings.py +252 -0
  152. omniload/src/primer/__init__.py +45 -0
  153. omniload/src/primer/helpers.py +79 -0
  154. omniload/src/quickbooks/__init__.py +117 -0
  155. omniload/src/reddit_ads/__init__.py +183 -0
  156. omniload/src/reddit_ads/helpers.py +232 -0
  157. omniload/src/resource.py +40 -0
  158. omniload/src/revenuecat/__init__.py +83 -0
  159. omniload/src/revenuecat/helpers.py +237 -0
  160. omniload/src/salesforce/__init__.py +170 -0
  161. omniload/src/salesforce/helpers.py +78 -0
  162. omniload/src/shopify/__init__.py +1953 -0
  163. omniload/src/shopify/exceptions.py +17 -0
  164. omniload/src/shopify/helpers.py +202 -0
  165. omniload/src/shopify/settings.py +19 -0
  166. omniload/src/slack/__init__.py +290 -0
  167. omniload/src/slack/helpers.py +218 -0
  168. omniload/src/slack/settings.py +36 -0
  169. omniload/src/smartsheets/__init__.py +82 -0
  170. omniload/src/snapchat_ads/__init__.py +455 -0
  171. omniload/src/snapchat_ads/client.py +72 -0
  172. omniload/src/snapchat_ads/helpers.py +630 -0
  173. omniload/src/snapchat_ads/settings.py +130 -0
  174. omniload/src/socrata_source/__init__.py +83 -0
  175. omniload/src/socrata_source/helpers.py +85 -0
  176. omniload/src/socrata_source/settings.py +8 -0
  177. omniload/src/solidgate/__init__.py +219 -0
  178. omniload/src/solidgate/helpers.py +154 -0
  179. omniload/src/sources.py +5408 -0
  180. omniload/src/sql_database/__init__.py +0 -0
  181. omniload/src/sql_database/callbacks.py +66 -0
  182. omniload/src/stripe_analytics/__init__.py +183 -0
  183. omniload/src/stripe_analytics/helpers.py +386 -0
  184. omniload/src/stripe_analytics/settings.py +80 -0
  185. omniload/src/table_definition.py +15 -0
  186. omniload/src/testdata/fakebqcredentials.json +14 -0
  187. omniload/src/tiktok_ads/__init__.py +150 -0
  188. omniload/src/tiktok_ads/tiktok_helpers.py +130 -0
  189. omniload/src/time.py +11 -0
  190. omniload/src/trustpilot/__init__.py +48 -0
  191. omniload/src/trustpilot/client.py +48 -0
  192. omniload/src/version.py +6 -0
  193. omniload/src/wise/__init__.py +68 -0
  194. omniload/src/wise/client.py +63 -0
  195. omniload/src/zendesk/__init__.py +480 -0
  196. omniload/src/zendesk/helpers/__init__.py +39 -0
  197. omniload/src/zendesk/helpers/api_helpers.py +119 -0
  198. omniload/src/zendesk/helpers/credentials.py +68 -0
  199. omniload/src/zendesk/helpers/talk_api.py +132 -0
  200. omniload/src/zendesk/settings.py +71 -0
  201. omniload/src/zoom/__init__.py +99 -0
  202. omniload/src/zoom/helpers.py +102 -0
  203. omniload/testdata/.gitignore +2 -0
  204. omniload/testdata/create_replace.csv +21 -0
  205. omniload/testdata/delete_insert_expected.csv +6 -0
  206. omniload/testdata/delete_insert_part1.csv +5 -0
  207. omniload/testdata/delete_insert_part2.csv +6 -0
  208. omniload/testdata/merge_expected.csv +5 -0
  209. omniload/testdata/merge_part1.csv +4 -0
  210. omniload/testdata/merge_part2.csv +5 -0
  211. omniload/tests/unit/test_smartsheets.py +133 -0
  212. omniload-0.0.0.dev0.dist-info/METADATA +439 -0
  213. omniload-0.0.0.dev0.dist-info/RECORD +218 -0
  214. omniload-0.0.0.dev0.dist-info/WHEEL +4 -0
  215. omniload-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  216. omniload-0.0.0.dev0.dist-info/licenses/LICENSE.Apache-2.0 +201 -0
  217. omniload-0.0.0.dev0.dist-info/licenses/LICENSE.md +21 -0
  218. omniload-0.0.0.dev0.dist-info/licenses/NOTICE +35 -0
@@ -0,0 +1,68 @@
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
+ """
16
+ This module handles how credentials are read in dlt sources
17
+ """
18
+
19
+ from typing import ClassVar, List, Union
20
+
21
+ import dlt
22
+ from dlt.common.configuration import configspec
23
+ from dlt.common.configuration.specs import CredentialsConfiguration
24
+ from dlt.common.typing import TSecretValue
25
+
26
+
27
+ @configspec
28
+ class ZendeskCredentialsBase(CredentialsConfiguration):
29
+ """
30
+ The Base version of all the ZendeskCredential classes.
31
+ """
32
+
33
+ subdomain: str = dlt.config.value
34
+ __config_gen_annotations__: ClassVar[List[str]] = []
35
+
36
+
37
+ @configspec
38
+ class ZendeskCredentialsEmailPass(ZendeskCredentialsBase):
39
+ """
40
+ This class is used to store credentials for Email + Password Authentication
41
+ """
42
+
43
+ email: str = dlt.config.value
44
+ password: TSecretValue = dlt.secrets.value
45
+
46
+
47
+ @configspec
48
+ class ZendeskCredentialsOAuth(ZendeskCredentialsBase):
49
+ """
50
+ This class is used to store credentials for OAuth Token Authentication
51
+ """
52
+
53
+ oauth_token: TSecretValue = dlt.secrets.value
54
+
55
+
56
+ @configspec
57
+ class ZendeskCredentialsToken(ZendeskCredentialsBase):
58
+ """
59
+ This class is used to store credentials for Token Authentication
60
+ """
61
+
62
+ email: str = dlt.config.value
63
+ token: TSecretValue = dlt.secrets.value
64
+
65
+
66
+ TZendeskCredentials = Union[
67
+ ZendeskCredentialsEmailPass, ZendeskCredentialsToken, ZendeskCredentialsOAuth
68
+ ]
@@ -0,0 +1,132 @@
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
+ from enum import Enum
16
+ from typing import Any, Dict, Iterator, Optional, Tuple
17
+
18
+ from dlt.common.typing import DictStrStr, TDataItems, TSecretValue
19
+ from dlt.sources.helpers.requests import client
20
+
21
+ from .. import settings
22
+ from .credentials import (
23
+ TZendeskCredentials,
24
+ ZendeskCredentialsEmailPass,
25
+ ZendeskCredentialsOAuth,
26
+ ZendeskCredentialsToken,
27
+ )
28
+
29
+
30
+ class PaginationType(Enum):
31
+ OFFSET = 0
32
+ CURSOR = 1
33
+ STREAM = 2
34
+ START_TIME = 3
35
+
36
+
37
+ class ZendeskAPIClient:
38
+ """
39
+ API client used to make requests to Zendesk talk, support and chat API
40
+ """
41
+
42
+ subdomain: str = ""
43
+ url: str = ""
44
+ headers: Optional[DictStrStr]
45
+ auth: Optional[Tuple[str, TSecretValue]]
46
+
47
+ def __init__(
48
+ self, credentials: TZendeskCredentials, url_prefix: Optional[str] = None
49
+ ) -> None:
50
+ """
51
+ Initializer for the API client which is then used to make API calls to the ZendeskAPI
52
+
53
+ Args:
54
+ credentials: ZendeskCredentials object which contains the necessary credentials to authenticate to ZendeskAPI
55
+ """
56
+ # oauth token is the preferred way to authenticate, followed by api token and then email + password combo
57
+ # fill headers and auth for every possibility of credentials given, raise error if credentials are of incorrect type
58
+ if isinstance(credentials, ZendeskCredentialsOAuth):
59
+ self.headers = {"Authorization": f"Bearer {credentials.oauth_token}"}
60
+ self.auth = None
61
+ elif isinstance(credentials, ZendeskCredentialsToken):
62
+ self.headers = None
63
+ self.auth = (f"{credentials.email}/token", credentials.token)
64
+ elif isinstance(credentials, ZendeskCredentialsEmailPass):
65
+ self.auth = (credentials.email, credentials.password)
66
+ self.headers = None
67
+ else:
68
+ raise TypeError(
69
+ "Wrong credentials type provided to ZendeskAPIClient. The credentials need to be of type: ZendeskCredentialsOAuth, ZendeskCredentialsToken or ZendeskCredentialsEmailPass"
70
+ )
71
+
72
+ # If url_prefix is set it overrides the default API URL (e.g. chat api uses zopim.com domain)
73
+ if url_prefix:
74
+ self.url = url_prefix
75
+ else:
76
+ self.subdomain = credentials.subdomain
77
+ self.url = f"https://{self.subdomain}.zendesk.com"
78
+
79
+ def get_pages(
80
+ self,
81
+ endpoint: str,
82
+ data_point_name: str,
83
+ pagination: PaginationType,
84
+ params: Optional[Dict[str, Any]] = None,
85
+ ) -> Iterator[TDataItems]:
86
+ """
87
+ Makes a request to a paginated endpoint and returns a generator of data items per page.
88
+
89
+ Args:
90
+ endpoint: The url to the endpoint, e.g. /api/v2/calls
91
+ data_point_name: The key which data items are nested under in the response object (e.g. calls)
92
+ params: Optional dict of query params to include in the request
93
+ pagination: Type of pagination type used by endpoint
94
+
95
+ Returns:
96
+ Generator of pages, each page is a list of dict data items
97
+ """
98
+ # update the page size to enable cursor pagination
99
+ params = params or {}
100
+ if pagination == PaginationType.CURSOR:
101
+ params["page[size]"] = settings.PAGE_SIZE
102
+ elif pagination == PaginationType.STREAM:
103
+ params["per_page"] = settings.INCREMENTAL_PAGE_SIZE
104
+ elif pagination == PaginationType.START_TIME:
105
+ params["limit"] = settings.INCREMENTAL_PAGE_SIZE
106
+
107
+ # make request and keep looping until there is no next page
108
+ get_url = f"{self.url}{endpoint}"
109
+ while get_url:
110
+ response = client.get(
111
+ get_url, headers=self.headers, auth=self.auth, params=params
112
+ )
113
+ response.raise_for_status()
114
+ response_json = response.json()
115
+ result = response_json[data_point_name]
116
+ yield result
117
+
118
+ get_url = None
119
+ if pagination == PaginationType.CURSOR:
120
+ if response_json["meta"]["has_more"]:
121
+ get_url = response_json["links"]["next"]
122
+ elif pagination == PaginationType.OFFSET:
123
+ get_url = response_json.get("next_page", None)
124
+ elif pagination == PaginationType.STREAM:
125
+ # See https://developer.zendesk.com/api-reference/ticketing/ticket-management/incremental_exports/#json-format
126
+ if not response_json["end_of_stream"]:
127
+ get_url = response_json["next_page"]
128
+ elif pagination == PaginationType.START_TIME:
129
+ if response_json["count"] > 0:
130
+ get_url = response_json["next_page"]
131
+
132
+ params = {}
@@ -0,0 +1,71 @@
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
+ """Zendesk source settings and constants"""
16
+
17
+ from dlt.common import pendulum
18
+
19
+ DEFAULT_START_DATE = pendulum.datetime(year=2024, month=10, day=3)
20
+
21
+ INCREMENTAL_PAGE_SIZE = 1000
22
+ PAGE_SIZE = 100
23
+
24
+
25
+ CUSTOM_FIELDS_STATE_KEY = "ticket_custom_fields_v2"
26
+
27
+ # Tuples of (Resource name, endpoint URL, data_key, supports pagination)
28
+ # data_key is the key which data list is nested under in responses
29
+ # if the data key is None it is assumed to be the same as the resource name
30
+ # The last element of the tuple says if endpoint supports cursor pagination
31
+ SUPPORT_ENDPOINTS = [
32
+ ("users", "/api/v2/users.json", "users", True),
33
+ ("sla_policies", "/api/v2/slas/policies.json", None, False),
34
+ ("groups", "/api/v2/groups.json", None, True),
35
+ ("organizations", "/api/v2/organizations.json", None, True),
36
+ ("brands", "/api/v2/brands.json", None, True),
37
+ ]
38
+
39
+ SUPPORT_EXTRA_ENDPOINTS = [
40
+ ("activities", "/api/v2/activities.json", None, True),
41
+ ("automations", "/api/v2/automations.json", None, True),
42
+ ("macros", "/api/v2/macros.json", None, True),
43
+ ("recipient_addresses", "/api/v2/recipient_addresses.json", None, True),
44
+ ("requests", "/api/v2/requests.json", None, True),
45
+ ("targets", "/api/v2/targets.json", None, False),
46
+ ("ticket_forms", "/api/v2/ticket_forms.json", None, False),
47
+ ("ticket_metrics", "/api/v2/ticket_metrics.json", None, True),
48
+ ("triggers", "/api/v2/triggers.json", None, True),
49
+ ("user_fields", "/api/v2/user_fields.json", None, True),
50
+ ]
51
+
52
+ TALK_ENDPOINTS = [
53
+ ("calls", "/api/v2/channels/voice/calls", None, False),
54
+ ("addresses", "/api/v2/channels/voice/addresses", None, False),
55
+ ("greetings", "/api/v2/channels/voice/greetings", None, False),
56
+ ("phone_numbers", "/api/v2/channels/voice/phone_numbers", None, False),
57
+ ("settings", "/api/v2/channels/voice/settings", None, False),
58
+ ("lines", "/api/v2/channels/voice/lines", None, False),
59
+ ("agents_activity", "/api/v2/channels/voice/stats/agents_activity", None, False),
60
+ (
61
+ "current_queue_activity",
62
+ "/api/v2/channels/voice/stats/current_queue_activity",
63
+ None,
64
+ False,
65
+ ),
66
+ ]
67
+
68
+ INCREMENTAL_TALK_ENDPOINTS = {
69
+ "calls": "/api/v2/channels/voice/stats/incremental/calls.json",
70
+ "legs": "/api/v2/channels/voice/stats/incremental/legs.json",
71
+ }
@@ -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 omniload.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,2 @@
1
+ *.db
2
+ *.duckdb
@@ -0,0 +1,21 @@
1
+ "symbol","date","isEnabled","name"
2
+ "A","2024-04-19","True","AGILENT TECHNOLOGIES INC"
3
+ "AA","2024-04-19","True","ALCOA CORP"
4
+ "AAA","2024-04-19","True","ALTERNATIVE ACCESS FIRST PRI"
5
+ "AAAU","2024-04-19","True","GOLDMAN SACHS PHYSICAL GOLD"
6
+ "AACG","2024-04-19","True","ATA CREATIVITY GLOBAL - ADR"
7
+ "AACI","2024-04-19","True","ARMADA ACQUISITION CORP I"
8
+ "AACIU","2024-04-19","True","ARMADA ACQUISITION CORP I"
9
+ "AACIW","2024-04-19","True",""
10
+ "AACT","2024-04-19","True","ARES ACQUISITION CORP II"
11
+ "AACT+","2024-04-19","True",""
12
+ "AACT=","2024-04-19","True","ARES ACQUISITION CORP II"
13
+ "AADI","2024-04-19","True","AADI BIOSCIENCE INC"
14
+ "AADR","2024-04-19","True","ADVISORSHARES DORSEY WRIGHT"
15
+ "AAGR","2024-04-19","True","AFRICAN AGRICULTURE HOLDINGS"
16
+ "AAGRW","2024-04-19","True",""
17
+ "AAL","2024-04-19","True","AMERICAN AIRLINES GROUP INC"
18
+ "AAMC","2024-04-19","True","ALTISOURCE ASSET MANAGEMENT"
19
+ "AAME","2024-04-19","True","ATLANTIC AMERICAN CORP"
20
+ "AAN","2024-04-19","True","AARON'S CO INC/THE"
21
+ "AAOI","2024-04-19","True","APPLIED OPTOELECTRONICS INC"
@@ -0,0 +1,6 @@
1
+ "symbol","date","isEnabled","name"
2
+ "A","2024-04-20","True","AGILENT TECHNOLOGIES INC____updated"
3
+ "AA","2024-04-19","True","ALCOA CORP____updated"
4
+ "AAA","2024-04-21","True","ALTERNATIVE ACCESS FIRST PRI____updated"
5
+ "AAAU","2024-04-22","True","GOLDMAN SACHS PHYSICAL GOLD____updated"
6
+ "B","2024-04-18","True","SOME TECHNOLOGIES INC"
@@ -0,0 +1,5 @@
1
+ "symbol","date","isEnabled","name"
2
+ "A","2024-04-19","True","AGILENT TECHNOLOGIES INC"
3
+ "AA","2024-04-19","True","ALCOA CORP"
4
+ "AAA","2024-04-19","True","ALTERNATIVE ACCESS FIRST PRI"
5
+ "B","2024-04-18","True","SOME TECHNOLOGIES INC"
@@ -0,0 +1,6 @@
1
+ "symbol","date","isEnabled","name"
2
+ "A","2024-04-20","True","AGILENT TECHNOLOGIES INC____updated"
3
+ "AA","2024-04-19","True","ALCOA CORP____updated"
4
+ "AAA","2024-04-21","True","ALTERNATIVE ACCESS FIRST PRI____updated"
5
+ "AAAU","2024-04-22","True","GOLDMAN SACHS PHYSICAL GOLD____updated"
6
+ "BBB","2024-04-18","True","SOME CORP____updated"
@@ -0,0 +1,5 @@
1
+ "symbol","date","isEnabled","name"
2
+ "A","2024-04-20","True","AGILENT TECHNOLOGIES INC____updated"
3
+ "AA","2024-04-19","True","ALCOA CORP"
4
+ "AAA","2024-04-21","True","ALTERNATIVE ACCESS FIRST PRI____updated"
5
+ "AAAU","2024-04-22","True","GOLDMAN SACHS PHYSICAL GOLD____updated"
@@ -0,0 +1,4 @@
1
+ "symbol","date","isEnabled","name"
2
+ "A","2024-04-19","True","AGILENT TECHNOLOGIES INC"
3
+ "AA","2024-04-19","True","ALCOA CORP"
4
+ "AAA","2024-04-19","True","ALTERNATIVE ACCESS FIRST PRI"
@@ -0,0 +1,5 @@
1
+ "symbol","date","isEnabled","name"
2
+ "A","2024-04-20","True","AGILENT TECHNOLOGIES INC____updated"
3
+ "AA","2024-04-19","True","ALCOA CORP____updated"
4
+ "AAA","2024-04-21","True","ALTERNATIVE ACCESS FIRST PRI____updated"
5
+ "AAAU","2024-04-22","True","GOLDMAN SACHS PHYSICAL GOLD____updated"
@@ -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 omniload.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("omniload.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("omniload.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()