foxreach-cli 0.1.0__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.
@@ -0,0 +1,3 @@
1
+ """FoxReach CLI — command-line interface for the FoxReach API."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from foxreach import FoxReach
6
+
7
+ from . import config as cfg
8
+ from .output import print_error
9
+
10
+
11
+ def get_client() -> FoxReach:
12
+ api_key = cfg.get_api_key()
13
+ if not api_key:
14
+ print_error("No API key configured. Run: foxreach config set-key")
15
+ sys.exit(1)
16
+
17
+ kwargs = {"api_key": api_key}
18
+ base_url = cfg.get_base_url()
19
+ if base_url:
20
+ kwargs["base_url"] = base_url
21
+
22
+ return FoxReach(**kwargs)
File without changes
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from dataclasses import asdict
5
+
6
+ import click
7
+
8
+ from foxreach import FoxReachError
9
+
10
+ from ..client_factory import get_client
11
+ from ..output import print_detail, print_error, print_json, print_meta, print_success, print_table
12
+
13
+
14
+ @click.group()
15
+ def accounts() -> None:
16
+ """Manage email accounts."""
17
+
18
+
19
+ @accounts.command("list")
20
+ @click.option("--page", default=1, type=int)
21
+ @click.option("--limit", default=50, type=int)
22
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
23
+ def accounts_list(page: int, limit: int, as_json: bool) -> None:
24
+ """List email accounts."""
25
+ client = get_client()
26
+ try:
27
+ result = client.email_accounts.list(page=page, page_size=limit)
28
+ except FoxReachError as e:
29
+ print_error(str(e))
30
+ sys.exit(1)
31
+
32
+ if as_json:
33
+ print_json({"data": [asdict(a) for a in result.data], "meta": asdict(result.meta)})
34
+ return
35
+
36
+ if not result.data:
37
+ click.echo("No email accounts found.")
38
+ return
39
+
40
+ print_table(
41
+ result.data,
42
+ ["ID", "Email", "Display Name", "Active", "Daily Limit", "Sent Today", "Health"],
43
+ {
44
+ "ID": "id", "Email": "email", "Display Name": "display_name",
45
+ "Active": "is_active", "Daily Limit": "daily_limit",
46
+ "Sent Today": "sent_today", "Health": "health_score",
47
+ },
48
+ )
49
+ print_meta(result.meta)
50
+
51
+
52
+ @accounts.command("get")
53
+ @click.argument("account_id")
54
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
55
+ def accounts_get(account_id: str, as_json: bool) -> None:
56
+ """Get an email account by ID."""
57
+ client = get_client()
58
+ try:
59
+ account = client.email_accounts.get(account_id)
60
+ except FoxReachError as e:
61
+ print_error(str(e))
62
+ sys.exit(1)
63
+
64
+ if as_json:
65
+ print_json(account)
66
+ return
67
+
68
+ print_detail(account, [
69
+ "id", "email", "display_name", "provider", "is_active",
70
+ "daily_limit", "sent_today", "warmup_enabled",
71
+ "health_score", "bounce_rate", "reply_rate",
72
+ "connection_status", "created_at",
73
+ ])
74
+
75
+
76
+ @accounts.command("delete")
77
+ @click.argument("account_id")
78
+ @click.confirmation_option(prompt="Are you sure you want to delete this email account?")
79
+ def accounts_delete(account_id: str) -> None:
80
+ """Delete an email account."""
81
+ client = get_client()
82
+ try:
83
+ client.email_accounts.delete(account_id)
84
+ except FoxReachError as e:
85
+ print_error(str(e))
86
+ sys.exit(1)
87
+ print_success(f"Email account deleted: {account_id}")
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from dataclasses import asdict
5
+
6
+ import click
7
+
8
+ from foxreach import FoxReachError
9
+
10
+ from ..client_factory import get_client
11
+ from ..output import print_detail, print_error, print_json, print_table
12
+
13
+
14
+ @click.group()
15
+ def analytics() -> None:
16
+ """View analytics and stats."""
17
+
18
+
19
+ @analytics.command("overview")
20
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
21
+ def analytics_overview(as_json: bool) -> None:
22
+ """Show dashboard overview stats."""
23
+ client = get_client()
24
+ try:
25
+ stats = client.analytics.overview()
26
+ except FoxReachError as e:
27
+ print_error(str(e))
28
+ sys.exit(1)
29
+
30
+ if as_json:
31
+ print_json(stats)
32
+ return
33
+
34
+ print_detail(stats, [
35
+ "total_accounts", "active_accounts",
36
+ "total_campaigns", "active_campaigns",
37
+ "total_leads", "total_sent", "total_replies",
38
+ "reply_rate", "account_health_avg",
39
+ ])
40
+
41
+
42
+ @analytics.command("campaign")
43
+ @click.argument("campaign_id")
44
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
45
+ def analytics_campaign(campaign_id: str, as_json: bool) -> None:
46
+ """Show campaign analytics."""
47
+ client = get_client()
48
+ try:
49
+ stats = client.analytics.campaign(campaign_id)
50
+ except FoxReachError as e:
51
+ print_error(str(e))
52
+ sys.exit(1)
53
+
54
+ if as_json:
55
+ print_json(stats)
56
+ return
57
+
58
+ print_detail(stats, [
59
+ "sent", "delivered", "bounced", "replied", "opened",
60
+ "reply_rate", "bounce_rate",
61
+ ])
62
+
63
+ if stats.daily_stats:
64
+ click.echo("\nDaily Stats:")
65
+ print_table(
66
+ stats.daily_stats,
67
+ ["Date", "Sent", "Delivered", "Bounced", "Replied", "Opened"],
68
+ {"Date": "date", "Sent": "sent", "Delivered": "delivered", "Bounced": "bounced", "Replied": "replied", "Opened": "opened"},
69
+ )
@@ -0,0 +1,218 @@
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 CampaignCreate, CampaignUpdate, FoxReachError
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 campaigns() -> None:
17
+ """Manage campaigns."""
18
+
19
+
20
+ @campaigns.command("list")
21
+ @click.option("--status", default=None, type=click.Choice(["draft", "active", "paused", "completed"]))
22
+ @click.option("--page", default=1, type=int)
23
+ @click.option("--limit", default=50, type=int)
24
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
25
+ def campaigns_list(status: Optional[str], page: int, limit: int, as_json: bool) -> None:
26
+ """List campaigns."""
27
+ client = get_client()
28
+ try:
29
+ result = client.campaigns.list(page=page, page_size=limit, status=status)
30
+ except FoxReachError as e:
31
+ print_error(str(e))
32
+ sys.exit(1)
33
+
34
+ if as_json:
35
+ print_json({"data": [asdict(c) for c in result.data], "meta": asdict(result.meta)})
36
+ return
37
+
38
+ if not result.data:
39
+ click.echo("No campaigns found.")
40
+ return
41
+
42
+ print_table(
43
+ result.data,
44
+ ["ID", "Name", "Status", "Sent", "Replied", "Leads", "Daily Limit"],
45
+ {
46
+ "ID": "id", "Name": "name", "Status": "status",
47
+ "Sent": "total_sent", "Replied": "total_replied",
48
+ "Leads": "total_leads", "Daily Limit": "daily_limit",
49
+ },
50
+ )
51
+ print_meta(result.meta)
52
+
53
+
54
+ @campaigns.command("get")
55
+ @click.argument("campaign_id")
56
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
57
+ def campaigns_get(campaign_id: str, as_json: bool) -> None:
58
+ """Get a campaign by ID."""
59
+ client = get_client()
60
+ try:
61
+ campaign = client.campaigns.get(campaign_id)
62
+ except FoxReachError as e:
63
+ print_error(str(e))
64
+ sys.exit(1)
65
+
66
+ if as_json:
67
+ print_json(campaign)
68
+ return
69
+
70
+ print_detail(campaign, [
71
+ "id", "name", "status", "timezone", "sending_days",
72
+ "sending_start_hour", "sending_end_hour", "daily_limit",
73
+ "total_leads", "total_sent", "total_delivered",
74
+ "total_bounced", "total_replied", "total_opened",
75
+ "created_at", "started_at", "completed_at",
76
+ ])
77
+
78
+
79
+ @campaigns.command("create")
80
+ @click.option("--name", required=True, help="Campaign name")
81
+ @click.option("--timezone", default=None, help="IANA timezone (e.g. America/New_York)")
82
+ @click.option("--daily-limit", default=None, type=int, help="Max emails per day")
83
+ @click.option("--start-hour", default=None, type=int, help="Sending start hour (0-23)")
84
+ @click.option("--end-hour", default=None, type=int, help="Sending end hour (0-23)")
85
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
86
+ def campaigns_create(
87
+ name: str,
88
+ timezone: Optional[str],
89
+ daily_limit: Optional[int],
90
+ start_hour: Optional[int],
91
+ end_hour: Optional[int],
92
+ as_json: bool,
93
+ ) -> None:
94
+ """Create a new campaign."""
95
+ client = get_client()
96
+ try:
97
+ campaign = client.campaigns.create(CampaignCreate(
98
+ name=name,
99
+ timezone=timezone or "UTC",
100
+ daily_limit=daily_limit,
101
+ sending_start_hour=start_hour,
102
+ sending_end_hour=end_hour,
103
+ ))
104
+ except FoxReachError as e:
105
+ print_error(str(e))
106
+ sys.exit(1)
107
+
108
+ if as_json:
109
+ print_json(campaign)
110
+ else:
111
+ print_success(f"Campaign created: {campaign.id}")
112
+
113
+
114
+ @campaigns.command("update")
115
+ @click.argument("campaign_id")
116
+ @click.option("--name", default=None, help="Campaign name")
117
+ @click.option("--timezone", default=None, help="IANA timezone")
118
+ @click.option("--daily-limit", default=None, type=int, help="Max emails per day")
119
+ @click.option("--start-hour", default=None, type=int, help="Sending start hour")
120
+ @click.option("--end-hour", default=None, type=int, help="Sending end hour")
121
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
122
+ def campaigns_update(
123
+ campaign_id: str,
124
+ name: Optional[str],
125
+ timezone: Optional[str],
126
+ daily_limit: Optional[int],
127
+ start_hour: Optional[int],
128
+ end_hour: Optional[int],
129
+ as_json: bool,
130
+ ) -> None:
131
+ """Update a campaign."""
132
+ client = get_client()
133
+ try:
134
+ campaign = client.campaigns.update(campaign_id, CampaignUpdate(
135
+ name=name,
136
+ timezone=timezone,
137
+ daily_limit=daily_limit,
138
+ sending_start_hour=start_hour,
139
+ sending_end_hour=end_hour,
140
+ ))
141
+ except FoxReachError as e:
142
+ print_error(str(e))
143
+ sys.exit(1)
144
+
145
+ if as_json:
146
+ print_json(campaign)
147
+ else:
148
+ print_success(f"Campaign updated: {campaign.id}")
149
+
150
+
151
+ @campaigns.command("delete")
152
+ @click.argument("campaign_id")
153
+ @click.confirmation_option(prompt="Are you sure you want to delete this campaign?")
154
+ def campaigns_delete(campaign_id: str) -> None:
155
+ """Delete a campaign (must be in draft status)."""
156
+ client = get_client()
157
+ try:
158
+ client.campaigns.delete(campaign_id)
159
+ except FoxReachError as e:
160
+ print_error(str(e))
161
+ sys.exit(1)
162
+ print_success(f"Campaign deleted: {campaign_id}")
163
+
164
+
165
+ @campaigns.command("start")
166
+ @click.argument("campaign_id")
167
+ def campaigns_start(campaign_id: str) -> None:
168
+ """Start a campaign."""
169
+ client = get_client()
170
+ try:
171
+ campaign = client.campaigns.start(campaign_id)
172
+ except FoxReachError as e:
173
+ print_error(str(e))
174
+ sys.exit(1)
175
+ print_success(f"Campaign started: {campaign.name} ({campaign.id})")
176
+
177
+
178
+ @campaigns.command("pause")
179
+ @click.argument("campaign_id")
180
+ def campaigns_pause(campaign_id: str) -> None:
181
+ """Pause a running campaign."""
182
+ client = get_client()
183
+ try:
184
+ campaign = client.campaigns.pause(campaign_id)
185
+ except FoxReachError as e:
186
+ print_error(str(e))
187
+ sys.exit(1)
188
+ print_success(f"Campaign paused: {campaign.name} ({campaign.id})")
189
+
190
+
191
+ @campaigns.command("add-leads")
192
+ @click.argument("campaign_id")
193
+ @click.option("--lead-ids", required=True, help="Comma-separated lead IDs")
194
+ def campaigns_add_leads(campaign_id: str, lead_ids: str) -> None:
195
+ """Add leads to a campaign."""
196
+ ids = [i.strip() for i in lead_ids.split(",") if i.strip()]
197
+ client = get_client()
198
+ try:
199
+ result = client.campaigns.add_leads(campaign_id, ids)
200
+ except FoxReachError as e:
201
+ print_error(str(e))
202
+ sys.exit(1)
203
+ print_success(f"Added {result.get('added', len(ids))} leads to campaign {campaign_id}")
204
+
205
+
206
+ @campaigns.command("add-accounts")
207
+ @click.argument("campaign_id")
208
+ @click.option("--account-ids", required=True, help="Comma-separated account IDs")
209
+ def campaigns_add_accounts(campaign_id: str, account_ids: str) -> None:
210
+ """Assign email accounts to a campaign."""
211
+ ids = [i.strip() for i in account_ids.split(",") if i.strip()]
212
+ client = get_client()
213
+ try:
214
+ result = client.campaigns.add_accounts(campaign_id, ids)
215
+ except FoxReachError as e:
216
+ print_error(str(e))
217
+ sys.exit(1)
218
+ print_success(f"Added {result.get('added', len(ids))} accounts to campaign {campaign_id}")
@@ -0,0 +1,123 @@
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, ThreadUpdate
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 inbox() -> None:
17
+ """Manage inbox threads."""
18
+
19
+
20
+ @inbox.command("list")
21
+ @click.option("--category", default=None, help="Filter by category")
22
+ @click.option("--unread", is_flag=True, default=False, help="Show only unread")
23
+ @click.option("--starred", is_flag=True, default=False, help="Show only starred")
24
+ @click.option("--campaign", default=None, help="Filter by campaign ID")
25
+ @click.option("--account", default=None, help="Filter by account ID")
26
+ @click.option("--search", default=None, help="Search threads")
27
+ @click.option("--page", default=1, type=int)
28
+ @click.option("--limit", default=50, type=int)
29
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
30
+ def inbox_list(
31
+ category: Optional[str],
32
+ unread: bool,
33
+ starred: bool,
34
+ campaign: Optional[str],
35
+ account: Optional[str],
36
+ search: Optional[str],
37
+ page: int,
38
+ limit: int,
39
+ as_json: bool,
40
+ ) -> None:
41
+ """List inbox threads."""
42
+ client = get_client()
43
+ try:
44
+ result = client.inbox.list_threads(
45
+ page=page,
46
+ page_size=limit,
47
+ category=category,
48
+ is_read=False if unread else None,
49
+ is_starred=True if starred else None,
50
+ campaign_id=campaign,
51
+ account_id=account,
52
+ search=search,
53
+ )
54
+ except FoxReachError as e:
55
+ print_error(str(e))
56
+ sys.exit(1)
57
+
58
+ if as_json:
59
+ print_json({"data": [asdict(t) for t in result.data], "meta": asdict(result.meta)})
60
+ return
61
+
62
+ if not result.data:
63
+ click.echo("No threads found.")
64
+ return
65
+
66
+ print_table(
67
+ result.data,
68
+ ["ID", "From", "Subject", "Category", "Read", "Starred", "Received"],
69
+ {
70
+ "ID": "id", "From": "from_email", "Subject": "subject",
71
+ "Category": "category", "Read": "is_read",
72
+ "Starred": "is_starred", "Received": "received_at",
73
+ },
74
+ )
75
+ print_meta(result.meta)
76
+
77
+
78
+ @inbox.command("get")
79
+ @click.argument("reply_id")
80
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
81
+ def inbox_get(reply_id: str, as_json: bool) -> None:
82
+ """Get a thread by ID."""
83
+ client = get_client()
84
+ try:
85
+ thread = client.inbox.get(reply_id)
86
+ except FoxReachError as e:
87
+ print_error(str(e))
88
+ sys.exit(1)
89
+
90
+ if as_json:
91
+ print_json(thread)
92
+ return
93
+
94
+ print_detail(thread, [
95
+ "id", "from_email", "to_email", "subject",
96
+ "body_plain", "category", "is_read", "is_starred",
97
+ "campaign_id", "lead_id", "received_at",
98
+ ])
99
+
100
+
101
+ @inbox.command("update")
102
+ @click.argument("reply_id")
103
+ @click.option("--read/--unread", default=None, help="Mark as read or unread")
104
+ @click.option("--starred/--unstarred", default=None, help="Star or unstar")
105
+ @click.option("--category", default=None, help="Set category")
106
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
107
+ def inbox_update(reply_id: str, read: Optional[bool], starred: Optional[bool], category: Optional[str], as_json: bool) -> None:
108
+ """Update a thread (read status, star, category)."""
109
+ client = get_client()
110
+ try:
111
+ thread = client.inbox.update(reply_id, ThreadUpdate(
112
+ is_read=read,
113
+ is_starred=starred,
114
+ category=category,
115
+ ))
116
+ except FoxReachError as e:
117
+ print_error(str(e))
118
+ sys.exit(1)
119
+
120
+ if as_json:
121
+ print_json(thread)
122
+ else:
123
+ print_success(f"Thread updated: {reply_id}")