commune-cli 0.1.4__tar.gz → 0.1.5__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.
- {commune_cli-0.1.4 → commune_cli-0.1.5}/PKG-INFO +1 -1
- commune_cli-0.1.5/commune_cli/commands/credits.py +66 -0
- commune_cli-0.1.5/commune_cli/commands/phone_numbers.py +92 -0
- commune_cli-0.1.5/commune_cli/commands/sms.py +187 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/main.py +6 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/pyproject.toml +1 -1
- {commune_cli-0.1.4 → commune_cli-0.1.5}/.gitignore +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/README.md +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/__init__.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/banner.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/client.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/commands/__init__.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/commands/attachments.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/commands/config_cmd.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/commands/data.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/commands/delivery.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/commands/dmarc.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/commands/domains.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/commands/inboxes.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/commands/messages.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/commands/search.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/commands/threads.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/commands/webhooks.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/config.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/errors.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/output.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/commune_cli/state.py +0 -0
- {commune_cli-0.1.4 → commune_cli-0.1.5}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: commune-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Official CLI for the Commune email API — agent-native, pipe-friendly, covers every API surface.
|
|
5
5
|
Project-URL: Homepage, https://commune.email
|
|
6
6
|
Project-URL: Documentation, https://docs.commune.email
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""commune credits — credit balance and bundle management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ..client import CommuneClient
|
|
8
|
+
from ..errors import api_error, auth_required_error, network_error
|
|
9
|
+
from ..output import print_list, print_record
|
|
10
|
+
from ..state import AppState
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Credit balance and available bundles.", no_args_is_help=True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command("balance")
|
|
16
|
+
def credits_balance(
|
|
17
|
+
ctx: typer.Context,
|
|
18
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Get current credit balance. GET /v1/credits."""
|
|
21
|
+
state: AppState = ctx.obj or AppState()
|
|
22
|
+
if not state.has_any_auth():
|
|
23
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
24
|
+
|
|
25
|
+
client = CommuneClient.from_state(state)
|
|
26
|
+
try:
|
|
27
|
+
r = client.get("/v1/credits")
|
|
28
|
+
except Exception as exc:
|
|
29
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
30
|
+
|
|
31
|
+
if not r.is_success:
|
|
32
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
33
|
+
|
|
34
|
+
print_record(r.json(), json_output=json_output or state.should_json(), title="Credit Balance")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.command("bundles")
|
|
38
|
+
def credits_bundles(
|
|
39
|
+
ctx: typer.Context,
|
|
40
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
41
|
+
) -> None:
|
|
42
|
+
"""List available credit bundles. GET /v1/credits/bundles."""
|
|
43
|
+
state: AppState = ctx.obj or AppState()
|
|
44
|
+
if not state.has_any_auth():
|
|
45
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
46
|
+
|
|
47
|
+
client = CommuneClient.from_state(state)
|
|
48
|
+
try:
|
|
49
|
+
r = client.get("/v1/credits/bundles")
|
|
50
|
+
except Exception as exc:
|
|
51
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
52
|
+
|
|
53
|
+
if not r.is_success:
|
|
54
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
55
|
+
|
|
56
|
+
print_list(
|
|
57
|
+
r.json(),
|
|
58
|
+
json_output=json_output or state.should_json(),
|
|
59
|
+
title="Credit Bundles",
|
|
60
|
+
columns=[
|
|
61
|
+
("ID", "id"),
|
|
62
|
+
("Credits", "credits"),
|
|
63
|
+
("Price", "price"),
|
|
64
|
+
("Description", "description"),
|
|
65
|
+
],
|
|
66
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""commune phone-numbers — phone number management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from ..client import CommuneClient
|
|
10
|
+
from ..errors import api_error, auth_required_error, network_error
|
|
11
|
+
from ..output import print_list, print_record
|
|
12
|
+
from ..state import AppState
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="Phone number management: list, get, settings.", no_args_is_help=True)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("list")
|
|
18
|
+
def phone_numbers_list(
|
|
19
|
+
ctx: typer.Context,
|
|
20
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
21
|
+
) -> None:
|
|
22
|
+
"""List all phone numbers in your organization. GET /v1/phone-numbers."""
|
|
23
|
+
state: AppState = ctx.obj or AppState()
|
|
24
|
+
if not state.has_any_auth():
|
|
25
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
26
|
+
|
|
27
|
+
client = CommuneClient.from_state(state)
|
|
28
|
+
try:
|
|
29
|
+
r = client.get("/v1/phone-numbers")
|
|
30
|
+
except Exception as exc:
|
|
31
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
32
|
+
|
|
33
|
+
if not r.is_success:
|
|
34
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
35
|
+
|
|
36
|
+
print_list(
|
|
37
|
+
r.json(),
|
|
38
|
+
json_output=json_output or state.should_json(),
|
|
39
|
+
title="Phone Numbers",
|
|
40
|
+
columns=[
|
|
41
|
+
("ID", "id"),
|
|
42
|
+
("Number", "phoneNumber"),
|
|
43
|
+
("Friendly Name", "friendlyName"),
|
|
44
|
+
("Status", "status"),
|
|
45
|
+
("Created", "createdAt"),
|
|
46
|
+
],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@app.command("get")
|
|
51
|
+
def phone_numbers_get(
|
|
52
|
+
ctx: typer.Context,
|
|
53
|
+
phone_number_id: str = typer.Argument(..., help="Phone number ID."),
|
|
54
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Get a specific phone number. GET /v1/phone-numbers/{phoneNumberId}."""
|
|
57
|
+
state: AppState = ctx.obj or AppState()
|
|
58
|
+
if not state.has_any_auth():
|
|
59
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
60
|
+
|
|
61
|
+
client = CommuneClient.from_state(state)
|
|
62
|
+
try:
|
|
63
|
+
r = client.get(f"/v1/phone-numbers/{phone_number_id}")
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
66
|
+
|
|
67
|
+
if not r.is_success:
|
|
68
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
69
|
+
|
|
70
|
+
print_record(r.json(), json_output=json_output or state.should_json(), title="Phone Number")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command("settings")
|
|
74
|
+
def phone_numbers_settings(
|
|
75
|
+
ctx: typer.Context,
|
|
76
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Get SMS quota settings for your organization. GET /v1/phone-settings."""
|
|
79
|
+
state: AppState = ctx.obj or AppState()
|
|
80
|
+
if not state.has_any_auth():
|
|
81
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
82
|
+
|
|
83
|
+
client = CommuneClient.from_state(state)
|
|
84
|
+
try:
|
|
85
|
+
r = client.get("/v1/phone-settings")
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
88
|
+
|
|
89
|
+
if not r.is_success:
|
|
90
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
91
|
+
|
|
92
|
+
print_record(r.json(), json_output=json_output or state.should_json(), title="SMS Settings")
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""commune sms — send SMS and manage conversations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from ..client import CommuneClient
|
|
11
|
+
from ..errors import api_error, auth_required_error, network_error, validation_error
|
|
12
|
+
from ..output import print_json, print_list, print_record, print_success
|
|
13
|
+
from ..state import AppState
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="Send SMS and manage conversations.", no_args_is_help=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("send")
|
|
19
|
+
def sms_send(
|
|
20
|
+
ctx: typer.Context,
|
|
21
|
+
to: str = typer.Option(..., "--to", help="Recipient phone number in E.164 format (e.g. +1234567890)."),
|
|
22
|
+
body: str = typer.Option(..., "--body", help="SMS message body."),
|
|
23
|
+
phone_number_id: Optional[str] = typer.Option(
|
|
24
|
+
None,
|
|
25
|
+
"--phone-number-id",
|
|
26
|
+
help="Phone number ID to send from. Uses org default if omitted.",
|
|
27
|
+
),
|
|
28
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Send an SMS message. POST /v1/sms/send."""
|
|
31
|
+
state: AppState = ctx.obj or AppState()
|
|
32
|
+
if not state.has_any_auth():
|
|
33
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
34
|
+
|
|
35
|
+
payload: dict = {"to": to, "body": body}
|
|
36
|
+
if phone_number_id:
|
|
37
|
+
payload["phone_number_id"] = phone_number_id
|
|
38
|
+
|
|
39
|
+
client = CommuneClient.from_state(state)
|
|
40
|
+
try:
|
|
41
|
+
r = client.post("/v1/sms/send", json=payload)
|
|
42
|
+
except Exception as exc:
|
|
43
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
44
|
+
|
|
45
|
+
if not r.is_success:
|
|
46
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
47
|
+
|
|
48
|
+
data = r.json()
|
|
49
|
+
if json_output or state.should_json():
|
|
50
|
+
print_json(data)
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
msg_id = data.get("id") or data.get("messageId", "")
|
|
54
|
+
print_success(f"SMS sent. ID: [bold]{msg_id}[/bold]")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command("conversations")
|
|
58
|
+
def sms_conversations(
|
|
59
|
+
ctx: typer.Context,
|
|
60
|
+
phone_number_id: Optional[str] = typer.Option(
|
|
61
|
+
None,
|
|
62
|
+
"--phone-number-id",
|
|
63
|
+
help="Filter by phone number ID.",
|
|
64
|
+
),
|
|
65
|
+
limit: Optional[int] = typer.Option(20, "--limit", help="Maximum results to return."),
|
|
66
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
67
|
+
) -> None:
|
|
68
|
+
"""List SMS conversations. GET /v1/sms/conversations."""
|
|
69
|
+
state: AppState = ctx.obj or AppState()
|
|
70
|
+
if not state.has_any_auth():
|
|
71
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
72
|
+
|
|
73
|
+
client = CommuneClient.from_state(state)
|
|
74
|
+
try:
|
|
75
|
+
r = client.get("/v1/sms/conversations", params={
|
|
76
|
+
"phone_number_id": phone_number_id,
|
|
77
|
+
"limit": limit,
|
|
78
|
+
})
|
|
79
|
+
except Exception as exc:
|
|
80
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
81
|
+
|
|
82
|
+
if not r.is_success:
|
|
83
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
84
|
+
|
|
85
|
+
print_list(
|
|
86
|
+
r.json(),
|
|
87
|
+
json_output=json_output or state.should_json(),
|
|
88
|
+
title="SMS Conversations",
|
|
89
|
+
columns=[
|
|
90
|
+
("Remote Number", "remoteNumber"),
|
|
91
|
+
("Last Message", "lastMessage"),
|
|
92
|
+
("Direction", "lastDirection"),
|
|
93
|
+
("Updated", "updatedAt"),
|
|
94
|
+
],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@app.command("thread")
|
|
99
|
+
def sms_thread(
|
|
100
|
+
ctx: typer.Context,
|
|
101
|
+
remote_number: str = typer.Argument(..., help="Remote phone number (E.164, e.g. +1234567890)."),
|
|
102
|
+
phone_number_id: str = typer.Option(
|
|
103
|
+
...,
|
|
104
|
+
"--phone-number-id",
|
|
105
|
+
help="Phone number ID for the conversation.",
|
|
106
|
+
),
|
|
107
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Get the message thread with a specific number. GET /v1/sms/conversations/{remoteNumber}."""
|
|
110
|
+
state: AppState = ctx.obj or AppState()
|
|
111
|
+
if not state.has_any_auth():
|
|
112
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
113
|
+
|
|
114
|
+
encoded = quote(remote_number, safe="")
|
|
115
|
+
client = CommuneClient.from_state(state)
|
|
116
|
+
try:
|
|
117
|
+
r = client.get(
|
|
118
|
+
f"/v1/sms/conversations/{encoded}",
|
|
119
|
+
params={"phone_number_id": phone_number_id},
|
|
120
|
+
)
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
123
|
+
|
|
124
|
+
if not r.is_success:
|
|
125
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
126
|
+
|
|
127
|
+
data = r.json()
|
|
128
|
+
if json_output or state.should_json():
|
|
129
|
+
print_json(data)
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
messages = data if isinstance(data, list) else data.get("data", data)
|
|
133
|
+
print_list(
|
|
134
|
+
messages,
|
|
135
|
+
json_output=False,
|
|
136
|
+
title=f"Thread with {remote_number}",
|
|
137
|
+
columns=[
|
|
138
|
+
("ID", "id"),
|
|
139
|
+
("Direction", "direction"),
|
|
140
|
+
("Body", "body"),
|
|
141
|
+
("Sent At", "createdAt"),
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@app.command("search")
|
|
147
|
+
def sms_search(
|
|
148
|
+
ctx: typer.Context,
|
|
149
|
+
query: str = typer.Argument(..., help="Search query."),
|
|
150
|
+
phone_number_id: Optional[str] = typer.Option(
|
|
151
|
+
None,
|
|
152
|
+
"--phone-number-id",
|
|
153
|
+
help="Scope search to a specific phone number.",
|
|
154
|
+
),
|
|
155
|
+
limit: Optional[int] = typer.Option(20, "--limit", help="Maximum results to return."),
|
|
156
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Search SMS messages. GET /v1/sms/search."""
|
|
159
|
+
state: AppState = ctx.obj or AppState()
|
|
160
|
+
if not state.has_any_auth():
|
|
161
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
162
|
+
|
|
163
|
+
client = CommuneClient.from_state(state)
|
|
164
|
+
try:
|
|
165
|
+
r = client.get("/v1/sms/search", params={
|
|
166
|
+
"q": query,
|
|
167
|
+
"phone_number_id": phone_number_id,
|
|
168
|
+
"limit": limit,
|
|
169
|
+
})
|
|
170
|
+
except Exception as exc:
|
|
171
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
172
|
+
|
|
173
|
+
if not r.is_success:
|
|
174
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
175
|
+
|
|
176
|
+
print_list(
|
|
177
|
+
r.json(),
|
|
178
|
+
json_output=json_output or state.should_json(),
|
|
179
|
+
title=f"SMS Search: {query}",
|
|
180
|
+
columns=[
|
|
181
|
+
("ID", "id"),
|
|
182
|
+
("From", "from"),
|
|
183
|
+
("To", "to"),
|
|
184
|
+
("Body", "body"),
|
|
185
|
+
("Date", "createdAt"),
|
|
186
|
+
],
|
|
187
|
+
)
|
|
@@ -24,6 +24,9 @@ from .commands import (
|
|
|
24
24
|
webhooks,
|
|
25
25
|
dmarc,
|
|
26
26
|
data,
|
|
27
|
+
phone_numbers,
|
|
28
|
+
sms,
|
|
29
|
+
credits,
|
|
27
30
|
)
|
|
28
31
|
|
|
29
32
|
app = typer.Typer(
|
|
@@ -49,6 +52,9 @@ app.add_typer(delivery.app, name="delivery", help="Delivery metrics, even
|
|
|
49
52
|
app.add_typer(webhooks.app, name="webhooks", help="Webhook delivery log: list, retry, health.")
|
|
50
53
|
app.add_typer(dmarc.app, name="dmarc", help="DMARC reports and summary.")
|
|
51
54
|
app.add_typer(data.app, name="data", help="Data deletion requests (GDPR / destructive).")
|
|
55
|
+
app.add_typer(phone_numbers.app, name="phone-numbers", help="Phone number management: list, get, settings.")
|
|
56
|
+
app.add_typer(sms.app, name="sms", help="Send SMS and manage conversations.")
|
|
57
|
+
app.add_typer(credits.app, name="credits", help="Credit balance and available bundles.")
|
|
52
58
|
|
|
53
59
|
|
|
54
60
|
@app.callback(invoke_without_command=True)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "commune-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.5"
|
|
8
8
|
description = "Official CLI for the Commune email API — agent-native, pipe-friendly, covers every API surface."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
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
|
|
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
|