devrel-origin 0.2.14__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.
- devrel_origin/__init__.py +15 -0
- devrel_origin/cli/__init__.py +92 -0
- devrel_origin/cli/_common.py +243 -0
- devrel_origin/cli/analytics.py +28 -0
- devrel_origin/cli/argus.py +497 -0
- devrel_origin/cli/auth.py +227 -0
- devrel_origin/cli/config.py +108 -0
- devrel_origin/cli/content.py +259 -0
- devrel_origin/cli/cost.py +108 -0
- devrel_origin/cli/cro.py +298 -0
- devrel_origin/cli/deliverables.py +65 -0
- devrel_origin/cli/docs.py +91 -0
- devrel_origin/cli/doctor.py +178 -0
- devrel_origin/cli/experiment.py +29 -0
- devrel_origin/cli/growth.py +97 -0
- devrel_origin/cli/init.py +472 -0
- devrel_origin/cli/intel.py +27 -0
- devrel_origin/cli/kb.py +96 -0
- devrel_origin/cli/listen.py +31 -0
- devrel_origin/cli/marketing.py +66 -0
- devrel_origin/cli/migrate.py +45 -0
- devrel_origin/cli/run.py +46 -0
- devrel_origin/cli/sales.py +57 -0
- devrel_origin/cli/schedule.py +62 -0
- devrel_origin/cli/synthesize.py +28 -0
- devrel_origin/cli/triage.py +29 -0
- devrel_origin/cli/video.py +35 -0
- devrel_origin/core/__init__.py +58 -0
- devrel_origin/core/agent_config.py +75 -0
- devrel_origin/core/argus.py +964 -0
- devrel_origin/core/atlas.py +1450 -0
- devrel_origin/core/base.py +372 -0
- devrel_origin/core/cyra.py +563 -0
- devrel_origin/core/dex.py +708 -0
- devrel_origin/core/echo.py +614 -0
- devrel_origin/core/growth/__init__.py +27 -0
- devrel_origin/core/growth/recommendations.py +219 -0
- devrel_origin/core/growth/target_kinds.py +51 -0
- devrel_origin/core/iris.py +513 -0
- devrel_origin/core/kai.py +1367 -0
- devrel_origin/core/llm.py +542 -0
- devrel_origin/core/llm_backends.py +274 -0
- devrel_origin/core/mox.py +514 -0
- devrel_origin/core/nova.py +349 -0
- devrel_origin/core/pax.py +1205 -0
- devrel_origin/core/rex.py +532 -0
- devrel_origin/core/sage.py +486 -0
- devrel_origin/core/sentinel.py +385 -0
- devrel_origin/core/types.py +98 -0
- devrel_origin/core/video/__init__.py +22 -0
- devrel_origin/core/video/assembler.py +131 -0
- devrel_origin/core/video/browser_recorder.py +118 -0
- devrel_origin/core/video/desktop_recorder.py +254 -0
- devrel_origin/core/video/overlay_renderer.py +143 -0
- devrel_origin/core/video/script_parser.py +147 -0
- devrel_origin/core/video/tts_engine.py +82 -0
- devrel_origin/core/vox.py +268 -0
- devrel_origin/core/watchdog.py +321 -0
- devrel_origin/project/__init__.py +1 -0
- devrel_origin/project/config.py +75 -0
- devrel_origin/project/cost_sink.py +61 -0
- devrel_origin/project/init.py +104 -0
- devrel_origin/project/paths.py +75 -0
- devrel_origin/project/state.py +241 -0
- devrel_origin/project/templates/__init__.py +4 -0
- devrel_origin/project/templates/config.toml +24 -0
- devrel_origin/project/templates/devrel.gitignore +10 -0
- devrel_origin/project/templates/slop-blocklist.md +45 -0
- devrel_origin/project/templates/style.md +24 -0
- devrel_origin/project/templates/voice.md +29 -0
- devrel_origin/quality/__init__.py +66 -0
- devrel_origin/quality/editorial.py +357 -0
- devrel_origin/quality/persona.py +84 -0
- devrel_origin/quality/readability.py +148 -0
- devrel_origin/quality/slop.py +167 -0
- devrel_origin/quality/style.py +110 -0
- devrel_origin/quality/voice.py +15 -0
- devrel_origin/tools/__init__.py +9 -0
- devrel_origin/tools/analytics.py +304 -0
- devrel_origin/tools/api_client.py +393 -0
- devrel_origin/tools/apollo_client.py +305 -0
- devrel_origin/tools/code_validator.py +428 -0
- devrel_origin/tools/github_tools.py +297 -0
- devrel_origin/tools/instantly_client.py +412 -0
- devrel_origin/tools/kb_harvester.py +340 -0
- devrel_origin/tools/mcp_server.py +578 -0
- devrel_origin/tools/notifications.py +245 -0
- devrel_origin/tools/run_report.py +193 -0
- devrel_origin/tools/scheduler.py +231 -0
- devrel_origin/tools/search_tools.py +321 -0
- devrel_origin/tools/self_improve.py +168 -0
- devrel_origin/tools/sheets.py +236 -0
- devrel_origin-0.2.14.dist-info/METADATA +354 -0
- devrel_origin-0.2.14.dist-info/RECORD +98 -0
- devrel_origin-0.2.14.dist-info/WHEEL +5 -0
- devrel_origin-0.2.14.dist-info/entry_points.txt +2 -0
- devrel_origin-0.2.14.dist-info/licenses/LICENSE +21 -0
- devrel_origin-0.2.14.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
# tools/instantly_client.py
|
|
2
|
+
"""
|
|
3
|
+
Instantly AI API v2 async client.
|
|
4
|
+
|
|
5
|
+
Provides typed async access to Instantly's REST API for:
|
|
6
|
+
- Campaign creation, activation, and analytics
|
|
7
|
+
- Lead management (single and bulk)
|
|
8
|
+
- Email listing and reply sending
|
|
9
|
+
- Lead list management
|
|
10
|
+
|
|
11
|
+
Authentication: Bearer token via API key.
|
|
12
|
+
Rate limits: Emails endpoint 20 req/min; others higher.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Errors
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class InstantlyAPIError(Exception):
|
|
33
|
+
"""Non-retryable error from the Instantly API (4xx)."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, message: str, status_code: int = 0, response_body: Any = None):
|
|
36
|
+
super().__init__(message)
|
|
37
|
+
self.status_code = status_code
|
|
38
|
+
self.response_body = response_body
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Data Transfer Objects
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class InstantlyLead:
|
|
48
|
+
"""A lead to be added to an Instantly campaign."""
|
|
49
|
+
|
|
50
|
+
email: str
|
|
51
|
+
first_name: str = ""
|
|
52
|
+
last_name: str = ""
|
|
53
|
+
company_name: str = ""
|
|
54
|
+
title: str = ""
|
|
55
|
+
custom_variables: dict[str, str] = field(default_factory=dict)
|
|
56
|
+
|
|
57
|
+
def to_api_dict(self) -> dict[str, Any]:
|
|
58
|
+
"""Convert to Instantly API payload format."""
|
|
59
|
+
d: dict[str, Any] = {"email": self.email}
|
|
60
|
+
if self.first_name:
|
|
61
|
+
d["first_name"] = self.first_name
|
|
62
|
+
if self.last_name:
|
|
63
|
+
d["last_name"] = self.last_name
|
|
64
|
+
if self.company_name:
|
|
65
|
+
d["company_name"] = self.company_name
|
|
66
|
+
if self.title:
|
|
67
|
+
d["title"] = self.title
|
|
68
|
+
if self.custom_variables:
|
|
69
|
+
d["custom_variables"] = self.custom_variables
|
|
70
|
+
return d
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class InstantlyCampaign:
|
|
75
|
+
"""Represents an Instantly campaign."""
|
|
76
|
+
|
|
77
|
+
id: str
|
|
78
|
+
name: str
|
|
79
|
+
status: str # "draft", "active", "paused", "completed"
|
|
80
|
+
accounts: list[str] = field(default_factory=list)
|
|
81
|
+
sequences: list[dict[str, Any]] = field(default_factory=list)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class InstantlyEmail:
|
|
86
|
+
"""An email message from Instantly."""
|
|
87
|
+
|
|
88
|
+
id: str
|
|
89
|
+
campaign_id: str
|
|
90
|
+
lead_email: str
|
|
91
|
+
subject: str
|
|
92
|
+
body: str
|
|
93
|
+
is_reply: bool
|
|
94
|
+
timestamp: str
|
|
95
|
+
thread_id: str | None = None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class CampaignAnalytics:
|
|
100
|
+
"""Aggregated analytics for a campaign."""
|
|
101
|
+
|
|
102
|
+
campaign_id: str
|
|
103
|
+
campaign_name: str
|
|
104
|
+
total_leads: int
|
|
105
|
+
emails_sent: int
|
|
106
|
+
emails_opened: int
|
|
107
|
+
emails_replied: int
|
|
108
|
+
emails_bounced: int
|
|
109
|
+
open_rate: float
|
|
110
|
+
reply_rate: float
|
|
111
|
+
bounce_rate: float
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# Client
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class InstantlyClient:
|
|
120
|
+
"""Async client for Instantly AI API v2."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, api_key: str, base_url: str = "https://api.instantly.ai"):
|
|
123
|
+
self.api_key = api_key
|
|
124
|
+
self._client = httpx.AsyncClient(
|
|
125
|
+
base_url=base_url,
|
|
126
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
127
|
+
timeout=30.0,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
async def close(self) -> None:
|
|
131
|
+
"""Close the underlying HTTP client."""
|
|
132
|
+
await self._client.aclose()
|
|
133
|
+
|
|
134
|
+
@retry(
|
|
135
|
+
stop=stop_after_attempt(3),
|
|
136
|
+
wait=wait_exponential(multiplier=2, min=2, max=60),
|
|
137
|
+
retry=retry_if_exception_type(httpx.HTTPStatusError),
|
|
138
|
+
)
|
|
139
|
+
async def _request(
|
|
140
|
+
self,
|
|
141
|
+
method: str,
|
|
142
|
+
path: str,
|
|
143
|
+
json: dict | list | None = None,
|
|
144
|
+
params: dict | None = None,
|
|
145
|
+
) -> dict:
|
|
146
|
+
"""Send an HTTP request with retry on 5xx and 429."""
|
|
147
|
+
response = await self._client.request(method, path, json=json, params=params)
|
|
148
|
+
|
|
149
|
+
if response.status_code == 429 or response.status_code >= 500:
|
|
150
|
+
response.raise_for_status() # triggers tenacity retry
|
|
151
|
+
|
|
152
|
+
if 400 <= response.status_code < 500:
|
|
153
|
+
body = response.json() if response.content else {}
|
|
154
|
+
raise InstantlyAPIError(
|
|
155
|
+
f"Instantly API error {response.status_code}: {body}",
|
|
156
|
+
status_code=response.status_code,
|
|
157
|
+
response_body=body,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
logger.info(
|
|
161
|
+
"instantly_api_call",
|
|
162
|
+
extra={"method": method, "path": path, "status": response.status_code},
|
|
163
|
+
)
|
|
164
|
+
return response.json() if response.content else {}
|
|
165
|
+
|
|
166
|
+
# -- Campaigns --------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
async def create_campaign(
|
|
169
|
+
self,
|
|
170
|
+
name: str,
|
|
171
|
+
sequences: list[dict],
|
|
172
|
+
accounts: list[str] | None = None,
|
|
173
|
+
campaign_schedule: dict | None = None,
|
|
174
|
+
) -> InstantlyCampaign:
|
|
175
|
+
"""Create a new campaign."""
|
|
176
|
+
payload: dict[str, Any] = {"name": name, "sequences": sequences}
|
|
177
|
+
if accounts:
|
|
178
|
+
payload["email_list"] = accounts
|
|
179
|
+
# Instantly v2 requires campaign_schedule
|
|
180
|
+
payload["campaign_schedule"] = campaign_schedule or {
|
|
181
|
+
"schedules": [
|
|
182
|
+
{
|
|
183
|
+
"name": "Default",
|
|
184
|
+
"days": {
|
|
185
|
+
"0": True,
|
|
186
|
+
"1": True,
|
|
187
|
+
"2": True,
|
|
188
|
+
"3": True,
|
|
189
|
+
"4": True,
|
|
190
|
+
"5": False,
|
|
191
|
+
"6": False,
|
|
192
|
+
},
|
|
193
|
+
"timezone": os.getenv("CAMPAIGN_TIMEZONE", "America/New_York"),
|
|
194
|
+
"timing": {"from": "09:00", "to": "17:00"},
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
}
|
|
198
|
+
data = await self._request("POST", "/api/v2/campaigns", json=payload)
|
|
199
|
+
return InstantlyCampaign(
|
|
200
|
+
id=data["id"],
|
|
201
|
+
name=data["name"],
|
|
202
|
+
status=data.get("status", "draft"),
|
|
203
|
+
accounts=data.get("accounts", []),
|
|
204
|
+
sequences=data.get("sequences", []),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
async def get_campaign(self, campaign_id: str) -> InstantlyCampaign:
|
|
208
|
+
"""Get campaign details by ID."""
|
|
209
|
+
data = await self._request("GET", f"/api/v2/campaigns/{campaign_id}")
|
|
210
|
+
return InstantlyCampaign(
|
|
211
|
+
id=data["id"],
|
|
212
|
+
name=data["name"],
|
|
213
|
+
status=data.get("status", ""),
|
|
214
|
+
accounts=data.get("accounts", []),
|
|
215
|
+
sequences=data.get("sequences", []),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
async def list_campaigns(
|
|
219
|
+
self,
|
|
220
|
+
limit: int = 100,
|
|
221
|
+
skip: int = 0,
|
|
222
|
+
) -> list[InstantlyCampaign]:
|
|
223
|
+
"""List campaigns with pagination."""
|
|
224
|
+
data = await self._request(
|
|
225
|
+
"GET",
|
|
226
|
+
"/api/v2/campaigns",
|
|
227
|
+
params={"limit": limit, "skip": skip},
|
|
228
|
+
)
|
|
229
|
+
return [
|
|
230
|
+
InstantlyCampaign(
|
|
231
|
+
id=c["id"],
|
|
232
|
+
name=c["name"],
|
|
233
|
+
status=c.get("status", ""),
|
|
234
|
+
accounts=c.get("accounts", []),
|
|
235
|
+
sequences=c.get("sequences", []),
|
|
236
|
+
)
|
|
237
|
+
for c in data.get("items", data if isinstance(data, list) else [])
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
async def activate_campaign(self, campaign_id: str) -> dict:
|
|
241
|
+
"""Activate/resume a campaign."""
|
|
242
|
+
return await self._request(
|
|
243
|
+
"POST",
|
|
244
|
+
f"/api/v2/campaigns/{campaign_id}/activate",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
async def stop_campaign(self, campaign_id: str) -> dict:
|
|
248
|
+
"""Stop/pause a campaign."""
|
|
249
|
+
return await self._request(
|
|
250
|
+
"POST",
|
|
251
|
+
f"/api/v2/campaigns/{campaign_id}/stop",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
async def get_campaign_analytics(self, campaign_id: str) -> CampaignAnalytics:
|
|
255
|
+
"""Get analytics overview for a campaign."""
|
|
256
|
+
data = await self._request(
|
|
257
|
+
"GET",
|
|
258
|
+
f"/api/v2/campaigns/{campaign_id}/analytics/overview",
|
|
259
|
+
)
|
|
260
|
+
return CampaignAnalytics(
|
|
261
|
+
campaign_id=data.get("campaign_id", campaign_id),
|
|
262
|
+
campaign_name=data.get("campaign_name", ""),
|
|
263
|
+
total_leads=data.get("total_leads", 0),
|
|
264
|
+
emails_sent=data.get("emails_sent", 0),
|
|
265
|
+
emails_opened=data.get("emails_opened", 0),
|
|
266
|
+
emails_replied=data.get("emails_replied", 0),
|
|
267
|
+
emails_bounced=data.get("emails_bounced", 0),
|
|
268
|
+
open_rate=data.get("open_rate", 0.0),
|
|
269
|
+
reply_rate=data.get("reply_rate", 0.0),
|
|
270
|
+
bounce_rate=data.get("bounce_rate", 0.0),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# -- Leads ------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
async def create_lead(
|
|
276
|
+
self,
|
|
277
|
+
email: str,
|
|
278
|
+
campaign_id: str,
|
|
279
|
+
first_name: str = "",
|
|
280
|
+
last_name: str = "",
|
|
281
|
+
company_name: str = "",
|
|
282
|
+
custom_variables: dict[str, str] | None = None,
|
|
283
|
+
) -> dict:
|
|
284
|
+
"""Add a single lead."""
|
|
285
|
+
lead = InstantlyLead(
|
|
286
|
+
email=email,
|
|
287
|
+
first_name=first_name,
|
|
288
|
+
last_name=last_name,
|
|
289
|
+
company_name=company_name,
|
|
290
|
+
custom_variables=custom_variables or {},
|
|
291
|
+
)
|
|
292
|
+
payload = lead.to_api_dict()
|
|
293
|
+
payload["campaign"] = campaign_id
|
|
294
|
+
return await self._request("POST", "/api/v2/leads", json=payload)
|
|
295
|
+
|
|
296
|
+
async def add_leads_bulk(
|
|
297
|
+
self,
|
|
298
|
+
campaign_id: str,
|
|
299
|
+
leads: list[InstantlyLead],
|
|
300
|
+
concurrency: int = 10,
|
|
301
|
+
) -> dict:
|
|
302
|
+
"""Add leads to a campaign and ensure custom variables are set.
|
|
303
|
+
|
|
304
|
+
Instantly deduplicates by email at the org level, so re-adding an
|
|
305
|
+
existing lead won't update its payload. After the POST we PATCH
|
|
306
|
+
custom_variables onto the returned (or existing) lead record.
|
|
307
|
+
|
|
308
|
+
Processes leads in parallel batches of *concurrency* to respect
|
|
309
|
+
API rate limits while avoiding sequential bottlenecks.
|
|
310
|
+
"""
|
|
311
|
+
added = 0
|
|
312
|
+
errors: list[str] = []
|
|
313
|
+
semaphore = asyncio.Semaphore(concurrency)
|
|
314
|
+
|
|
315
|
+
async def _add_one(lead: InstantlyLead) -> bool:
|
|
316
|
+
async with semaphore:
|
|
317
|
+
payload = lead.to_api_dict()
|
|
318
|
+
payload["campaign"] = campaign_id
|
|
319
|
+
try:
|
|
320
|
+
data = await self._request("POST", "/api/v2/leads", json=payload)
|
|
321
|
+
lead_id = data.get("id")
|
|
322
|
+
if lead_id and lead.custom_variables:
|
|
323
|
+
try:
|
|
324
|
+
await self._request(
|
|
325
|
+
"PATCH",
|
|
326
|
+
f"/api/v2/leads/{lead_id}",
|
|
327
|
+
json={"custom_variables": lead.custom_variables},
|
|
328
|
+
)
|
|
329
|
+
except Exception as patch_exc:
|
|
330
|
+
logger.debug("PATCH vars failed for %s: %s", lead.email, patch_exc)
|
|
331
|
+
return True
|
|
332
|
+
except Exception as exc:
|
|
333
|
+
errors.append(f"{lead.email}: {exc}")
|
|
334
|
+
logger.debug("Failed to add lead %s: %s", lead.email, exc)
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
results = await asyncio.gather(*[_add_one(lead) for lead in leads])
|
|
338
|
+
added = sum(1 for r in results if r)
|
|
339
|
+
return {"added": added, "errors": errors, "total": len(leads)}
|
|
340
|
+
|
|
341
|
+
async def list_leads(self, campaign_id: str, limit: int = 100) -> list[dict]:
|
|
342
|
+
"""List leads in a campaign."""
|
|
343
|
+
data = await self._request(
|
|
344
|
+
"POST",
|
|
345
|
+
"/api/v2/leads/list",
|
|
346
|
+
json={"campaign_id": campaign_id, "limit": limit},
|
|
347
|
+
)
|
|
348
|
+
return data.get("items", [])
|
|
349
|
+
|
|
350
|
+
async def update_lead_interest(self, lead_id: str, status: str) -> dict:
|
|
351
|
+
"""Update a lead's interest status."""
|
|
352
|
+
return await self._request(
|
|
353
|
+
"PATCH",
|
|
354
|
+
f"/api/v2/leads/{lead_id}/interest-status",
|
|
355
|
+
json={"interest_status": status},
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# -- Emails -----------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
async def list_emails(
|
|
361
|
+
self,
|
|
362
|
+
campaign_id: str | None = None,
|
|
363
|
+
is_reply: bool | None = None,
|
|
364
|
+
limit: int = 50,
|
|
365
|
+
) -> list[InstantlyEmail]:
|
|
366
|
+
"""List emails, optionally filtered by campaign and reply status."""
|
|
367
|
+
params: dict[str, Any] = {"limit": limit}
|
|
368
|
+
if campaign_id:
|
|
369
|
+
params["campaign_id"] = campaign_id
|
|
370
|
+
if is_reply is not None:
|
|
371
|
+
params["is_reply"] = str(is_reply).lower()
|
|
372
|
+
data = await self._request("GET", "/api/v2/emails", params=params)
|
|
373
|
+
return [
|
|
374
|
+
InstantlyEmail(
|
|
375
|
+
id=e["id"],
|
|
376
|
+
campaign_id=e.get("campaign_id", ""),
|
|
377
|
+
lead_email=e.get("lead_email", ""),
|
|
378
|
+
subject=e.get("subject", ""),
|
|
379
|
+
body=e.get("body", ""),
|
|
380
|
+
is_reply=e.get("is_reply", False),
|
|
381
|
+
timestamp=e.get("timestamp", ""),
|
|
382
|
+
thread_id=e.get("thread_id"),
|
|
383
|
+
)
|
|
384
|
+
for e in data.get("items", [])
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
async def reply_to_email(
|
|
388
|
+
self,
|
|
389
|
+
email_id: str,
|
|
390
|
+
campaign_id: str,
|
|
391
|
+
body: str,
|
|
392
|
+
thread_id: str | None = None,
|
|
393
|
+
) -> dict:
|
|
394
|
+
"""Send a reply to an email."""
|
|
395
|
+
payload: dict[str, Any] = {
|
|
396
|
+
"email_id": email_id,
|
|
397
|
+
"campaign_id": campaign_id,
|
|
398
|
+
"body": body,
|
|
399
|
+
}
|
|
400
|
+
if thread_id:
|
|
401
|
+
payload["thread_id"] = thread_id
|
|
402
|
+
return await self._request("POST", "/api/v2/emails/reply", json=payload)
|
|
403
|
+
|
|
404
|
+
# -- Lead Lists -------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
async def create_lead_list(self, name: str) -> dict:
|
|
407
|
+
"""Create a named lead list."""
|
|
408
|
+
return await self._request(
|
|
409
|
+
"POST",
|
|
410
|
+
"/api/v2/lead-lists",
|
|
411
|
+
json={"name": name},
|
|
412
|
+
)
|