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,753 @@
1
+ import time
2
+ from typing import Any, Dict, Iterator, List, Optional
3
+
4
+ import pendulum
5
+ import requests
6
+
7
+ from omniload.src.http_client import create_client as create_http_client
8
+
9
+ GRAPHQL_API_BASE_URL = "https://api.fireflies.ai/graphql"
10
+
11
+
12
+ def create_client() -> requests.Session:
13
+ # - 429: Rate limit exceeded
14
+ # - 500: Internal server error (INTERNAL_SERVER_ERROR)
15
+ return create_http_client(retry_status_codes=[429, 500])
16
+
17
+
18
+ def check_graphql_errors(result: dict) -> None:
19
+ """Raise ValueError if GraphQL response contains errors."""
20
+ if "errors" in result:
21
+ error_messages = [
22
+ error.get("message", "Unknown error") for error in result["errors"]
23
+ ]
24
+ raise ValueError(f"Fireflies GraphQL Error: {', '.join(error_messages)}")
25
+
26
+
27
+ def extract_item_errors(
28
+ result: dict, items: List[dict], entity_name: str
29
+ ) -> Dict[int, List[str]]:
30
+ """Extract per-item errors from GraphQL response.
31
+
32
+ Returns a dict mapping item index to list of error field names.
33
+ Raises ValueError if errors exist but no data is available.
34
+ """
35
+ errors_by_index: Dict[int, List[str]] = {}
36
+
37
+ if "errors" in result:
38
+ if "data" in result and items:
39
+ for error in result["errors"]:
40
+ error_path = error.get("path", [])
41
+ if len(error_path) >= 2 and error_path[0] == entity_name:
42
+ item_idx = error_path[1]
43
+ if isinstance(item_idx, int) and item_idx < len(items):
44
+ field_name = error_path[2] if len(error_path) > 2 else "unknown"
45
+ if item_idx not in errors_by_index:
46
+ errors_by_index[item_idx] = []
47
+ errors_by_index[item_idx].append(field_name)
48
+ else:
49
+ check_graphql_errors(result)
50
+
51
+ return errors_by_index
52
+
53
+
54
+ def apply_item_errors(items: List[dict], errors_by_index: Dict[int, List[str]]) -> None:
55
+ """Apply error information to items."""
56
+ for idx, item in enumerate(items):
57
+ if idx in errors_by_index:
58
+ item["error"] = ", ".join(errors_by_index[idx])
59
+ else:
60
+ item["error"] = None
61
+
62
+
63
+ ACTIVE_MEETINGS_QUERY = """
64
+ query ActiveMeetings {
65
+ active_meetings {
66
+ id
67
+ title
68
+ organizer_email
69
+ meeting_link
70
+ start_time
71
+ end_time
72
+ privacy
73
+ state
74
+ }
75
+ }
76
+ """
77
+
78
+ CHANNELS_QUERY = """
79
+ query Channels {
80
+ channels {
81
+ id
82
+ title
83
+ is_private
84
+ created_by
85
+ created_at
86
+ updated_at
87
+ members {
88
+ user_id
89
+ email
90
+ name
91
+ }
92
+ }
93
+ }
94
+ """
95
+
96
+ USERS_QUERY = """
97
+ query Users {
98
+ users {
99
+ user_id
100
+ email
101
+ name
102
+ num_transcripts
103
+ recent_transcript
104
+ recent_meeting
105
+ minutes_consumed
106
+ is_admin
107
+ integrations
108
+ user_groups {
109
+ id
110
+ name
111
+ handle
112
+ members {
113
+ user_id
114
+ first_name
115
+ last_name
116
+ email
117
+ }
118
+ }
119
+ }
120
+ }
121
+ """
122
+
123
+ USER_GROUPS_QUERY = """
124
+ query UserGroups {
125
+ user_groups {
126
+ id
127
+ name
128
+ handle
129
+ members {
130
+ user_id
131
+ first_name
132
+ last_name
133
+ email
134
+ }
135
+ }
136
+ }
137
+ """
138
+
139
+ CONTACTS_QUERY = """
140
+ query Contacts {
141
+ contacts {
142
+ email
143
+ name
144
+ picture
145
+ last_meeting_date
146
+ }
147
+ }
148
+ """
149
+
150
+ BITES_QUERY = """
151
+ query Bites($my_team: Boolean, $limit: Int, $skip: Int) {
152
+ bites(my_team: $my_team, limit: $limit, skip: $skip){
153
+ transcript_id
154
+ name
155
+ id
156
+ thumbnail
157
+ preview
158
+ status
159
+ summary
160
+ user_id
161
+ start_time
162
+ end_time
163
+ summary_status
164
+ media_type
165
+ created_at
166
+ created_from {
167
+ description
168
+ duration
169
+ id
170
+ name
171
+ type
172
+ }
173
+ captions {
174
+ end_time
175
+ index
176
+ speaker_id
177
+ speaker_name
178
+ start_time
179
+ text
180
+ }
181
+ sources {
182
+ src
183
+ type
184
+ }
185
+ privacies
186
+ user {
187
+ first_name
188
+ last_name
189
+ picture
190
+ name
191
+ id
192
+ }
193
+ }
194
+ }
195
+ """
196
+
197
+ ANALYTICS_QUERY = """
198
+ query Analytics($startTime: String!, $endTime: String!) {
199
+ analytics(start_time: $startTime, end_time: $endTime) {
200
+ team {
201
+ conversation {
202
+ average_filler_words
203
+ average_filler_words_diff_pct
204
+ average_monologues_count
205
+ average_monologues_count_diff_pct
206
+ average_questions
207
+ average_questions_diff_pct
208
+ average_sentiments {
209
+ negative_pct
210
+ neutral_pct
211
+ positive_pct
212
+ }
213
+ average_silence_duration
214
+ average_silence_duration_diff_pct
215
+ average_talk_listen_ratio
216
+ average_words_per_minute
217
+ longest_monologue_duration_sec
218
+ longest_monologue_duration_diff_pct
219
+ total_filler_words
220
+ total_filler_words_diff_pct
221
+ total_meeting_notes_count
222
+ total_meetings_count
223
+ total_monologues_count
224
+ total_monologues_diff_pct
225
+ teammates_count
226
+ total_questions
227
+ total_questions_diff_pct
228
+ total_silence_duration
229
+ total_silence_duration_diff_pct
230
+ }
231
+ meeting {
232
+ count
233
+ count_diff_pct
234
+ duration
235
+ duration_diff_pct
236
+ average_count
237
+ average_count_diff_pct
238
+ average_duration
239
+ average_duration_diff_pct
240
+ }
241
+ }
242
+ users {
243
+ user_id
244
+ user_name
245
+ user_email
246
+ conversation {
247
+ talk_listen_pct
248
+ talk_listen_ratio
249
+ total_silence_duration
250
+ total_silence_duration_compare_to
251
+ total_silence_pct
252
+ total_silence_ratio
253
+ total_speak_duration
254
+ total_speak_duration_with_user
255
+ total_word_count
256
+ user_filler_words
257
+ user_filler_words_compare_to
258
+ user_filler_words_diff_pct
259
+ user_longest_monologue_sec
260
+ user_longest_monologue_compare_to
261
+ user_longest_monologue_diff_pct
262
+ user_monologues_count
263
+ user_monologues_count_compare_to
264
+ user_monologues_count_diff_pct
265
+ user_questions
266
+ user_questions_compare_to
267
+ user_questions_diff_pct
268
+ user_speak_duration
269
+ user_word_count
270
+ user_words_per_minute
271
+ user_words_per_minute_compare_to
272
+ user_words_per_minute_diff_pct
273
+ }
274
+ meeting {
275
+ count
276
+ count_diff
277
+ count_diff_compared_to
278
+ count_diff_pct
279
+ duration
280
+ duration_diff
281
+ duration_diff_compared_to
282
+ duration_diff_pct
283
+ }
284
+ }
285
+ }
286
+ }
287
+ """
288
+
289
+ TRANSCRIPTS_QUERY = """
290
+ query Transcripts(
291
+ $limit: Int
292
+ $skip: Int
293
+ $fromDate: DateTime
294
+ $toDate: DateTime
295
+ ) {
296
+ transcripts(
297
+ limit: $limit
298
+ skip: $skip
299
+ fromDate: $fromDate
300
+ toDate: $toDate
301
+ ) {
302
+ id
303
+ title
304
+ date
305
+ duration
306
+ transcript_url
307
+ audio_url
308
+ video_url
309
+ meeting_link
310
+ host_email
311
+ organizer_email
312
+ participants
313
+ fireflies_users
314
+ calendar_id
315
+ cal_id
316
+ calendar_type
317
+ channels {
318
+ id
319
+ }
320
+ speakers {
321
+ id
322
+ name
323
+ }
324
+ analytics {
325
+ sentiments {
326
+ negative_pct
327
+ neutral_pct
328
+ positive_pct
329
+ }
330
+ categories {
331
+ questions
332
+ date_times
333
+ metrics
334
+ tasks
335
+ }
336
+ speakers {
337
+ speaker_id
338
+ name
339
+ duration
340
+ duration_pct
341
+ word_count
342
+ words_per_minute
343
+ longest_monologue
344
+ monologues_count
345
+ filler_words
346
+ questions
347
+ }
348
+ }
349
+ sentences {
350
+ index
351
+ speaker_name
352
+ speaker_id
353
+ text
354
+ raw_text
355
+ start_time
356
+ end_time
357
+ ai_filters {
358
+ task
359
+ pricing
360
+ metric
361
+ question
362
+ date_and_time
363
+ text_cleanup
364
+ sentiment
365
+ }
366
+ }
367
+ meeting_info {
368
+ fred_joined
369
+ silent_meeting
370
+ summary_status
371
+ }
372
+ meeting_attendees {
373
+ displayName
374
+ email
375
+ phoneNumber
376
+ name
377
+ location
378
+ }
379
+ meeting_attendance {
380
+ name
381
+ join_time
382
+ leave_time
383
+ }
384
+ summary {
385
+ keywords
386
+ action_items
387
+ outline
388
+ shorthand_bullet
389
+ overview
390
+ bullet_gist
391
+ gist
392
+ short_summary
393
+ short_overview
394
+ meeting_type
395
+ topics_discussed
396
+ transcript_chapters
397
+ }
398
+ user {
399
+ user_id
400
+ email
401
+ name
402
+ num_transcripts
403
+ recent_meeting
404
+ minutes_consumed
405
+ is_admin
406
+ integrations
407
+ }
408
+ apps_preview {
409
+ outputs {
410
+ transcript_id
411
+ user_id
412
+ app_id
413
+ created_at
414
+ title
415
+ prompt
416
+ response
417
+ }
418
+ }
419
+ }
420
+ }
421
+ """
422
+
423
+
424
+ class FirefliesAPI:
425
+ def __init__(self, api_key: str):
426
+ self.api_key = api_key
427
+ self.headers = {
428
+ "Authorization": f"Bearer {api_key}",
429
+ "Content-Type": "application/json",
430
+ }
431
+ self.client = create_client()
432
+
433
+ def fetch_active_meetings(self) -> Iterator[List[dict]]:
434
+ response = self.client.post(
435
+ url=GRAPHQL_API_BASE_URL,
436
+ json={"query": ACTIVE_MEETINGS_QUERY},
437
+ headers=self.headers,
438
+ )
439
+
440
+ response.raise_for_status()
441
+
442
+ result = response.json()
443
+ check_graphql_errors(result)
444
+
445
+ active_meetings = result.get("data", {}).get("active_meetings", [])
446
+
447
+ if active_meetings:
448
+ yield active_meetings
449
+
450
+ def _parse_date_range(
451
+ self, from_date: Optional[str], to_date: Optional[str]
452
+ ) -> tuple[pendulum.DateTime, pendulum.DateTime]:
453
+ """Parse date strings into pendulum DateTime objects."""
454
+ start: pendulum.DateTime = (
455
+ pendulum.parse(from_date) # type: ignore[assignment]
456
+ if from_date
457
+ else pendulum.datetime(1970, 1, 1, tz="UTC")
458
+ )
459
+ end: pendulum.DateTime = (
460
+ pendulum.parse(to_date) # type: ignore[assignment]
461
+ if to_date
462
+ else pendulum.now(tz="UTC")
463
+ )
464
+ return start, end
465
+
466
+ def fetch_analytics(
467
+ self,
468
+ from_date: Optional[str] = None,
469
+ to_date: Optional[str] = None,
470
+ ) -> Iterator[List[dict]]:
471
+ """Fetch analytics with default 30-day chunks."""
472
+ MAX_DAYS = 30
473
+ start, end = self._parse_date_range(from_date, to_date)
474
+
475
+ total_days = (end - start).days
476
+
477
+ if total_days <= MAX_DAYS:
478
+ yield from self._fetch_analytics_chunk(
479
+ start.to_iso8601_string(), end.to_iso8601_string()
480
+ )
481
+ else:
482
+ current_start: pendulum.DateTime = start
483
+
484
+ while current_start <= end:
485
+ chunk_end: pendulum.DateTime = current_start.add(days=MAX_DAYS)
486
+ if chunk_end > end:
487
+ chunk_end = end
488
+
489
+ yield from self._fetch_analytics_chunk(
490
+ current_start.to_iso8601_string(), chunk_end.to_iso8601_string()
491
+ )
492
+
493
+ current_start = chunk_end.add(days=1)
494
+ time.sleep(0.5)
495
+
496
+ def fetch_analytics_daily(
497
+ self,
498
+ from_date: Optional[str] = None,
499
+ to_date: Optional[str] = None,
500
+ ) -> Iterator[List[dict]]:
501
+ """Fetch analytics with 1-day chunks respecting provided date range."""
502
+ start, end = self._parse_date_range(from_date, to_date)
503
+ # Use actual start time for first chunk
504
+ current_start: pendulum.DateTime = start
505
+
506
+ while current_start < end:
507
+ # For first chunk or partial day: go to next midnight
508
+ # For subsequent chunks: go day by day
509
+ next_midnight: pendulum.DateTime = current_start.add(days=1).start_of("day")
510
+ chunk_end: pendulum.DateTime = min(next_midnight, end)
511
+
512
+ yield from self._fetch_analytics_chunk(
513
+ current_start.to_iso8601_string(), chunk_end.to_iso8601_string()
514
+ )
515
+
516
+ # Move to next day boundary (midnight)
517
+ current_start = chunk_end
518
+ time.sleep(0.3)
519
+
520
+ def fetch_analytics_hourly(
521
+ self,
522
+ from_date: Optional[str] = None,
523
+ to_date: Optional[str] = None,
524
+ ) -> Iterator[List[dict]]:
525
+ """Fetch analytics with 1-hour chunks respecting provided date range."""
526
+ start, end = self._parse_date_range(from_date, to_date)
527
+ # Use actual start time for first chunk
528
+ current_start: pendulum.DateTime = start
529
+
530
+ while current_start < end:
531
+ # For first chunk or partial hour: go to next full hour
532
+ # For subsequent chunks: go hour by hour
533
+ next_hour: pendulum.DateTime = current_start.add(hours=1).start_of("hour")
534
+ chunk_end: pendulum.DateTime = min(next_hour, end)
535
+
536
+ yield from self._fetch_analytics_chunk(
537
+ current_start.to_iso8601_string(), chunk_end.to_iso8601_string()
538
+ )
539
+
540
+ # Move to next hour boundary
541
+ current_start = chunk_end
542
+ time.sleep(0.1)
543
+
544
+ def fetch_analytics_monthly(
545
+ self,
546
+ from_date: Optional[str] = None,
547
+ to_date: Optional[str] = None,
548
+ ) -> Iterator[List[dict]]:
549
+ """Fetch analytics with month-aligned chunks respecting provided date range."""
550
+ start, end = self._parse_date_range(from_date, to_date)
551
+ # Use actual start date, not aligned to start of month
552
+ current_start: pendulum.DateTime = start
553
+
554
+ while current_start < end:
555
+ # Last day of current month at 00:00:00
556
+ month_last_day: pendulum.DateTime = current_start.end_of("month").start_of(
557
+ "day"
558
+ )
559
+ chunk_end: pendulum.DateTime = min(month_last_day, end)
560
+
561
+ yield from self._fetch_analytics_chunk(
562
+ current_start.to_iso8601_string(), chunk_end.to_iso8601_string()
563
+ )
564
+
565
+ # Move to start of next month
566
+ current_start = current_start.add(months=1).start_of("month")
567
+ time.sleep(0.5)
568
+
569
+ def _fetch_analytics_chunk(
570
+ self, start_time: str, end_time: str
571
+ ) -> Iterator[List[dict]]:
572
+ variables = {
573
+ "startTime": start_time,
574
+ "endTime": end_time,
575
+ }
576
+
577
+ response = self.client.post(
578
+ url=GRAPHQL_API_BASE_URL,
579
+ json={"query": ANALYTICS_QUERY, "variables": variables},
580
+ headers=self.headers,
581
+ )
582
+
583
+ response.raise_for_status()
584
+
585
+ result = response.json()
586
+
587
+ if "errors" in result:
588
+ data = result.get("data", {})
589
+ if not data or not data.get("analytics"):
590
+ check_graphql_errors(result)
591
+
592
+ analytics = result.get("data", {}).get("analytics", {})
593
+
594
+ if analytics:
595
+ analytics["start_time"] = start_time
596
+ analytics["end_time"] = end_time
597
+ yield [analytics]
598
+
599
+ def fetch_channels(self) -> Iterator[List[dict]]:
600
+ response = self.client.post(
601
+ url=GRAPHQL_API_BASE_URL,
602
+ json={"query": CHANNELS_QUERY},
603
+ headers=self.headers,
604
+ )
605
+
606
+ response.raise_for_status()
607
+
608
+ result = response.json()
609
+ check_graphql_errors(result)
610
+
611
+ channels = result.get("data", {}).get("channels", [])
612
+
613
+ if channels:
614
+ yield channels
615
+
616
+ def fetch_users(self) -> Iterator[List[dict]]:
617
+ response = self.client.post(
618
+ url=GRAPHQL_API_BASE_URL,
619
+ json={"query": USERS_QUERY},
620
+ headers=self.headers,
621
+ )
622
+
623
+ response.raise_for_status()
624
+
625
+ result = response.json()
626
+ check_graphql_errors(result)
627
+
628
+ users = result.get("data", {}).get("users", [])
629
+
630
+ if users:
631
+ yield users
632
+
633
+ def fetch_user_groups(self) -> Iterator[List[dict]]:
634
+ response = self.client.post(
635
+ url=GRAPHQL_API_BASE_URL,
636
+ json={"query": USER_GROUPS_QUERY},
637
+ headers=self.headers,
638
+ )
639
+
640
+ response.raise_for_status()
641
+
642
+ result = response.json()
643
+ check_graphql_errors(result)
644
+
645
+ user_groups = result.get("data", {}).get("user_groups", [])
646
+
647
+ if user_groups:
648
+ yield user_groups
649
+
650
+ def fetch_contacts(self) -> Iterator[List[dict]]:
651
+ response = self.client.post(
652
+ url=GRAPHQL_API_BASE_URL,
653
+ json={"query": CONTACTS_QUERY},
654
+ headers=self.headers,
655
+ )
656
+
657
+ response.raise_for_status()
658
+
659
+ result = response.json()
660
+ check_graphql_errors(result)
661
+
662
+ contacts = result.get("data", {}).get("contacts", [])
663
+
664
+ if contacts:
665
+ yield contacts
666
+
667
+ def fetch_bites(self) -> Iterator[List[dict]]:
668
+ PAGE_LIMIT = 50
669
+ skip_offset = 0
670
+
671
+ while True:
672
+ variables: Dict[str, Any] = {
673
+ "my_team": True,
674
+ "limit": PAGE_LIMIT,
675
+ }
676
+
677
+ if skip_offset > 0:
678
+ variables["skip"] = skip_offset
679
+
680
+ response = self.client.post(
681
+ url=GRAPHQL_API_BASE_URL,
682
+ json={"query": BITES_QUERY, "variables": variables},
683
+ headers=self.headers,
684
+ )
685
+
686
+ response.raise_for_status()
687
+ result = response.json()
688
+
689
+ bites = result.get("data", {}).get("bites", [])
690
+
691
+ errors_by_index = extract_item_errors(result, bites, "bites")
692
+ apply_item_errors(bites, errors_by_index)
693
+
694
+ fetched_count = len(bites)
695
+
696
+ if not bites:
697
+ break
698
+
699
+ yield bites
700
+
701
+ if fetched_count < PAGE_LIMIT:
702
+ break
703
+
704
+ time.sleep(0.5)
705
+
706
+ skip_offset += fetched_count
707
+
708
+ def fetch_transcripts(
709
+ self,
710
+ from_date: Optional[str] = None,
711
+ to_date: Optional[str] = None,
712
+ ) -> Iterator[List[dict]]:
713
+ PAGE_LIMIT = 50
714
+ skip_offset = 0
715
+
716
+ while True:
717
+ variables: Dict[str, Any] = {
718
+ "skip": skip_offset,
719
+ "limit": PAGE_LIMIT,
720
+ }
721
+
722
+ if from_date is not None:
723
+ variables["fromDate"] = from_date
724
+ if to_date is not None:
725
+ variables["toDate"] = to_date
726
+
727
+ response = self.client.post(
728
+ url=GRAPHQL_API_BASE_URL,
729
+ json={"query": TRANSCRIPTS_QUERY, "variables": variables},
730
+ headers=self.headers,
731
+ )
732
+
733
+ response.raise_for_status()
734
+ result = response.json()
735
+
736
+ transcripts = result.get("data", {}).get("transcripts", [])
737
+
738
+ errors_by_index = extract_item_errors(result, transcripts, "transcripts")
739
+ apply_item_errors(transcripts, errors_by_index)
740
+
741
+ fetched_count = len(transcripts)
742
+
743
+ if not transcripts:
744
+ break
745
+
746
+ yield transcripts
747
+
748
+ if fetched_count < PAGE_LIMIT:
749
+ break
750
+
751
+ time.sleep(0.5)
752
+
753
+ skip_offset += fetched_count