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,530 @@
1
+ from typing import Any
2
+
3
+ import requests
4
+
5
+ BASE_URLS = {
6
+ "us": "https://api.customer.io",
7
+ "eu": "https://api-eu.customer.io",
8
+ }
9
+
10
+ # Maximum steps for each metrics period type
11
+ MAX_STEPS_BY_PERIOD = {
12
+ "hours": 24,
13
+ "days": 45,
14
+ "weeks": 12,
15
+ "months": 12,
16
+ }
17
+
18
+ # For newsletter metrics, months has a different max
19
+ MAX_STEPS_NEWSLETTER = {
20
+ "hours": 24,
21
+ "days": 45,
22
+ "weeks": 12,
23
+ "months": 121,
24
+ }
25
+
26
+
27
+ class CustomerIoClient:
28
+ def __init__(self, api_key: str, region: str = "us"):
29
+ self.api_key = api_key
30
+ self.base_url = BASE_URLS.get(region.lower(), BASE_URLS["us"])
31
+
32
+ def _get_headers(self):
33
+ return {
34
+ "Authorization": f"Bearer {self.api_key}",
35
+ "Content-Type": "application/json",
36
+ }
37
+
38
+ def _extract_metrics(
39
+ self,
40
+ result: dict,
41
+ period: str,
42
+ extra_fields: dict | None = None,
43
+ ) -> list:
44
+ """Extract metrics from API response and add common fields."""
45
+ metrics = []
46
+ for i, metric in enumerate(result.get("series", {}).get("series", [])):
47
+ metric["period"] = period
48
+ metric["step_index"] = i
49
+ if extra_fields:
50
+ metric.update(extra_fields)
51
+ metrics.append(metric)
52
+ return metrics
53
+
54
+ def _fetch_pages(
55
+ self,
56
+ session: requests.Session,
57
+ url: str,
58
+ params: dict[str, Any] | None = None,
59
+ data_key: str = "activities",
60
+ ) -> list:
61
+ all_items: list[Any] = []
62
+ if params is None:
63
+ params = {}
64
+
65
+ while True:
66
+ response = session.get(url=url, headers=self._get_headers(), params=params)
67
+ response.raise_for_status()
68
+ result = response.json()
69
+
70
+ items = result.get(data_key) or []
71
+ all_items.extend(items)
72
+
73
+ next_token = result.get("next")
74
+ if not next_token:
75
+ break
76
+
77
+ params["start"] = next_token
78
+
79
+ return all_items
80
+
81
+ def fetch_activities(
82
+ self,
83
+ session: requests.Session,
84
+ activity_type: str | None = None,
85
+ name: str | None = None,
86
+ customer_id: str | None = None,
87
+ id_type: str | None = None,
88
+ deleted: bool = False,
89
+ limit: int = 100,
90
+ ):
91
+ url = f"{self.base_url}/v1/activities"
92
+ params = {"limit": limit, "deleted": str(deleted).lower()}
93
+
94
+ if activity_type:
95
+ params["type"] = activity_type
96
+ if name:
97
+ params["name"] = name
98
+ if customer_id:
99
+ params["customer_id"] = customer_id
100
+ if id_type:
101
+ params["id_type"] = id_type
102
+
103
+ return self._fetch_pages(session, url, params, data_key="activities")
104
+
105
+ def fetch_broadcasts(self, session: requests.Session):
106
+ url = f"{self.base_url}/v1/broadcasts"
107
+ return self._fetch_pages(session, url, data_key="broadcasts")
108
+
109
+ def fetch_campaigns(self, session: requests.Session):
110
+ url = f"{self.base_url}/v1/campaigns"
111
+ return self._fetch_pages(session, url, data_key="campaigns")
112
+
113
+ def fetch_collections(self, session: requests.Session):
114
+ url = f"{self.base_url}/v1/collections"
115
+ return self._fetch_pages(session, url, data_key="collections")
116
+
117
+ def fetch_exports(self, session: requests.Session):
118
+ url = f"{self.base_url}/v1/exports"
119
+ return self._fetch_pages(session, url, data_key="exports")
120
+
121
+ def fetch_info_ip_addresses(self, session: requests.Session):
122
+ url = f"{self.base_url}/v1/info/ip_addresses"
123
+ response = session.get(url=url, headers=self._get_headers())
124
+ response.raise_for_status()
125
+ result = response.json()
126
+ ips = result.get("ips", [])
127
+ return [{"ip": ip} for ip in ips]
128
+
129
+ def fetch_messages(
130
+ self,
131
+ session: requests.Session,
132
+ start_ts: int | None = None,
133
+ end_ts: int | None = None,
134
+ ) -> list:
135
+ url = f"{self.base_url}/v1/messages"
136
+ params = {"limit": 1000}
137
+
138
+ if start_ts:
139
+ params["start_ts"] = start_ts
140
+ if end_ts:
141
+ params["end_ts"] = end_ts
142
+
143
+ return self._fetch_pages(session, url, params, data_key="messages")
144
+
145
+ def fetch_newsletters(self, session: requests.Session) -> list:
146
+ url = f"{self.base_url}/v1/newsletters"
147
+ params = {"limit": 100}
148
+ return self._fetch_pages(session, url, params, data_key="newsletters")
149
+
150
+ def fetch_reporting_webhooks(self, session: requests.Session) -> list:
151
+ url = f"{self.base_url}/v1/reporting_webhooks"
152
+ response = session.get(url=url, headers=self._get_headers())
153
+ response.raise_for_status()
154
+ result = response.json()
155
+ return result.get("reporting_webhooks") or []
156
+
157
+ def fetch_segments(self, session: requests.Session) -> list:
158
+ url = f"{self.base_url}/v1/segments"
159
+ return self._fetch_pages(session, url, data_key="segments")
160
+
161
+ def fetch_transactional_messages(self, session: requests.Session) -> list:
162
+ url = f"{self.base_url}/v1/transactional"
163
+ return self._fetch_pages(session, url, data_key="transactional")
164
+
165
+ def fetch_workspaces(self, session: requests.Session) -> list:
166
+ url = f"{self.base_url}/v1/workspaces"
167
+ response = session.get(url=url, headers=self._get_headers())
168
+ response.raise_for_status()
169
+ result = response.json()
170
+ return result.get("workspaces", [])
171
+
172
+ def fetch_newsletter_test_groups(
173
+ self, session: requests.Session, newsletter_id: int
174
+ ) -> list:
175
+ url = f"{self.base_url}/v1/newsletters/{newsletter_id}/test_groups"
176
+ response = session.get(url=url, headers=self._get_headers())
177
+ response.raise_for_status()
178
+ result = response.json()
179
+
180
+ groups = []
181
+ for group in result.get("test_groups", []):
182
+ group["newsletter_id"] = newsletter_id
183
+ groups.append(group)
184
+
185
+ return groups
186
+
187
+ def fetch_newsletter_metrics(
188
+ self,
189
+ session: requests.Session,
190
+ newsletter_id: int,
191
+ period: str = "days",
192
+ ) -> list:
193
+ steps = MAX_STEPS_NEWSLETTER.get(period, 45)
194
+ url = f"{self.base_url}/v1/newsletters/{newsletter_id}/metrics"
195
+ params: dict[str, Any] = {"period": period, "steps": steps}
196
+
197
+ response = session.get(url=url, headers=self._get_headers(), params=params)
198
+ response.raise_for_status()
199
+ return self._extract_metrics(
200
+ response.json(), period, {"newsletter_id": newsletter_id}
201
+ )
202
+
203
+ def fetch_broadcast_metrics(
204
+ self,
205
+ session: requests.Session,
206
+ period: str = "days",
207
+ ) -> list:
208
+ steps = MAX_STEPS_BY_PERIOD.get(period, 45)
209
+ broadcasts = self.fetch_broadcasts(session)
210
+ all_metrics = []
211
+
212
+ for broadcast in broadcasts:
213
+ broadcast_id = broadcast.get("id")
214
+ url = f"{self.base_url}/v1/broadcasts/{broadcast_id}/metrics"
215
+ params: dict[str, Any] = {"period": period, "steps": steps}
216
+
217
+ response = session.get(url=url, headers=self._get_headers(), params=params)
218
+ response.raise_for_status()
219
+ all_metrics.extend(
220
+ self._extract_metrics(
221
+ response.json(), period, {"broadcast_id": broadcast_id}
222
+ )
223
+ )
224
+
225
+ return all_metrics
226
+
227
+ def fetch_broadcast_actions(
228
+ self, session: requests.Session, broadcast_id: int
229
+ ) -> list:
230
+ url = f"{self.base_url}/v1/broadcasts/{broadcast_id}/actions"
231
+
232
+ response = session.get(url=url, headers=self._get_headers())
233
+ response.raise_for_status()
234
+ result = response.json()
235
+
236
+ actions = []
237
+ for action in result.get("actions", []):
238
+ action["broadcast_id"] = broadcast_id
239
+ actions.append(action)
240
+
241
+ return actions
242
+
243
+ def fetch_broadcast_messages(
244
+ self,
245
+ session: requests.Session,
246
+ broadcast_id: int,
247
+ start_ts: int | None = None,
248
+ end_ts: int | None = None,
249
+ ) -> list:
250
+ url = f"{self.base_url}/v1/broadcasts/{broadcast_id}/messages"
251
+ params: dict[str, Any] = {}
252
+
253
+ if start_ts:
254
+ params["start_ts"] = start_ts
255
+ if end_ts:
256
+ params["end_ts"] = end_ts
257
+
258
+ return self._fetch_pages(session, url, params, data_key="messages")
259
+
260
+ def fetch_broadcast_action_metrics(
261
+ self,
262
+ session: requests.Session,
263
+ broadcast_id: int,
264
+ action_id: int,
265
+ period: str = "days",
266
+ ) -> list:
267
+ steps = MAX_STEPS_BY_PERIOD.get(period, 45)
268
+ url = (
269
+ f"{self.base_url}/v1/broadcasts/{broadcast_id}/actions/{action_id}/metrics"
270
+ )
271
+ params: dict[str, Any] = {"period": period, "steps": steps}
272
+
273
+ response = session.get(url=url, headers=self._get_headers(), params=params)
274
+ response.raise_for_status()
275
+ return self._extract_metrics(
276
+ response.json(),
277
+ period,
278
+ {"broadcast_id": broadcast_id, "action_id": action_id},
279
+ )
280
+
281
+ def fetch_campaign_metrics(
282
+ self,
283
+ session: requests.Session,
284
+ campaign_id: int,
285
+ period: str = "days",
286
+ start_ts: int | None = None,
287
+ end_ts: int | None = None,
288
+ ) -> list:
289
+ url = f"{self.base_url}/v1/campaigns/{campaign_id}/metrics"
290
+ params: dict[str, Any] = {"version": 2, "res": period}
291
+
292
+ if start_ts:
293
+ params["start"] = start_ts
294
+ if end_ts:
295
+ params["end"] = end_ts
296
+
297
+ response = session.get(url=url, headers=self._get_headers(), params=params)
298
+ response.raise_for_status()
299
+ return self._extract_metrics(
300
+ response.json(), period, {"campaign_id": campaign_id}
301
+ )
302
+
303
+ def fetch_campaign_actions(
304
+ self, session: requests.Session, campaign_id: int
305
+ ) -> list:
306
+ url = f"{self.base_url}/v1/campaigns/{campaign_id}/actions"
307
+ actions = self._fetch_pages(session, url, data_key="actions")
308
+ for action in actions:
309
+ action["campaign_id"] = campaign_id
310
+ return actions
311
+
312
+ def fetch_sender_identities(self, session: requests.Session) -> list:
313
+ url = f"{self.base_url}/v1/sender_identities"
314
+ return self._fetch_pages(session, url, data_key="sender_identities")
315
+
316
+ def fetch_campaign_action_metrics(
317
+ self,
318
+ session: requests.Session,
319
+ campaign_id: int,
320
+ action_id: int,
321
+ period: str = "days",
322
+ start_ts: int | None = None,
323
+ end_ts: int | None = None,
324
+ ) -> list:
325
+ url = f"{self.base_url}/v1/campaigns/{campaign_id}/actions/{action_id}/metrics"
326
+ params: dict[str, Any] = {"version": 2, "res": period}
327
+
328
+ if start_ts:
329
+ params["start"] = start_ts
330
+ if end_ts:
331
+ params["end"] = end_ts
332
+
333
+ response = session.get(url=url, headers=self._get_headers(), params=params)
334
+ response.raise_for_status()
335
+ return self._extract_metrics(
336
+ response.json(),
337
+ period,
338
+ {"campaign_id": campaign_id, "action_id": action_id},
339
+ )
340
+
341
+ def fetch_customers(
342
+ self,
343
+ session: requests.Session,
344
+ limit: int = 1000,
345
+ ) -> list:
346
+ """Fetch customers by iterating through all segments and getting their members."""
347
+ # The POST /v1/customers endpoint requires a filter, so we fetch segment members instead
348
+ all_customers = {}
349
+ segments = self.fetch_segments(session)
350
+
351
+ for segment in segments:
352
+ segment_id = segment.get("id")
353
+ if segment_id:
354
+ members = self.fetch_segment_members(session, segment_id, limit)
355
+ for member in members:
356
+ # Use cio_id as unique key to deduplicate across segments
357
+ cio_id = member.get("cio_id")
358
+ if cio_id and cio_id not in all_customers:
359
+ all_customers[cio_id] = member
360
+
361
+ return list(all_customers.values())
362
+
363
+ def fetch_segment_members(
364
+ self,
365
+ session: requests.Session,
366
+ segment_id: int,
367
+ limit: int = 1000,
368
+ ) -> list:
369
+ """Fetch members of a specific segment."""
370
+ url = f"{self.base_url}/v1/segments/{segment_id}/membership"
371
+ params = {"limit": limit}
372
+ return self._fetch_pages(session, url, params, data_key="identifiers")
373
+
374
+ def _fetch_pages_post(
375
+ self,
376
+ session: requests.Session,
377
+ url: str,
378
+ params: dict[str, Any] | None = None,
379
+ data_key: str = "identifiers",
380
+ ) -> list:
381
+ """Pagination helper for POST requests."""
382
+ all_items: list[Any] = []
383
+ if params is None:
384
+ params = {}
385
+ query_params: dict[str, Any] = {}
386
+
387
+ while True:
388
+ response = session.post(
389
+ url=url, headers=self._get_headers(), json=params, params=query_params
390
+ )
391
+ response.raise_for_status()
392
+ result = response.json()
393
+
394
+ items = result.get(data_key) or []
395
+ all_items.extend(items)
396
+
397
+ next_token = result.get("next")
398
+ if not next_token:
399
+ break
400
+
401
+ query_params["start"] = next_token
402
+
403
+ return all_items
404
+
405
+ def fetch_customer_attributes(
406
+ self, session: requests.Session, customer_id: str
407
+ ) -> dict | None:
408
+ """Fetch attributes for a specific customer."""
409
+ url = f"{self.base_url}/v1/customers/{customer_id}/attributes"
410
+ response = session.get(url=url, headers=self._get_headers())
411
+ if response.status_code == 404:
412
+ return None
413
+ response.raise_for_status()
414
+ result = response.json()
415
+ customer = result.get("customer", {})
416
+ customer["customer_id"] = customer_id
417
+ return customer
418
+
419
+ def fetch_customer_messages(
420
+ self,
421
+ session: requests.Session,
422
+ customer_id: str,
423
+ start_ts: int | None = None,
424
+ end_ts: int | None = None,
425
+ ) -> list:
426
+ """Fetch messages sent to a specific customer."""
427
+ url = f"{self.base_url}/v1/customers/{customer_id}/messages"
428
+ params = {}
429
+ if start_ts:
430
+ params["start_ts"] = start_ts
431
+ if end_ts:
432
+ params["end_ts"] = end_ts
433
+
434
+ messages = self._fetch_pages(session, url, params, data_key="messages")
435
+ for msg in messages:
436
+ msg["customer_id"] = customer_id
437
+ return messages
438
+
439
+ def fetch_customer_activities(
440
+ self, session: requests.Session, customer_id: str
441
+ ) -> list:
442
+ """Fetch activities for a specific customer."""
443
+ url = f"{self.base_url}/v1/customers/{customer_id}/activities"
444
+ activities = self._fetch_pages(session, url, data_key="activities")
445
+ for activity in activities:
446
+ activity["customer_id"] = customer_id
447
+ return activities
448
+
449
+ def fetch_customer_relationships(
450
+ self, session: requests.Session, customer_id: str
451
+ ) -> list:
452
+ """Fetch object relationships for a specific customer."""
453
+ url = f"{self.base_url}/v1/customers/{customer_id}/relationships"
454
+ relationships = self._fetch_pages(session, url, data_key="cio_relationships")
455
+ for rel in relationships:
456
+ rel["customer_id"] = customer_id
457
+ return relationships
458
+
459
+ def fetch_object_types(self, session: requests.Session) -> list:
460
+ """Fetch all object types in the workspace."""
461
+ url = f"{self.base_url}/v1/object_types"
462
+ response = session.get(url=url, headers=self._get_headers())
463
+ response.raise_for_status()
464
+ result = response.json()
465
+ return result.get("types", [])
466
+
467
+ def fetch_objects(
468
+ self, session: requests.Session, object_type_id: str, limit: int = 1000
469
+ ) -> list:
470
+ """Fetch objects of a specific type."""
471
+ url = f"{self.base_url}/v1/objects"
472
+ params = {"object_type_id": object_type_id, "limit": limit}
473
+ objects = self._fetch_pages_post(session, url, params, data_key="identifiers")
474
+ for obj in objects:
475
+ obj["object_type_id"] = object_type_id
476
+ return objects
477
+
478
+ def fetch_object_attributes(
479
+ self, session: requests.Session, object_type_id: str, object_id: str
480
+ ) -> dict | None:
481
+ """Fetch attributes for a specific object."""
482
+ url = f"{self.base_url}/v1/objects/{object_type_id}/{object_id}/attributes"
483
+ response = session.get(url=url, headers=self._get_headers())
484
+ if response.status_code == 404:
485
+ return None
486
+ response.raise_for_status()
487
+ result = response.json()
488
+ obj = result.get("object", {})
489
+ obj["object_type_id"] = object_type_id
490
+ obj["object_id"] = object_id
491
+ return obj
492
+
493
+ def fetch_object_relationships(
494
+ self, session: requests.Session, object_type_id: str, object_id: str
495
+ ) -> list:
496
+ """Fetch people related to a specific object."""
497
+ url = f"{self.base_url}/v1/objects/{object_type_id}/{object_id}/relationships"
498
+ relationships = self._fetch_pages(session, url, data_key="cio_relationships")
499
+ for rel in relationships:
500
+ rel["object_type_id"] = object_type_id
501
+ rel["object_id"] = object_id
502
+ return relationships
503
+
504
+ def fetch_campaign_messages(
505
+ self,
506
+ session: requests.Session,
507
+ campaign_id: int,
508
+ start_ts: int | None = None,
509
+ end_ts: int | None = None,
510
+ ) -> list:
511
+ """Fetch messages/deliveries for a specific campaign."""
512
+ url = f"{self.base_url}/v1/campaigns/{campaign_id}/messages"
513
+ params = {}
514
+ if start_ts:
515
+ params["start_ts"] = start_ts
516
+ if end_ts:
517
+ params["end_ts"] = end_ts
518
+
519
+ messages = self._fetch_pages(session, url, params, data_key="messages")
520
+ for msg in messages:
521
+ msg["campaign_id"] = campaign_id
522
+ return messages
523
+
524
+ def fetch_subscription_topics(self, session: requests.Session) -> list:
525
+ """Fetch subscription topics in the workspace."""
526
+ url = f"{self.base_url}/v1/subscription_topics"
527
+ response = session.get(url=url, headers=self._get_headers())
528
+ response.raise_for_status()
529
+ result = response.json()
530
+ return result.get("topics", [])