foxreach-cli 0.1.0__tar.gz → 0.2.0__tar.gz
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.
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/PKG-INFO +2 -2
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/commands/campaigns.py +41 -0
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/commands/inbox.py +73 -0
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/commands/leads.py +29 -0
- foxreach_cli-0.2.0/foxreach_cli/commands/webhooks.py +133 -0
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/main.py +2 -0
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/output.py +7 -4
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/pyproject.toml +2 -2
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/.github/workflows/publish.yml +0 -0
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/.gitignore +0 -0
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/LICENSE +0 -0
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/README.md +0 -0
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/__init__.py +0 -0
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/client_factory.py +0 -0
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/commands/__init__.py +0 -0
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/commands/accounts.py +0 -0
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/commands/analytics.py +0 -0
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/commands/sequences.py +0 -0
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/commands/templates.py +0 -0
- {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: foxreach-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: CLI for the FoxReach cold email outreach API
|
|
5
5
|
Project-URL: Homepage, https://foxreach.io
|
|
6
6
|
Project-URL: Documentation, https://docs.foxreach.io/sdks/cli
|
|
@@ -23,7 +23,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
23
23
|
Classifier: Topic :: Communications :: Email
|
|
24
24
|
Requires-Python: >=3.9
|
|
25
25
|
Requires-Dist: click>=8.0
|
|
26
|
-
Requires-Dist: foxreach>=0.
|
|
26
|
+
Requires-Dist: foxreach>=0.2.0
|
|
27
27
|
Requires-Dist: rich>=13.0
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
@@ -188,6 +188,47 @@ def campaigns_pause(campaign_id: str) -> None:
|
|
|
188
188
|
print_success(f"Campaign paused: {campaign.name} ({campaign.id})")
|
|
189
189
|
|
|
190
190
|
|
|
191
|
+
@campaigns.command("resume")
|
|
192
|
+
@click.argument("campaign_id")
|
|
193
|
+
def campaigns_resume(campaign_id: str) -> None:
|
|
194
|
+
"""Resume a paused campaign."""
|
|
195
|
+
client = get_client()
|
|
196
|
+
try:
|
|
197
|
+
campaign = client.campaigns.resume(campaign_id)
|
|
198
|
+
except FoxReachError as e:
|
|
199
|
+
print_error(str(e))
|
|
200
|
+
sys.exit(1)
|
|
201
|
+
print_success(f"Campaign resumed: {campaign.name} ({campaign.id})")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@campaigns.command("remove-lead")
|
|
205
|
+
@click.argument("campaign_id")
|
|
206
|
+
@click.argument("lead_id")
|
|
207
|
+
def campaigns_remove_lead(campaign_id: str, lead_id: str) -> None:
|
|
208
|
+
"""Remove a lead from a campaign."""
|
|
209
|
+
client = get_client()
|
|
210
|
+
try:
|
|
211
|
+
client.campaigns.remove_lead(campaign_id, lead_id)
|
|
212
|
+
except FoxReachError as e:
|
|
213
|
+
print_error(str(e))
|
|
214
|
+
sys.exit(1)
|
|
215
|
+
print_success(f"Removed lead {lead_id} from campaign {campaign_id}")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@campaigns.command("remove-account")
|
|
219
|
+
@click.argument("campaign_id")
|
|
220
|
+
@click.argument("account_id")
|
|
221
|
+
def campaigns_remove_account(campaign_id: str, account_id: str) -> None:
|
|
222
|
+
"""Remove an email account from a campaign."""
|
|
223
|
+
client = get_client()
|
|
224
|
+
try:
|
|
225
|
+
client.campaigns.remove_account(campaign_id, account_id)
|
|
226
|
+
except FoxReachError as e:
|
|
227
|
+
print_error(str(e))
|
|
228
|
+
sys.exit(1)
|
|
229
|
+
print_success(f"Removed account {account_id} from campaign {campaign_id}")
|
|
230
|
+
|
|
231
|
+
|
|
191
232
|
@campaigns.command("add-leads")
|
|
192
233
|
@click.argument("campaign_id")
|
|
193
234
|
@click.option("--lead-ids", required=True, help="Comma-separated lead IDs")
|
|
@@ -121,3 +121,76 @@ def inbox_update(reply_id: str, read: Optional[bool], starred: Optional[bool], c
|
|
|
121
121
|
print_json(thread)
|
|
122
122
|
else:
|
|
123
123
|
print_success(f"Thread updated: {reply_id}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@inbox.command("conversation")
|
|
127
|
+
@click.argument("reply_id")
|
|
128
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
129
|
+
def inbox_conversation(reply_id: str, as_json: bool) -> None:
|
|
130
|
+
"""Get the full conversation thread for a reply."""
|
|
131
|
+
client = get_client()
|
|
132
|
+
try:
|
|
133
|
+
result = client.inbox.get_conversation(reply_id)
|
|
134
|
+
except FoxReachError as e:
|
|
135
|
+
print_error(str(e))
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
|
|
138
|
+
if as_json:
|
|
139
|
+
print_json(result)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
messages = result.get("messages", [])
|
|
143
|
+
if not messages:
|
|
144
|
+
click.echo("No messages in this conversation.")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
for msg in messages:
|
|
148
|
+
direction = msg.get("direction", "?")
|
|
149
|
+
ts = msg.get("timestamp", "")[:19] if msg.get("timestamp") else "\u2014"
|
|
150
|
+
from_email = msg.get("fromEmail", "")
|
|
151
|
+
subject = msg.get("subject", "")
|
|
152
|
+
click.echo(f" [{direction:>8}] {ts} {from_email} {subject}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@inbox.command("reply")
|
|
156
|
+
@click.argument("reply_id")
|
|
157
|
+
@click.option("--body", required=True, help="Reply body text")
|
|
158
|
+
@click.option("--subject", default=None, help="Override subject line")
|
|
159
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
160
|
+
def inbox_reply(reply_id: str, body: str, subject: Optional[str], as_json: bool) -> None:
|
|
161
|
+
"""Send a reply to an inbox thread."""
|
|
162
|
+
client = get_client()
|
|
163
|
+
try:
|
|
164
|
+
result = client.inbox.send_reply(reply_id, body, subject=subject)
|
|
165
|
+
except FoxReachError as e:
|
|
166
|
+
print_error(str(e))
|
|
167
|
+
sys.exit(1)
|
|
168
|
+
|
|
169
|
+
if as_json:
|
|
170
|
+
print_json(result)
|
|
171
|
+
else:
|
|
172
|
+
print_success(f"Reply sent (message ID: {result.get('messageId', 'unknown')})")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@inbox.command("stats")
|
|
176
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
177
|
+
def inbox_stats(as_json: bool) -> None:
|
|
178
|
+
"""Get inbox statistics."""
|
|
179
|
+
client = get_client()
|
|
180
|
+
try:
|
|
181
|
+
result = client.inbox.stats()
|
|
182
|
+
except FoxReachError as e:
|
|
183
|
+
print_error(str(e))
|
|
184
|
+
sys.exit(1)
|
|
185
|
+
|
|
186
|
+
if as_json:
|
|
187
|
+
print_json(result)
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
click.echo(f" Total: {result.get('total', 0)}")
|
|
191
|
+
click.echo(f" Unread: {result.get('unread', 0)}")
|
|
192
|
+
click.echo(f" Interested: {result.get('interested', 0)}")
|
|
193
|
+
click.echo(f" Not Interested: {result.get('notInterested', 0)}")
|
|
194
|
+
click.echo(f" Out of Office: {result.get('outOfOffice', 0)}")
|
|
195
|
+
click.echo(f" Uncategorized: {result.get('uncategorized', 0)}")
|
|
196
|
+
click.echo(f" Total Sent: {result.get('totalSent', 0)}")
|
|
@@ -176,3 +176,32 @@ def leads_delete(lead_id: str) -> None:
|
|
|
176
176
|
print_error(str(e))
|
|
177
177
|
sys.exit(1)
|
|
178
178
|
print_success(f"Lead deleted: {lead_id}")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@leads.command("activity")
|
|
182
|
+
@click.argument("lead_id")
|
|
183
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
184
|
+
def leads_activity(lead_id: str, as_json: bool) -> None:
|
|
185
|
+
"""Get activity timeline for a lead."""
|
|
186
|
+
client = get_client()
|
|
187
|
+
try:
|
|
188
|
+
result = client.leads.activity(lead_id)
|
|
189
|
+
except FoxReachError as e:
|
|
190
|
+
print_error(str(e))
|
|
191
|
+
sys.exit(1)
|
|
192
|
+
|
|
193
|
+
if as_json:
|
|
194
|
+
print_json(result)
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
activities = result.get("activities", [])
|
|
198
|
+
if not activities:
|
|
199
|
+
click.echo("No activity found for this lead.")
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
for a in activities:
|
|
203
|
+
ts = a.get("timestamp", "")[:19] if a.get("timestamp") else "\u2014"
|
|
204
|
+
atype = a.get("type", "unknown")
|
|
205
|
+
subject = a.get("subject", "")
|
|
206
|
+
campaign = a.get("campaignName", "")
|
|
207
|
+
click.echo(f" {ts} {atype:<16} {subject[:50]:<50} {campaign}")
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from dataclasses import asdict
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from foxreach import FoxReachError, WebhookCreate, WebhookUpdate
|
|
10
|
+
|
|
11
|
+
from ..client_factory import get_client
|
|
12
|
+
from ..output import print_detail, print_error, print_json, print_meta, print_success, print_table
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group()
|
|
16
|
+
def webhooks() -> None:
|
|
17
|
+
"""Manage webhooks."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@webhooks.command("list")
|
|
21
|
+
@click.option("--page", default=1, type=int)
|
|
22
|
+
@click.option("--limit", default=20, type=int)
|
|
23
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
24
|
+
def webhooks_list(page: int, limit: int, as_json: bool) -> None:
|
|
25
|
+
"""List webhooks."""
|
|
26
|
+
client = get_client()
|
|
27
|
+
try:
|
|
28
|
+
result = client.webhooks.list(page=page, page_size=limit)
|
|
29
|
+
except FoxReachError as e:
|
|
30
|
+
print_error(str(e))
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
if as_json:
|
|
34
|
+
print_json({"data": [asdict(w) for w in result.data], "meta": asdict(result.meta)})
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
if not result.data:
|
|
38
|
+
click.echo("No webhooks found.")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
print_table(
|
|
42
|
+
result.data,
|
|
43
|
+
["ID", "URL", "Active", "Events", "Failures"],
|
|
44
|
+
{
|
|
45
|
+
"ID": "id", "URL": "url", "Active": "is_active",
|
|
46
|
+
"Events": lambda w: ", ".join(w.events[:3]) + ("..." if len(w.events) > 3 else ""),
|
|
47
|
+
"Failures": "consecutive_failures",
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
print_meta(result.meta)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@webhooks.command("create")
|
|
54
|
+
@click.option("--url", required=True, help="Webhook endpoint URL")
|
|
55
|
+
@click.option("--events", required=True, help="Comma-separated event types")
|
|
56
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
57
|
+
def webhooks_create(url: str, events: str, as_json: bool) -> None:
|
|
58
|
+
"""Create a new webhook."""
|
|
59
|
+
event_list = [e.strip() for e in events.split(",") if e.strip()]
|
|
60
|
+
client = get_client()
|
|
61
|
+
try:
|
|
62
|
+
webhook = client.webhooks.create(WebhookCreate(url=url, events=event_list))
|
|
63
|
+
except FoxReachError as e:
|
|
64
|
+
print_error(str(e))
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
|
|
67
|
+
if as_json:
|
|
68
|
+
print_json(webhook)
|
|
69
|
+
else:
|
|
70
|
+
print_success(f"Webhook created: {webhook.id}")
|
|
71
|
+
if webhook.secret:
|
|
72
|
+
click.echo(f" Secret: {webhook.secret}")
|
|
73
|
+
click.echo(" (Save this secret - it won't be shown again)")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@webhooks.command("update")
|
|
77
|
+
@click.argument("webhook_id")
|
|
78
|
+
@click.option("--url", default=None, help="New endpoint URL")
|
|
79
|
+
@click.option("--events", default=None, help="Comma-separated event types")
|
|
80
|
+
@click.option("--active/--inactive", default=None, help="Enable or disable")
|
|
81
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
82
|
+
def webhooks_update(webhook_id: str, url: Optional[str], events: Optional[str], active: Optional[bool], as_json: bool) -> None:
|
|
83
|
+
"""Update a webhook."""
|
|
84
|
+
event_list = [e.strip() for e in events.split(",") if e.strip()] if events else None
|
|
85
|
+
client = get_client()
|
|
86
|
+
try:
|
|
87
|
+
webhook = client.webhooks.update(webhook_id, WebhookUpdate(
|
|
88
|
+
url=url,
|
|
89
|
+
events=event_list,
|
|
90
|
+
is_active=active,
|
|
91
|
+
))
|
|
92
|
+
except FoxReachError as e:
|
|
93
|
+
print_error(str(e))
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
if as_json:
|
|
97
|
+
print_json(webhook)
|
|
98
|
+
else:
|
|
99
|
+
print_success(f"Webhook updated: {webhook.id}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@webhooks.command("delete")
|
|
103
|
+
@click.argument("webhook_id")
|
|
104
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this webhook?")
|
|
105
|
+
def webhooks_delete(webhook_id: str) -> None:
|
|
106
|
+
"""Delete a webhook."""
|
|
107
|
+
client = get_client()
|
|
108
|
+
try:
|
|
109
|
+
client.webhooks.delete(webhook_id)
|
|
110
|
+
except FoxReachError as e:
|
|
111
|
+
print_error(str(e))
|
|
112
|
+
sys.exit(1)
|
|
113
|
+
print_success(f"Webhook deleted: {webhook_id}")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@webhooks.command("events")
|
|
117
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
118
|
+
def webhooks_events(as_json: bool) -> None:
|
|
119
|
+
"""List available webhook event types."""
|
|
120
|
+
client = get_client()
|
|
121
|
+
try:
|
|
122
|
+
events = client.webhooks.list_events()
|
|
123
|
+
except FoxReachError as e:
|
|
124
|
+
print_error(str(e))
|
|
125
|
+
sys.exit(1)
|
|
126
|
+
|
|
127
|
+
if as_json:
|
|
128
|
+
print_json(events)
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
click.echo("Available webhook event types:")
|
|
132
|
+
for event in events:
|
|
133
|
+
click.echo(f" - {event}")
|
|
@@ -70,6 +70,7 @@ from .commands.templates import templates
|
|
|
70
70
|
from .commands.accounts import accounts
|
|
71
71
|
from .commands.inbox import inbox
|
|
72
72
|
from .commands.analytics import analytics
|
|
73
|
+
from .commands.webhooks import webhooks
|
|
73
74
|
|
|
74
75
|
cli.add_command(leads)
|
|
75
76
|
cli.add_command(campaigns)
|
|
@@ -78,6 +79,7 @@ cli.add_command(templates)
|
|
|
78
79
|
cli.add_command(accounts)
|
|
79
80
|
cli.add_command(inbox)
|
|
80
81
|
cli.add_command(analytics)
|
|
82
|
+
cli.add_command(webhooks)
|
|
81
83
|
|
|
82
84
|
|
|
83
85
|
def main() -> None:
|
|
@@ -19,8 +19,8 @@ def print_json(data: Any) -> None:
|
|
|
19
19
|
click.echo(json.dumps(data, indent=2, default=str))
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def print_table(rows: Sequence[Any], columns: List[str], field_map: Optional[Dict[str,
|
|
23
|
-
"""Print a rich table. field_map maps column headers to dataclass field names."""
|
|
22
|
+
def print_table(rows: Sequence[Any], columns: List[str], field_map: Optional[Dict[str, Any]] = None) -> None:
|
|
23
|
+
"""Print a rich table. field_map maps column headers to dataclass field names or callables."""
|
|
24
24
|
table = Table(show_header=True, header_style="bold cyan")
|
|
25
25
|
for col in columns:
|
|
26
26
|
table.add_column(col)
|
|
@@ -28,8 +28,11 @@ def print_table(rows: Sequence[Any], columns: List[str], field_map: Optional[Dic
|
|
|
28
28
|
for row in rows:
|
|
29
29
|
values = []
|
|
30
30
|
for col in columns:
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
getter = (field_map or {}).get(col, col.lower().replace(" ", "_"))
|
|
32
|
+
if callable(getter):
|
|
33
|
+
val = getter(row)
|
|
34
|
+
else:
|
|
35
|
+
val = getattr(row, getter, "") if hasattr(row, getter) else ""
|
|
33
36
|
values.append(str(val) if val is not None else "")
|
|
34
37
|
table.add_row(*values)
|
|
35
38
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "foxreach-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "CLI for the FoxReach cold email outreach API"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.9"
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
dependencies = [
|
|
24
24
|
"click>=8.0",
|
|
25
25
|
"rich>=13.0",
|
|
26
|
-
"foxreach>=0.
|
|
26
|
+
"foxreach>=0.2.0",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
[project.urls]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|