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.
Files changed (20) hide show
  1. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/PKG-INFO +2 -2
  2. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/commands/campaigns.py +41 -0
  3. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/commands/inbox.py +73 -0
  4. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/commands/leads.py +29 -0
  5. foxreach_cli-0.2.0/foxreach_cli/commands/webhooks.py +133 -0
  6. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/main.py +2 -0
  7. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/output.py +7 -4
  8. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/pyproject.toml +2 -2
  9. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/.github/workflows/publish.yml +0 -0
  10. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/.gitignore +0 -0
  11. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/LICENSE +0 -0
  12. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/README.md +0 -0
  13. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/__init__.py +0 -0
  14. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/client_factory.py +0 -0
  15. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/commands/__init__.py +0 -0
  16. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/commands/accounts.py +0 -0
  17. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/commands/analytics.py +0 -0
  18. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/commands/sequences.py +0 -0
  19. {foxreach_cli-0.1.0 → foxreach_cli-0.2.0}/foxreach_cli/commands/templates.py +0 -0
  20. {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.1.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.1.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, str]] = None) -> None:
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
- field = (field_map or {}).get(col, col.lower().replace(" ", "_"))
32
- val = getattr(row, field, "") if hasattr(row, field) else ""
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.1.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.1.0",
26
+ "foxreach>=0.2.0",
27
27
  ]
28
28
 
29
29
  [project.urls]
File without changes
File without changes
File without changes