ingestr 0.13.2__py3-none-any.whl → 0.14.104__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. ingestr/conftest.py +72 -0
  2. ingestr/main.py +134 -87
  3. ingestr/src/adjust/__init__.py +4 -4
  4. ingestr/src/adjust/adjust_helpers.py +7 -3
  5. ingestr/src/airtable/__init__.py +3 -2
  6. ingestr/src/allium/__init__.py +128 -0
  7. ingestr/src/anthropic/__init__.py +277 -0
  8. ingestr/src/anthropic/helpers.py +525 -0
  9. ingestr/src/applovin/__init__.py +262 -0
  10. ingestr/src/applovin_max/__init__.py +117 -0
  11. ingestr/src/appsflyer/__init__.py +325 -0
  12. ingestr/src/appsflyer/client.py +49 -45
  13. ingestr/src/appstore/__init__.py +1 -0
  14. ingestr/src/arrow/__init__.py +9 -1
  15. ingestr/src/asana_source/__init__.py +1 -1
  16. ingestr/src/attio/__init__.py +102 -0
  17. ingestr/src/attio/helpers.py +65 -0
  18. ingestr/src/blob.py +38 -11
  19. ingestr/src/buildinfo.py +1 -0
  20. ingestr/src/chess/__init__.py +1 -1
  21. ingestr/src/clickup/__init__.py +85 -0
  22. ingestr/src/clickup/helpers.py +47 -0
  23. ingestr/src/collector/spinner.py +43 -0
  24. ingestr/src/couchbase_source/__init__.py +118 -0
  25. ingestr/src/couchbase_source/helpers.py +135 -0
  26. ingestr/src/cursor/__init__.py +83 -0
  27. ingestr/src/cursor/helpers.py +188 -0
  28. ingestr/src/destinations.py +520 -33
  29. ingestr/src/docebo/__init__.py +589 -0
  30. ingestr/src/docebo/client.py +435 -0
  31. ingestr/src/docebo/helpers.py +97 -0
  32. ingestr/src/elasticsearch/__init__.py +80 -0
  33. ingestr/src/elasticsearch/helpers.py +138 -0
  34. ingestr/src/errors.py +8 -0
  35. ingestr/src/facebook_ads/__init__.py +47 -28
  36. ingestr/src/facebook_ads/helpers.py +59 -37
  37. ingestr/src/facebook_ads/settings.py +2 -0
  38. ingestr/src/facebook_ads/utils.py +39 -0
  39. ingestr/src/factory.py +116 -2
  40. ingestr/src/filesystem/__init__.py +8 -3
  41. ingestr/src/filters.py +46 -3
  42. ingestr/src/fluxx/__init__.py +9906 -0
  43. ingestr/src/fluxx/helpers.py +209 -0
  44. ingestr/src/frankfurter/__init__.py +157 -0
  45. ingestr/src/frankfurter/helpers.py +48 -0
  46. ingestr/src/freshdesk/__init__.py +89 -0
  47. ingestr/src/freshdesk/freshdesk_client.py +137 -0
  48. ingestr/src/freshdesk/settings.py +9 -0
  49. ingestr/src/fundraiseup/__init__.py +95 -0
  50. ingestr/src/fundraiseup/client.py +81 -0
  51. ingestr/src/github/__init__.py +41 -6
  52. ingestr/src/github/helpers.py +5 -5
  53. ingestr/src/google_analytics/__init__.py +22 -4
  54. ingestr/src/google_analytics/helpers.py +124 -6
  55. ingestr/src/google_sheets/__init__.py +4 -4
  56. ingestr/src/google_sheets/helpers/data_processing.py +2 -2
  57. ingestr/src/hostaway/__init__.py +302 -0
  58. ingestr/src/hostaway/client.py +288 -0
  59. ingestr/src/http/__init__.py +35 -0
  60. ingestr/src/http/readers.py +114 -0
  61. ingestr/src/http_client.py +24 -0
  62. ingestr/src/hubspot/__init__.py +66 -23
  63. ingestr/src/hubspot/helpers.py +52 -22
  64. ingestr/src/hubspot/settings.py +14 -7
  65. ingestr/src/influxdb/__init__.py +46 -0
  66. ingestr/src/influxdb/client.py +34 -0
  67. ingestr/src/intercom/__init__.py +142 -0
  68. ingestr/src/intercom/helpers.py +674 -0
  69. ingestr/src/intercom/settings.py +279 -0
  70. ingestr/src/isoc_pulse/__init__.py +159 -0
  71. ingestr/src/jira_source/__init__.py +340 -0
  72. ingestr/src/jira_source/helpers.py +439 -0
  73. ingestr/src/jira_source/settings.py +170 -0
  74. ingestr/src/kafka/__init__.py +4 -1
  75. ingestr/src/kinesis/__init__.py +139 -0
  76. ingestr/src/kinesis/helpers.py +82 -0
  77. ingestr/src/klaviyo/{_init_.py → __init__.py} +5 -6
  78. ingestr/src/linear/__init__.py +634 -0
  79. ingestr/src/linear/helpers.py +111 -0
  80. ingestr/src/linkedin_ads/helpers.py +0 -1
  81. ingestr/src/loader.py +69 -0
  82. ingestr/src/mailchimp/__init__.py +126 -0
  83. ingestr/src/mailchimp/helpers.py +226 -0
  84. ingestr/src/mailchimp/settings.py +164 -0
  85. ingestr/src/masking.py +344 -0
  86. ingestr/src/mixpanel/__init__.py +62 -0
  87. ingestr/src/mixpanel/client.py +99 -0
  88. ingestr/src/monday/__init__.py +246 -0
  89. ingestr/src/monday/helpers.py +392 -0
  90. ingestr/src/monday/settings.py +328 -0
  91. ingestr/src/mongodb/__init__.py +72 -8
  92. ingestr/src/mongodb/helpers.py +915 -38
  93. ingestr/src/partition.py +32 -0
  94. ingestr/src/personio/__init__.py +331 -0
  95. ingestr/src/personio/helpers.py +86 -0
  96. ingestr/src/phantombuster/__init__.py +65 -0
  97. ingestr/src/phantombuster/client.py +87 -0
  98. ingestr/src/pinterest/__init__.py +82 -0
  99. ingestr/src/pipedrive/__init__.py +198 -0
  100. ingestr/src/pipedrive/helpers/__init__.py +23 -0
  101. ingestr/src/pipedrive/helpers/custom_fields_munger.py +102 -0
  102. ingestr/src/pipedrive/helpers/pages.py +115 -0
  103. ingestr/src/pipedrive/settings.py +27 -0
  104. ingestr/src/pipedrive/typing.py +3 -0
  105. ingestr/src/plusvibeai/__init__.py +335 -0
  106. ingestr/src/plusvibeai/helpers.py +544 -0
  107. ingestr/src/plusvibeai/settings.py +252 -0
  108. ingestr/src/quickbooks/__init__.py +117 -0
  109. ingestr/src/resource.py +40 -0
  110. ingestr/src/revenuecat/__init__.py +83 -0
  111. ingestr/src/revenuecat/helpers.py +237 -0
  112. ingestr/src/salesforce/__init__.py +156 -0
  113. ingestr/src/salesforce/helpers.py +64 -0
  114. ingestr/src/shopify/__init__.py +1 -17
  115. ingestr/src/smartsheets/__init__.py +82 -0
  116. ingestr/src/snapchat_ads/__init__.py +489 -0
  117. ingestr/src/snapchat_ads/client.py +72 -0
  118. ingestr/src/snapchat_ads/helpers.py +535 -0
  119. ingestr/src/socrata_source/__init__.py +83 -0
  120. ingestr/src/socrata_source/helpers.py +85 -0
  121. ingestr/src/socrata_source/settings.py +8 -0
  122. ingestr/src/solidgate/__init__.py +219 -0
  123. ingestr/src/solidgate/helpers.py +154 -0
  124. ingestr/src/sources.py +3132 -212
  125. ingestr/src/stripe_analytics/__init__.py +49 -21
  126. ingestr/src/stripe_analytics/helpers.py +286 -1
  127. ingestr/src/stripe_analytics/settings.py +62 -10
  128. ingestr/src/telemetry/event.py +10 -9
  129. ingestr/src/tiktok_ads/__init__.py +12 -6
  130. ingestr/src/tiktok_ads/tiktok_helpers.py +0 -1
  131. ingestr/src/trustpilot/__init__.py +48 -0
  132. ingestr/src/trustpilot/client.py +48 -0
  133. ingestr/src/version.py +6 -1
  134. ingestr/src/wise/__init__.py +68 -0
  135. ingestr/src/wise/client.py +63 -0
  136. ingestr/src/zoom/__init__.py +99 -0
  137. ingestr/src/zoom/helpers.py +102 -0
  138. ingestr/tests/unit/test_smartsheets.py +133 -0
  139. ingestr-0.14.104.dist-info/METADATA +563 -0
  140. ingestr-0.14.104.dist-info/RECORD +203 -0
  141. ingestr/src/appsflyer/_init_.py +0 -24
  142. ingestr-0.13.2.dist-info/METADATA +0 -302
  143. ingestr-0.13.2.dist-info/RECORD +0 -107
  144. {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/WHEEL +0 -0
  145. {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/entry_points.txt +0 -0
  146. {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/licenses/LICENSE.md +0 -0
@@ -0,0 +1,544 @@
1
+ """PlusVibeAI source helpers"""
2
+
3
+ import logging
4
+ import time
5
+ from typing import Any, Dict, Iterator, Optional
6
+ from urllib.parse import urljoin
7
+
8
+ import requests
9
+
10
+ from .settings import API_BASE_PATH, DEFAULT_PAGE_SIZE, REQUEST_TIMEOUT
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class PlusVibeAIAPIError(Exception):
16
+ """Custom exception for PlusVibeAI API errors."""
17
+
18
+ def __init__(
19
+ self,
20
+ message: str,
21
+ status_code: Optional[int] = None,
22
+ response_text: Optional[str] = None,
23
+ ):
24
+ super().__init__(message)
25
+ self.status_code = status_code
26
+ self.response_text = response_text
27
+
28
+
29
+ class PlusVibeAIAuthenticationError(PlusVibeAIAPIError):
30
+ """Exception raised for authentication failures."""
31
+
32
+ pass
33
+
34
+
35
+ class PlusVibeAIRateLimitError(PlusVibeAIAPIError):
36
+ """Exception raised when rate limit is exceeded."""
37
+
38
+ pass
39
+
40
+
41
+ class PlusVibeAIClient:
42
+ """PlusVibeAI REST API client with API key authentication and pagination support."""
43
+
44
+ def __init__(
45
+ self,
46
+ api_key: str,
47
+ workspace_id: str,
48
+ base_url: str = "https://api.plusvibe.ai",
49
+ timeout: int = REQUEST_TIMEOUT,
50
+ ):
51
+ """
52
+ Initialize PlusVibeAI client with API key authentication.
53
+
54
+ Args:
55
+ api_key: API key for authentication
56
+ workspace_id: Workspace ID to access
57
+ base_url: PlusVibeAI API base URL
58
+ timeout: Request timeout in seconds
59
+ """
60
+ self.base_url = base_url.rstrip("/")
61
+ self.api_url = urljoin(self.base_url, API_BASE_PATH)
62
+ self.workspace_id = workspace_id
63
+ self.timeout = timeout
64
+
65
+ self.headers = {
66
+ "x-api-key": api_key,
67
+ "Accept": "application/json",
68
+ "Content-Type": "application/json",
69
+ }
70
+
71
+ def _make_request(
72
+ self,
73
+ endpoint: str,
74
+ params: Optional[Dict[str, Any]] = None,
75
+ method: str = "GET",
76
+ max_retries: int = 3,
77
+ backoff_factor: float = 1.0,
78
+ ) -> Dict[str, Any]:
79
+ """
80
+ Make HTTP request to PlusVibeAI API with retry logic.
81
+
82
+ Args:
83
+ endpoint: API endpoint path
84
+ params: Query parameters
85
+ method: HTTP method
86
+ max_retries: Maximum number of retry attempts
87
+ backoff_factor: Factor for exponential backoff
88
+
89
+ Returns:
90
+ JSON response data
91
+
92
+ Raises:
93
+ PlusVibeAIAPIError: If request fails after retries
94
+ PlusVibeAIAuthenticationError: If authentication fails
95
+ PlusVibeAIRateLimitError: If rate limit is exceeded (5 requests per second)
96
+ """
97
+ url = urljoin(self.api_url + "/", endpoint.lstrip("/"))
98
+
99
+ # Add workspace_id to params
100
+ if params is None:
101
+ params = {}
102
+ params["workspace_id"] = self.workspace_id
103
+
104
+ for attempt in range(max_retries + 1):
105
+ try:
106
+ response = requests.request(
107
+ method=method,
108
+ url=url,
109
+ headers=self.headers,
110
+ params=params,
111
+ timeout=self.timeout,
112
+ )
113
+
114
+ # Handle different error status codes
115
+ if response.status_code == 401:
116
+ raise PlusVibeAIAuthenticationError(
117
+ "Authentication failed. Please check your API key.",
118
+ status_code=response.status_code,
119
+ response_text=response.text,
120
+ )
121
+ elif response.status_code == 403:
122
+ raise PlusVibeAIAuthenticationError(
123
+ "Access forbidden. Please check your permissions and workspace_id.",
124
+ status_code=response.status_code,
125
+ response_text=response.text,
126
+ )
127
+ elif response.status_code == 429:
128
+ # Rate limit exceeded (5 requests per second)
129
+ retry_after = int(response.headers.get("Retry-After", 1))
130
+ if attempt < max_retries:
131
+ logger.warning(
132
+ f"Rate limit exceeded (5 requests/second). Waiting {retry_after} seconds before retry."
133
+ )
134
+ time.sleep(retry_after)
135
+ continue
136
+ else:
137
+ raise PlusVibeAIRateLimitError(
138
+ f"Rate limit exceeded after {max_retries} retries. PlusVibeAI allows 5 requests per second.",
139
+ status_code=response.status_code,
140
+ response_text=response.text,
141
+ )
142
+ elif response.status_code >= 500:
143
+ # Server error - retry with backoff
144
+ if attempt < max_retries:
145
+ wait_time = backoff_factor * (2**attempt)
146
+ logger.warning(
147
+ f"Server error {response.status_code}. Retrying in {wait_time} seconds."
148
+ )
149
+ time.sleep(wait_time)
150
+ continue
151
+ else:
152
+ raise PlusVibeAIAPIError(
153
+ f"Server error after {max_retries} retries.",
154
+ status_code=response.status_code,
155
+ response_text=response.text,
156
+ )
157
+
158
+ # Raise for other HTTP errors
159
+ response.raise_for_status()
160
+
161
+ # Try to parse JSON response
162
+ try:
163
+ return response.json()
164
+ except ValueError as e:
165
+ logger.error(
166
+ f"Invalid JSON response. Status: {response.status_code}, URL: {url}, Response text: {response.text[:500]}"
167
+ )
168
+ raise PlusVibeAIAPIError(
169
+ f"Invalid JSON response: {str(e)}",
170
+ status_code=response.status_code,
171
+ response_text=response.text,
172
+ )
173
+
174
+ except requests.RequestException as e:
175
+ if attempt < max_retries:
176
+ wait_time = backoff_factor * (2**attempt)
177
+ logger.warning(
178
+ f"Request failed: {str(e)}. Retrying in {wait_time} seconds."
179
+ )
180
+ time.sleep(wait_time)
181
+ continue
182
+ else:
183
+ raise PlusVibeAIAPIError(
184
+ f"Request failed after {max_retries} retries: {str(e)}"
185
+ )
186
+
187
+ raise PlusVibeAIAPIError(f"Request failed after {max_retries} retries")
188
+
189
+ def get_paginated(
190
+ self,
191
+ endpoint: str,
192
+ params: Optional[Dict[str, Any]] = None,
193
+ page_size: int = DEFAULT_PAGE_SIZE,
194
+ max_results: Optional[int] = None,
195
+ use_page_param: bool = False,
196
+ ) -> Iterator[Dict[str, Any]]:
197
+ """
198
+ Get paginated results from PlusVibeAI API with error handling.
199
+
200
+ Args:
201
+ endpoint: API endpoint path
202
+ params: Query parameters
203
+ page_size: Number of items per page
204
+ max_results: Maximum total results to return
205
+ use_page_param: If True, use 'page' parameter (1-based) instead of 'skip' (0-based)
206
+
207
+ Yields:
208
+ Individual items from paginated response
209
+
210
+ Raises:
211
+ PlusVibeAIAPIError: If pagination fails
212
+ """
213
+ if params is None:
214
+ params = {}
215
+
216
+ params["limit"] = page_size
217
+
218
+ if use_page_param:
219
+ params["page"] = 1
220
+ else:
221
+ params["skip"] = 0
222
+
223
+ total_returned = 0
224
+ consecutive_empty_pages = 0
225
+ max_empty_pages = 3
226
+
227
+ while True:
228
+ try:
229
+ response = self._make_request(endpoint, params)
230
+
231
+ # Handle different response structures
232
+ if "data" in response:
233
+ items = response["data"]
234
+ total = response.get("total", len(items))
235
+ elif "results" in response:
236
+ items = response["results"]
237
+ total = response.get("total", len(items))
238
+ elif isinstance(response, list):
239
+ # Some endpoints return arrays directly
240
+ items = response
241
+ total = len(items)
242
+ else:
243
+ # Single item response
244
+ yield response
245
+ break
246
+
247
+ # Check for empty pages
248
+ if not items:
249
+ consecutive_empty_pages += 1
250
+ if consecutive_empty_pages >= max_empty_pages:
251
+ logger.warning(
252
+ f"Received {consecutive_empty_pages} consecutive empty pages, stopping pagination"
253
+ )
254
+ break
255
+ else:
256
+ consecutive_empty_pages = 0
257
+
258
+ for item in items:
259
+ if max_results and total_returned >= max_results:
260
+ return
261
+ yield item
262
+ total_returned += 1
263
+
264
+ # Check if we've reached the end
265
+ if len(items) < page_size:
266
+ break
267
+
268
+ # Check if we've got all available items
269
+ if total and total_returned >= total:
270
+ break
271
+
272
+ # Move to next page
273
+ if use_page_param:
274
+ params["page"] += 1
275
+ # Safety check
276
+ if params["page"] > 10000:
277
+ logger.warning(
278
+ f"Pagination safety limit reached for {endpoint}, stopping"
279
+ )
280
+ break
281
+ else:
282
+ params["skip"] += page_size
283
+ # Safety check
284
+ if params["skip"] > 100000:
285
+ logger.warning(
286
+ f"Pagination safety limit reached for {endpoint}, stopping"
287
+ )
288
+ break
289
+
290
+ except PlusVibeAIAPIError as e:
291
+ logger.error(f"API error during pagination of {endpoint}: {str(e)}")
292
+ raise
293
+ except Exception as e:
294
+ logger.error(
295
+ f"Unexpected error during pagination of {endpoint}: {str(e)}"
296
+ )
297
+ raise PlusVibeAIAPIError(f"Pagination failed: {str(e)}")
298
+
299
+ def get_campaigns(
300
+ self,
301
+ page_size: int = DEFAULT_PAGE_SIZE,
302
+ max_results: Optional[int] = None,
303
+ ) -> Iterator[Dict[str, Any]]:
304
+ """
305
+ Get all campaigns from PlusVibeAI.
306
+
307
+ Args:
308
+ page_size: Number of items per page
309
+ max_results: Maximum total results to return
310
+
311
+ Yields:
312
+ Campaign data
313
+ """
314
+ yield from self.get_paginated(
315
+ "campaign/list-all", page_size=page_size, max_results=max_results
316
+ )
317
+
318
+ def get_leads(
319
+ self,
320
+ page_size: int = DEFAULT_PAGE_SIZE,
321
+ max_results: Optional[int] = None,
322
+ ) -> Iterator[Dict[str, Any]]:
323
+ """
324
+ Get workspace leads from PlusVibeAI.
325
+
326
+ Args:
327
+ page_size: Number of items per page
328
+ max_results: Maximum total results to return
329
+
330
+ Yields:
331
+ Lead data
332
+ """
333
+ yield from self.get_paginated(
334
+ "lead/workspace-leads",
335
+ page_size=page_size,
336
+ max_results=max_results,
337
+ use_page_param=True, # Leads endpoint uses 'page' parameter instead of 'skip'
338
+ )
339
+
340
+ def get_email_accounts(
341
+ self,
342
+ page_size: int = DEFAULT_PAGE_SIZE,
343
+ max_results: Optional[int] = None,
344
+ ) -> Iterator[Dict[str, Any]]:
345
+ """
346
+ Get email accounts from PlusVibeAI.
347
+
348
+ Args:
349
+ page_size: Number of items per page
350
+ max_results: Maximum total results to return
351
+
352
+ Yields:
353
+ Email account data
354
+ """
355
+ # Email accounts endpoint returns data in 'accounts' key
356
+ for response in self.get_paginated(
357
+ "account/list", page_size=page_size, max_results=max_results
358
+ ):
359
+ # Response structure: {"accounts": [...]}
360
+ if isinstance(response, dict) and "accounts" in response:
361
+ for account in response["accounts"]:
362
+ yield account
363
+ else:
364
+ yield response
365
+
366
+ def get_emails(
367
+ self,
368
+ max_results: Optional[int] = None,
369
+ ) -> Iterator[Dict[str, Any]]:
370
+ """
371
+ Get emails from PlusVibeAI (uses cursor-based pagination with page_trail).
372
+
373
+ Args:
374
+ max_results: Maximum total results to return
375
+
376
+ Yields:
377
+ Email data
378
+ """
379
+ params: Dict[str, Any] = {}
380
+ total_returned = 0
381
+
382
+ while True:
383
+ response = self._make_request("unibox/emails", params)
384
+
385
+ if isinstance(response, dict):
386
+ items = response.get("data", [])
387
+ page_trail = response.get("page_trail")
388
+
389
+ if not items:
390
+ break
391
+
392
+ for item in items:
393
+ if max_results and total_returned >= max_results:
394
+ return
395
+ yield item
396
+ total_returned += 1
397
+
398
+ # page_trail can be empty string when there are no more pages
399
+ if page_trail and page_trail.strip():
400
+ params["page_trail"] = page_trail
401
+ else:
402
+ break
403
+ else:
404
+ break
405
+
406
+ def get_blocklist(
407
+ self,
408
+ page_size: int = DEFAULT_PAGE_SIZE,
409
+ max_results: Optional[int] = None,
410
+ ) -> Iterator[Dict[str, Any]]:
411
+ """
412
+ Get blocklist entries from PlusVibeAI.
413
+
414
+ Note: Blocklist API returns data in format {"0": {...}, "1": {...}}
415
+ instead of standard array format.
416
+
417
+ Args:
418
+ page_size: Number of items per page
419
+ max_results: Maximum total results to return
420
+
421
+ Yields:
422
+ Blocklist entry data
423
+ """
424
+ if max_results is None:
425
+ max_results_limit = float("inf")
426
+ else:
427
+ max_results_limit = max_results
428
+
429
+ params = {"limit": page_size, "skip": 0}
430
+ total_returned = 0
431
+
432
+ while total_returned < max_results_limit:
433
+ response = self._make_request("blocklist/list", params)
434
+
435
+ # Blocklist API returns {"0": {...}, "1": {...}} format
436
+ if isinstance(response, dict):
437
+ # Extract items from numbered keys
438
+ items = []
439
+ for key in sorted(
440
+ response.keys(),
441
+ key=lambda x: int(x) if x.isdigit() else float("inf"),
442
+ ):
443
+ if key.isdigit():
444
+ items.append(response[key])
445
+
446
+ if not items:
447
+ break
448
+
449
+ for item in items:
450
+ if max_results and total_returned >= max_results:
451
+ return
452
+ yield item
453
+ total_returned += 1
454
+
455
+ # If we got fewer items than page_size, we're done
456
+ if len(items) < page_size:
457
+ break
458
+
459
+ # Move to next page
460
+ params["skip"] += page_size
461
+ else:
462
+ break
463
+
464
+ def get_webhooks(
465
+ self,
466
+ page_size: int = DEFAULT_PAGE_SIZE,
467
+ max_results: Optional[int] = None,
468
+ ) -> Iterator[Dict[str, Any]]:
469
+ """
470
+ Get webhooks from PlusVibeAI.
471
+
472
+ Args:
473
+ page_size: Number of items per page
474
+ max_results: Maximum total results to return
475
+
476
+ Yields:
477
+ Webhook data
478
+ """
479
+ # Webhooks endpoint returns data in 'hooks' key
480
+ response = self._make_request("hook/list")
481
+
482
+ if isinstance(response, dict) and "hooks" in response:
483
+ hooks = response["hooks"]
484
+ if isinstance(hooks, list):
485
+ count = 0
486
+ for hook in hooks:
487
+ if max_results and count >= max_results:
488
+ break
489
+ yield hook
490
+ count += 1
491
+ else:
492
+ # Single hook response
493
+ yield hooks
494
+ elif isinstance(response, list):
495
+ # Direct array response
496
+ count = 0
497
+ for hook in response:
498
+ if max_results and count >= max_results:
499
+ break
500
+ yield hook
501
+ count += 1
502
+ else:
503
+ # Single item response
504
+ yield response
505
+
506
+ def get_tags(
507
+ self,
508
+ page_size: int = DEFAULT_PAGE_SIZE,
509
+ max_results: Optional[int] = None,
510
+ ) -> Iterator[Dict[str, Any]]:
511
+ """
512
+ Get tags from PlusVibeAI.
513
+
514
+ Args:
515
+ page_size: Number of items per page
516
+ max_results: Maximum total results to return
517
+
518
+ Yields:
519
+ Tag data
520
+ """
521
+ yield from self.get_paginated(
522
+ "tags/list", page_size=page_size, max_results=max_results
523
+ )
524
+
525
+
526
+ def get_client(
527
+ api_key: str,
528
+ workspace_id: str,
529
+ base_url: str = "https://api.plusvibe.ai",
530
+ timeout: int = REQUEST_TIMEOUT,
531
+ ) -> PlusVibeAIClient:
532
+ """
533
+ Create and return a PlusVibeAI API client.
534
+
535
+ Args:
536
+ api_key: API key for authentication
537
+ workspace_id: Workspace ID to access
538
+ base_url: PlusVibeAI API base URL
539
+ timeout: Request timeout in seconds
540
+
541
+ Returns:
542
+ PlusVibeAIClient instance
543
+ """
544
+ return PlusVibeAIClient(api_key, workspace_id, base_url, timeout)