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.
- ingestr/conftest.py +72 -0
- ingestr/main.py +134 -87
- ingestr/src/adjust/__init__.py +4 -4
- ingestr/src/adjust/adjust_helpers.py +7 -3
- ingestr/src/airtable/__init__.py +3 -2
- ingestr/src/allium/__init__.py +128 -0
- ingestr/src/anthropic/__init__.py +277 -0
- ingestr/src/anthropic/helpers.py +525 -0
- ingestr/src/applovin/__init__.py +262 -0
- ingestr/src/applovin_max/__init__.py +117 -0
- ingestr/src/appsflyer/__init__.py +325 -0
- ingestr/src/appsflyer/client.py +49 -45
- ingestr/src/appstore/__init__.py +1 -0
- ingestr/src/arrow/__init__.py +9 -1
- ingestr/src/asana_source/__init__.py +1 -1
- ingestr/src/attio/__init__.py +102 -0
- ingestr/src/attio/helpers.py +65 -0
- ingestr/src/blob.py +38 -11
- ingestr/src/buildinfo.py +1 -0
- ingestr/src/chess/__init__.py +1 -1
- ingestr/src/clickup/__init__.py +85 -0
- ingestr/src/clickup/helpers.py +47 -0
- ingestr/src/collector/spinner.py +43 -0
- ingestr/src/couchbase_source/__init__.py +118 -0
- ingestr/src/couchbase_source/helpers.py +135 -0
- ingestr/src/cursor/__init__.py +83 -0
- ingestr/src/cursor/helpers.py +188 -0
- ingestr/src/destinations.py +520 -33
- ingestr/src/docebo/__init__.py +589 -0
- ingestr/src/docebo/client.py +435 -0
- ingestr/src/docebo/helpers.py +97 -0
- ingestr/src/elasticsearch/__init__.py +80 -0
- ingestr/src/elasticsearch/helpers.py +138 -0
- ingestr/src/errors.py +8 -0
- ingestr/src/facebook_ads/__init__.py +47 -28
- ingestr/src/facebook_ads/helpers.py +59 -37
- ingestr/src/facebook_ads/settings.py +2 -0
- ingestr/src/facebook_ads/utils.py +39 -0
- ingestr/src/factory.py +116 -2
- ingestr/src/filesystem/__init__.py +8 -3
- ingestr/src/filters.py +46 -3
- ingestr/src/fluxx/__init__.py +9906 -0
- ingestr/src/fluxx/helpers.py +209 -0
- ingestr/src/frankfurter/__init__.py +157 -0
- ingestr/src/frankfurter/helpers.py +48 -0
- ingestr/src/freshdesk/__init__.py +89 -0
- ingestr/src/freshdesk/freshdesk_client.py +137 -0
- ingestr/src/freshdesk/settings.py +9 -0
- ingestr/src/fundraiseup/__init__.py +95 -0
- ingestr/src/fundraiseup/client.py +81 -0
- ingestr/src/github/__init__.py +41 -6
- ingestr/src/github/helpers.py +5 -5
- ingestr/src/google_analytics/__init__.py +22 -4
- ingestr/src/google_analytics/helpers.py +124 -6
- ingestr/src/google_sheets/__init__.py +4 -4
- ingestr/src/google_sheets/helpers/data_processing.py +2 -2
- ingestr/src/hostaway/__init__.py +302 -0
- ingestr/src/hostaway/client.py +288 -0
- ingestr/src/http/__init__.py +35 -0
- ingestr/src/http/readers.py +114 -0
- ingestr/src/http_client.py +24 -0
- ingestr/src/hubspot/__init__.py +66 -23
- ingestr/src/hubspot/helpers.py +52 -22
- ingestr/src/hubspot/settings.py +14 -7
- ingestr/src/influxdb/__init__.py +46 -0
- ingestr/src/influxdb/client.py +34 -0
- ingestr/src/intercom/__init__.py +142 -0
- ingestr/src/intercom/helpers.py +674 -0
- ingestr/src/intercom/settings.py +279 -0
- ingestr/src/isoc_pulse/__init__.py +159 -0
- ingestr/src/jira_source/__init__.py +340 -0
- ingestr/src/jira_source/helpers.py +439 -0
- ingestr/src/jira_source/settings.py +170 -0
- ingestr/src/kafka/__init__.py +4 -1
- ingestr/src/kinesis/__init__.py +139 -0
- ingestr/src/kinesis/helpers.py +82 -0
- ingestr/src/klaviyo/{_init_.py → __init__.py} +5 -6
- ingestr/src/linear/__init__.py +634 -0
- ingestr/src/linear/helpers.py +111 -0
- ingestr/src/linkedin_ads/helpers.py +0 -1
- ingestr/src/loader.py +69 -0
- ingestr/src/mailchimp/__init__.py +126 -0
- ingestr/src/mailchimp/helpers.py +226 -0
- ingestr/src/mailchimp/settings.py +164 -0
- ingestr/src/masking.py +344 -0
- ingestr/src/mixpanel/__init__.py +62 -0
- ingestr/src/mixpanel/client.py +99 -0
- ingestr/src/monday/__init__.py +246 -0
- ingestr/src/monday/helpers.py +392 -0
- ingestr/src/monday/settings.py +328 -0
- ingestr/src/mongodb/__init__.py +72 -8
- ingestr/src/mongodb/helpers.py +915 -38
- ingestr/src/partition.py +32 -0
- ingestr/src/personio/__init__.py +331 -0
- ingestr/src/personio/helpers.py +86 -0
- ingestr/src/phantombuster/__init__.py +65 -0
- ingestr/src/phantombuster/client.py +87 -0
- ingestr/src/pinterest/__init__.py +82 -0
- ingestr/src/pipedrive/__init__.py +198 -0
- ingestr/src/pipedrive/helpers/__init__.py +23 -0
- ingestr/src/pipedrive/helpers/custom_fields_munger.py +102 -0
- ingestr/src/pipedrive/helpers/pages.py +115 -0
- ingestr/src/pipedrive/settings.py +27 -0
- ingestr/src/pipedrive/typing.py +3 -0
- ingestr/src/plusvibeai/__init__.py +335 -0
- ingestr/src/plusvibeai/helpers.py +544 -0
- ingestr/src/plusvibeai/settings.py +252 -0
- ingestr/src/quickbooks/__init__.py +117 -0
- ingestr/src/resource.py +40 -0
- ingestr/src/revenuecat/__init__.py +83 -0
- ingestr/src/revenuecat/helpers.py +237 -0
- ingestr/src/salesforce/__init__.py +156 -0
- ingestr/src/salesforce/helpers.py +64 -0
- ingestr/src/shopify/__init__.py +1 -17
- ingestr/src/smartsheets/__init__.py +82 -0
- ingestr/src/snapchat_ads/__init__.py +489 -0
- ingestr/src/snapchat_ads/client.py +72 -0
- ingestr/src/snapchat_ads/helpers.py +535 -0
- ingestr/src/socrata_source/__init__.py +83 -0
- ingestr/src/socrata_source/helpers.py +85 -0
- ingestr/src/socrata_source/settings.py +8 -0
- ingestr/src/solidgate/__init__.py +219 -0
- ingestr/src/solidgate/helpers.py +154 -0
- ingestr/src/sources.py +3132 -212
- ingestr/src/stripe_analytics/__init__.py +49 -21
- ingestr/src/stripe_analytics/helpers.py +286 -1
- ingestr/src/stripe_analytics/settings.py +62 -10
- ingestr/src/telemetry/event.py +10 -9
- ingestr/src/tiktok_ads/__init__.py +12 -6
- ingestr/src/tiktok_ads/tiktok_helpers.py +0 -1
- ingestr/src/trustpilot/__init__.py +48 -0
- ingestr/src/trustpilot/client.py +48 -0
- ingestr/src/version.py +6 -1
- ingestr/src/wise/__init__.py +68 -0
- ingestr/src/wise/client.py +63 -0
- ingestr/src/zoom/__init__.py +99 -0
- ingestr/src/zoom/helpers.py +102 -0
- ingestr/tests/unit/test_smartsheets.py +133 -0
- ingestr-0.14.104.dist-info/METADATA +563 -0
- ingestr-0.14.104.dist-info/RECORD +203 -0
- ingestr/src/appsflyer/_init_.py +0 -24
- ingestr-0.13.2.dist-info/METADATA +0 -302
- ingestr-0.13.2.dist-info/RECORD +0 -107
- {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/WHEEL +0 -0
- {ingestr-0.13.2.dist-info → ingestr-0.14.104.dist-info}/entry_points.txt +0 -0
- {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)
|