commune-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.
- commune_cli/__init__.py +3 -0
- commune_cli/client.py +111 -0
- commune_cli/commands/__init__.py +1 -0
- commune_cli/commands/attachments.py +127 -0
- commune_cli/commands/config_cmd.py +89 -0
- commune_cli/commands/data.py +130 -0
- commune_cli/commands/delivery.py +126 -0
- commune_cli/commands/dmarc.py +81 -0
- commune_cli/commands/domains.py +158 -0
- commune_cli/commands/inboxes.py +282 -0
- commune_cli/commands/messages.py +143 -0
- commune_cli/commands/search.py +56 -0
- commune_cli/commands/threads.py +247 -0
- commune_cli/commands/webhooks.py +130 -0
- commune_cli/config.py +123 -0
- commune_cli/errors.py +114 -0
- commune_cli/main.py +106 -0
- commune_cli/output.py +191 -0
- commune_cli/state.py +37 -0
- commune_cli-0.1.0.dist-info/METADATA +243 -0
- commune_cli-0.1.0.dist-info/RECORD +23 -0
- commune_cli-0.1.0.dist-info/WHEEL +4 -0
- commune_cli-0.1.0.dist-info/entry_points.txt +2 -0
commune_cli/__init__.py
ADDED
commune_cli/client.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""CommuneClient — thin httpx wrapper with API key auth.
|
|
2
|
+
|
|
3
|
+
Auth: Authorization: Bearer comm_...
|
|
4
|
+
|
|
5
|
+
Raises:
|
|
6
|
+
httpx.ConnectError / httpx.TimeoutException — caller wraps with network_error()
|
|
7
|
+
All non-2xx responses — caller checks response.is_success and calls api_error()
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from .state import AppState
|
|
17
|
+
|
|
18
|
+
DEFAULT_TIMEOUT = 30.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CommuneClient:
|
|
22
|
+
"""HTTP client for the Commune API.
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
client = CommuneClient.from_state(state)
|
|
26
|
+
r = client.get("/v1/domains")
|
|
27
|
+
if not r.is_success:
|
|
28
|
+
api_error(r, json_output=state.should_json())
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
base_url: str,
|
|
34
|
+
api_key: Optional[str] = None,
|
|
35
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
36
|
+
):
|
|
37
|
+
self.base_url = base_url.rstrip("/")
|
|
38
|
+
self.api_key = api_key
|
|
39
|
+
self.timeout = timeout
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_state(cls, state: AppState) -> "CommuneClient":
|
|
43
|
+
return cls(
|
|
44
|
+
base_url=state.base_url,
|
|
45
|
+
api_key=state.api_key,
|
|
46
|
+
timeout=DEFAULT_TIMEOUT,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def _base_headers(self) -> dict[str, str]:
|
|
50
|
+
headers: dict[str, str] = {
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
"User-Agent": "commune-cli/0.1.0",
|
|
53
|
+
}
|
|
54
|
+
if self.api_key:
|
|
55
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
56
|
+
return headers
|
|
57
|
+
|
|
58
|
+
def _url(self, path: str) -> str:
|
|
59
|
+
return self.base_url + path
|
|
60
|
+
|
|
61
|
+
def _req(
|
|
62
|
+
self,
|
|
63
|
+
method: str,
|
|
64
|
+
path: str,
|
|
65
|
+
*,
|
|
66
|
+
params: Optional[dict[str, Any]] = None,
|
|
67
|
+
json: Optional[Any] = None,
|
|
68
|
+
data: Optional[bytes] = None,
|
|
69
|
+
extra_headers: Optional[dict[str, str]] = None,
|
|
70
|
+
) -> httpx.Response:
|
|
71
|
+
headers = self._base_headers()
|
|
72
|
+
if extra_headers:
|
|
73
|
+
headers.update(extra_headers)
|
|
74
|
+
if data is not None:
|
|
75
|
+
headers.pop("Content-Type", None)
|
|
76
|
+
|
|
77
|
+
if params:
|
|
78
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
79
|
+
|
|
80
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
81
|
+
return client.request(
|
|
82
|
+
method,
|
|
83
|
+
self._url(path),
|
|
84
|
+
headers=headers,
|
|
85
|
+
params=params or None,
|
|
86
|
+
json=json,
|
|
87
|
+
content=data,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def get(self, path: str, *, params: Optional[dict[str, Any]] = None) -> httpx.Response:
|
|
91
|
+
return self._req("GET", path, params=params)
|
|
92
|
+
|
|
93
|
+
def post(
|
|
94
|
+
self,
|
|
95
|
+
path: str,
|
|
96
|
+
*,
|
|
97
|
+
json: Optional[Any] = None,
|
|
98
|
+
data: Optional[bytes] = None,
|
|
99
|
+
extra_headers: Optional[dict[str, str]] = None,
|
|
100
|
+
params: Optional[dict[str, Any]] = None,
|
|
101
|
+
) -> httpx.Response:
|
|
102
|
+
return self._req("POST", path, json=json, data=data, extra_headers=extra_headers, params=params)
|
|
103
|
+
|
|
104
|
+
def patch(self, path: str, *, json: Optional[Any] = None) -> httpx.Response:
|
|
105
|
+
return self._req("PATCH", path, json=json)
|
|
106
|
+
|
|
107
|
+
def delete(self, path: str, *, params: Optional[dict[str, Any]] = None) -> httpx.Response:
|
|
108
|
+
return self._req("DELETE", path, params=params)
|
|
109
|
+
|
|
110
|
+
def put(self, path: str, *, json: Optional[Any] = None) -> httpx.Response:
|
|
111
|
+
return self._req("PUT", path, json=json)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command modules for the Commune CLI."""
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""commune attachments — upload files and get download URLs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
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_record, print_success
|
|
13
|
+
from ..state import AppState
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="Attachment management.", no_args_is_help=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("upload")
|
|
19
|
+
def attachments_upload(
|
|
20
|
+
ctx: typer.Context,
|
|
21
|
+
file: Path = typer.Argument(..., help="Path to the file to upload.", exists=True, readable=True),
|
|
22
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Upload a file as an attachment. POST /v1/attachments/upload.
|
|
25
|
+
|
|
26
|
+
Returns an attachment ID that can be referenced when sending emails.
|
|
27
|
+
"""
|
|
28
|
+
state: AppState = ctx.obj or AppState()
|
|
29
|
+
if not state.has_any_auth():
|
|
30
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
31
|
+
|
|
32
|
+
file_path = Path(file)
|
|
33
|
+
if not file_path.is_file():
|
|
34
|
+
validation_error(f"File not found: {file}", json_output=json_output or state.should_json())
|
|
35
|
+
|
|
36
|
+
import mimetypes
|
|
37
|
+
mime_type, _ = mimetypes.guess_type(str(file_path))
|
|
38
|
+
if not mime_type:
|
|
39
|
+
mime_type = "application/octet-stream"
|
|
40
|
+
|
|
41
|
+
file_data = file_path.read_bytes()
|
|
42
|
+
|
|
43
|
+
client = CommuneClient.from_state(state)
|
|
44
|
+
try:
|
|
45
|
+
r = client.post(
|
|
46
|
+
"/v1/attachments/upload",
|
|
47
|
+
data=file_data,
|
|
48
|
+
extra_headers={
|
|
49
|
+
"Content-Type": mime_type,
|
|
50
|
+
"X-Filename": file_path.name,
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
except Exception as exc:
|
|
54
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
55
|
+
|
|
56
|
+
if not r.is_success:
|
|
57
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
58
|
+
|
|
59
|
+
data = r.json()
|
|
60
|
+
if json_output or state.should_json():
|
|
61
|
+
print_json(data)
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
att_id = data.get("id") or data.get("attachmentId", "")
|
|
65
|
+
print_success(f"Uploaded [bold]{file_path.name}[/bold]. Attachment ID: [cyan]{att_id}[/cyan]")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.command("get")
|
|
69
|
+
def attachments_get(
|
|
70
|
+
ctx: typer.Context,
|
|
71
|
+
attachment_id: str = typer.Argument(..., help="Attachment ID."),
|
|
72
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Get attachment metadata. GET /v1/attachments/{attachmentId}."""
|
|
75
|
+
state: AppState = ctx.obj or AppState()
|
|
76
|
+
if not state.has_any_auth():
|
|
77
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
78
|
+
|
|
79
|
+
client = CommuneClient.from_state(state)
|
|
80
|
+
try:
|
|
81
|
+
r = client.get(f"/v1/attachments/{attachment_id}")
|
|
82
|
+
except Exception as exc:
|
|
83
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
84
|
+
|
|
85
|
+
if not r.is_success:
|
|
86
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
87
|
+
|
|
88
|
+
print_record(r.json(), json_output=json_output or state.should_json(), title="Attachment")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@app.command("url")
|
|
92
|
+
def attachments_url(
|
|
93
|
+
ctx: typer.Context,
|
|
94
|
+
attachment_id: str = typer.Argument(..., help="Attachment ID."),
|
|
95
|
+
expires_in: Optional[int] = typer.Option(
|
|
96
|
+
None,
|
|
97
|
+
"--expires-in",
|
|
98
|
+
help="URL expiry in seconds. Default: 3600.",
|
|
99
|
+
),
|
|
100
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Get a presigned download URL for an attachment. GET /v1/attachments/{attachmentId}/url."""
|
|
103
|
+
state: AppState = ctx.obj or AppState()
|
|
104
|
+
if not state.has_any_auth():
|
|
105
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
106
|
+
|
|
107
|
+
client = CommuneClient.from_state(state)
|
|
108
|
+
params: dict = {}
|
|
109
|
+
if expires_in is not None:
|
|
110
|
+
params["expiresIn"] = expires_in
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
r = client.get(f"/v1/attachments/{attachment_id}/url", params=params or None)
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
116
|
+
|
|
117
|
+
if not r.is_success:
|
|
118
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
119
|
+
|
|
120
|
+
data = r.json()
|
|
121
|
+
if json_output or state.should_json():
|
|
122
|
+
print_json(data)
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
url = data.get("url", "")
|
|
126
|
+
from ..output import print_value
|
|
127
|
+
print_value(url, json_output=False, key="url")
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""commune config — manage ~/.commune/config.toml."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from ..config import KNOWN_KEYS, config_path, delete_value, get_value, load_config, mask, set_value
|
|
10
|
+
from ..output import print_kv, print_success, print_value, print_warning
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Manage CLI configuration.", no_args_is_help=True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command("set")
|
|
16
|
+
def config_set(
|
|
17
|
+
key: str = typer.Argument(..., help=f"Config key. Known keys: {', '.join(KNOWN_KEYS)}."),
|
|
18
|
+
value: str = typer.Argument(..., help="Value to store."),
|
|
19
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Set a config key and persist to ~/.commune/config.toml."""
|
|
22
|
+
if key not in KNOWN_KEYS:
|
|
23
|
+
known = ", ".join(KNOWN_KEYS)
|
|
24
|
+
print_warning(f"Unknown key '{key}'. Known keys: {known}")
|
|
25
|
+
set_value(key, value)
|
|
26
|
+
display = mask(value) if key == "api_key" else value
|
|
27
|
+
print_success(f"Set {key} = {display} ({config_path()})")
|
|
28
|
+
if json_output:
|
|
29
|
+
from ..output import print_json
|
|
30
|
+
print_json({"key": key, "set": True})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.command("get")
|
|
34
|
+
def config_get(
|
|
35
|
+
key: str = typer.Argument(..., help="Config key to retrieve."),
|
|
36
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Get a single config value."""
|
|
39
|
+
value = get_value(key)
|
|
40
|
+
if value is None:
|
|
41
|
+
print_warning(f"Key '{key}' is not set in config.")
|
|
42
|
+
raise typer.Exit(1)
|
|
43
|
+
# Mask api_key in terminal display
|
|
44
|
+
display = mask(value) if key == "api_key" else value
|
|
45
|
+
print_value(display, json_output=json_output, key=key)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.command("show")
|
|
49
|
+
def config_show(
|
|
50
|
+
reveal: bool = typer.Option(False, "--reveal", help="Show full api_key (not masked)."),
|
|
51
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Print all config values. Masks api_key unless --reveal is passed."""
|
|
54
|
+
cfg = load_config()
|
|
55
|
+
if not cfg:
|
|
56
|
+
print_warning(f"No config file found at {config_path()}")
|
|
57
|
+
raise typer.Exit(0)
|
|
58
|
+
|
|
59
|
+
pairs: dict[str, str] = {}
|
|
60
|
+
for k, v in cfg.items():
|
|
61
|
+
if k == "api_key" and not reveal:
|
|
62
|
+
pairs[k] = mask(v)
|
|
63
|
+
else:
|
|
64
|
+
pairs[k] = str(v)
|
|
65
|
+
|
|
66
|
+
print_kv(pairs, json_output=json_output, title=f"Config {config_path()}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command("unset")
|
|
70
|
+
def config_unset(
|
|
71
|
+
key: str = typer.Argument(..., help="Config key to remove."),
|
|
72
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Remove a key from config."""
|
|
75
|
+
removed = delete_value(key)
|
|
76
|
+
if removed:
|
|
77
|
+
print_success(f"Removed '{key}' from config.")
|
|
78
|
+
else:
|
|
79
|
+
print_warning(f"Key '{key}' was not set.")
|
|
80
|
+
if json_output:
|
|
81
|
+
from ..output import print_json
|
|
82
|
+
print_json({"key": key, "removed": removed})
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.command("path")
|
|
86
|
+
def config_path_cmd() -> None:
|
|
87
|
+
"""Print the path to the config file."""
|
|
88
|
+
import sys
|
|
89
|
+
sys.stdout.write(str(config_path()) + "\n")
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""commune data — data deletion requests (GDPR / destructive).
|
|
2
|
+
|
|
3
|
+
These commands are destructive and irreversible. They require explicit confirmation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from ..client import CommuneClient
|
|
13
|
+
from ..errors import api_error, auth_required_error, network_error
|
|
14
|
+
from ..output import print_json, print_record, print_success, print_warning, print_status
|
|
15
|
+
from ..state import AppState
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
help="Data deletion requests. Destructive — use with care.",
|
|
19
|
+
no_args_is_help=True,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command("delete-request")
|
|
24
|
+
def data_delete_request(
|
|
25
|
+
ctx: typer.Context,
|
|
26
|
+
email: Optional[str] = typer.Option(None, "--email", help="Email address to delete data for."),
|
|
27
|
+
inbox_id: Optional[str] = typer.Option(None, "--inbox-id", help="Inbox ID scope for deletion."),
|
|
28
|
+
domain_id: Optional[str] = typer.Option(None, "--domain-id", help="Domain ID scope for deletion."),
|
|
29
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Initiate a data deletion request. POST /v1/data/deletion-request.
|
|
32
|
+
|
|
33
|
+
Returns a preview of what will be deleted and a confirmation token.
|
|
34
|
+
Use `commune data delete-confirm <id>` to execute.
|
|
35
|
+
"""
|
|
36
|
+
state: AppState = ctx.obj or AppState()
|
|
37
|
+
if not state.has_any_auth():
|
|
38
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
39
|
+
|
|
40
|
+
body: dict = {}
|
|
41
|
+
if email:
|
|
42
|
+
body["email"] = email
|
|
43
|
+
if inbox_id:
|
|
44
|
+
body["inboxId"] = inbox_id
|
|
45
|
+
if domain_id:
|
|
46
|
+
body["domainId"] = domain_id
|
|
47
|
+
|
|
48
|
+
client = CommuneClient.from_state(state)
|
|
49
|
+
try:
|
|
50
|
+
r = client.post("/v1/data/deletion-request", json=body)
|
|
51
|
+
except Exception as exc:
|
|
52
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
53
|
+
|
|
54
|
+
if not r.is_success:
|
|
55
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
56
|
+
|
|
57
|
+
data = r.json()
|
|
58
|
+
if json_output or state.should_json():
|
|
59
|
+
print_json(data)
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
req_id = data.get("id", "")
|
|
63
|
+
print_warning(f"Deletion request created. ID: [bold]{req_id}[/bold]")
|
|
64
|
+
print_status("Review the preview above, then confirm with:")
|
|
65
|
+
print_status(f" commune data delete-confirm {req_id}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.command("delete-confirm")
|
|
69
|
+
def data_delete_confirm(
|
|
70
|
+
ctx: typer.Context,
|
|
71
|
+
request_id: str = typer.Argument(..., help="Deletion request ID from `commune data delete-request`."),
|
|
72
|
+
confirm: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt. REQUIRED for non-interactive use."),
|
|
73
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Confirm and execute a data deletion request. POST /v1/data/deletion-request/{id}/confirm.
|
|
76
|
+
|
|
77
|
+
This action is IRREVERSIBLE. Data is permanently deleted.
|
|
78
|
+
"""
|
|
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
|
+
if not confirm:
|
|
84
|
+
if state.is_tty():
|
|
85
|
+
print_warning("[bold red]WARNING:[/bold red] This action permanently deletes data and cannot be undone.")
|
|
86
|
+
typer.confirm(f"Confirm deletion of request {request_id}?", abort=True)
|
|
87
|
+
else:
|
|
88
|
+
# Non-TTY without --yes flag: require explicit confirmation
|
|
89
|
+
from ..errors import validation_error
|
|
90
|
+
validation_error(
|
|
91
|
+
"Deletion requires --yes flag in non-interactive mode.",
|
|
92
|
+
json_output=json_output or state.should_json(),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
client = CommuneClient.from_state(state)
|
|
96
|
+
try:
|
|
97
|
+
r = client.post(f"/v1/data/deletion-request/{request_id}/confirm")
|
|
98
|
+
except Exception as exc:
|
|
99
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
100
|
+
|
|
101
|
+
if not r.is_success:
|
|
102
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
103
|
+
|
|
104
|
+
if json_output or state.should_json():
|
|
105
|
+
print_json(r.json())
|
|
106
|
+
return
|
|
107
|
+
print_success(f"Deletion confirmed. Request [bold]{request_id}[/bold] is processing.")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@app.command("delete-status")
|
|
111
|
+
def data_delete_status(
|
|
112
|
+
ctx: typer.Context,
|
|
113
|
+
request_id: str = typer.Argument(..., help="Deletion request ID."),
|
|
114
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Get the status of a data deletion request. GET /v1/data/deletion-request/{id}."""
|
|
117
|
+
state: AppState = ctx.obj or AppState()
|
|
118
|
+
if not state.has_any_auth():
|
|
119
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
120
|
+
|
|
121
|
+
client = CommuneClient.from_state(state)
|
|
122
|
+
try:
|
|
123
|
+
r = client.get(f"/v1/data/deletion-request/{request_id}")
|
|
124
|
+
except Exception as exc:
|
|
125
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
126
|
+
|
|
127
|
+
if not r.is_success:
|
|
128
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
129
|
+
|
|
130
|
+
print_record(r.json(), json_output=json_output or state.should_json(), title=f"Deletion Request {request_id}")
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""commune delivery — delivery metrics, events, and suppressions."""
|
|
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="Delivery metrics, events, and suppressions.", no_args_is_help=True)
|
|
15
|
+
|
|
16
|
+
PERIOD_HELP = "Time period: 1d, 7d, 30d (default: 7d)."
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command("metrics")
|
|
20
|
+
def delivery_metrics(
|
|
21
|
+
ctx: typer.Context,
|
|
22
|
+
domain_id: Optional[str] = typer.Option(None, "--domain-id", help="Filter by domain ID."),
|
|
23
|
+
inbox_id: Optional[str] = typer.Option(None, "--inbox-id", help="Filter by inbox ID."),
|
|
24
|
+
period: Optional[str] = typer.Option(None, "--period", help=PERIOD_HELP),
|
|
25
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Get delivery metrics (sent, delivered, bounced, spam). GET /v1/delivery/metrics."""
|
|
28
|
+
state: AppState = ctx.obj or AppState()
|
|
29
|
+
if not state.has_any_auth():
|
|
30
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
31
|
+
|
|
32
|
+
client = CommuneClient.from_state(state)
|
|
33
|
+
try:
|
|
34
|
+
r = client.get("/v1/delivery/metrics", params={
|
|
35
|
+
"domainId": domain_id,
|
|
36
|
+
"inboxId": inbox_id,
|
|
37
|
+
"period": period,
|
|
38
|
+
})
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
41
|
+
|
|
42
|
+
if not r.is_success:
|
|
43
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
44
|
+
|
|
45
|
+
print_record(r.json(), json_output=json_output or state.should_json(), title="Delivery Metrics")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.command("events")
|
|
49
|
+
def delivery_events(
|
|
50
|
+
ctx: typer.Context,
|
|
51
|
+
domain_id: Optional[str] = typer.Option(None, "--domain-id", help="Filter by domain ID."),
|
|
52
|
+
inbox_id: Optional[str] = typer.Option(None, "--inbox-id", help="Filter by inbox ID."),
|
|
53
|
+
limit: Optional[int] = typer.Option(None, "--limit", help="Maximum results to return."),
|
|
54
|
+
cursor: Optional[str] = typer.Option(None, "--cursor", help="Pagination cursor."),
|
|
55
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Get delivery events (sent, bounce, complaint, open, click). GET /v1/delivery/events."""
|
|
58
|
+
state: AppState = ctx.obj or AppState()
|
|
59
|
+
if not state.has_any_auth():
|
|
60
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
61
|
+
|
|
62
|
+
client = CommuneClient.from_state(state)
|
|
63
|
+
try:
|
|
64
|
+
r = client.get("/v1/delivery/events", params={
|
|
65
|
+
"domainId": domain_id,
|
|
66
|
+
"inboxId": inbox_id,
|
|
67
|
+
"limit": limit,
|
|
68
|
+
"cursor": cursor,
|
|
69
|
+
})
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
72
|
+
|
|
73
|
+
if not r.is_success:
|
|
74
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
75
|
+
|
|
76
|
+
print_list(
|
|
77
|
+
r.json(),
|
|
78
|
+
json_output=json_output or state.should_json(),
|
|
79
|
+
title="Delivery Events",
|
|
80
|
+
columns=[
|
|
81
|
+
("Type", "type"),
|
|
82
|
+
("Email", "email"),
|
|
83
|
+
("Message ID", "messageId"),
|
|
84
|
+
("Timestamp", "timestamp"),
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command("suppressions")
|
|
90
|
+
def delivery_suppressions(
|
|
91
|
+
ctx: typer.Context,
|
|
92
|
+
domain_id: Optional[str] = typer.Option(None, "--domain-id", help="Filter by domain ID."),
|
|
93
|
+
inbox_id: Optional[str] = typer.Option(None, "--inbox-id", help="Filter by inbox ID."),
|
|
94
|
+
limit: Optional[int] = typer.Option(None, "--limit", help="Maximum results."),
|
|
95
|
+
cursor: Optional[str] = typer.Option(None, "--cursor", help="Pagination cursor."),
|
|
96
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
97
|
+
) -> None:
|
|
98
|
+
"""List suppressed email addresses (bounces + spam complaints). GET /v1/delivery/suppressions."""
|
|
99
|
+
state: AppState = ctx.obj or AppState()
|
|
100
|
+
if not state.has_any_auth():
|
|
101
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
102
|
+
|
|
103
|
+
client = CommuneClient.from_state(state)
|
|
104
|
+
try:
|
|
105
|
+
r = client.get("/v1/delivery/suppressions", params={
|
|
106
|
+
"domainId": domain_id,
|
|
107
|
+
"inboxId": inbox_id,
|
|
108
|
+
"limit": limit,
|
|
109
|
+
"cursor": cursor,
|
|
110
|
+
})
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
113
|
+
|
|
114
|
+
if not r.is_success:
|
|
115
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
116
|
+
|
|
117
|
+
print_list(
|
|
118
|
+
r.json(),
|
|
119
|
+
json_output=json_output or state.should_json(),
|
|
120
|
+
title="Suppressed Addresses",
|
|
121
|
+
columns=[
|
|
122
|
+
("Email", "email"),
|
|
123
|
+
("Reason", "reason"),
|
|
124
|
+
("Suppressed At", "suppressedAt"),
|
|
125
|
+
],
|
|
126
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""commune dmarc — DMARC reporting."""
|
|
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, validation_error
|
|
11
|
+
from ..output import print_list, print_record
|
|
12
|
+
from ..state import AppState
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="DMARC reports and summary.", no_args_is_help=True)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("reports")
|
|
18
|
+
def dmarc_reports(
|
|
19
|
+
ctx: typer.Context,
|
|
20
|
+
domain: str = typer.Argument(..., help="Domain name to get DMARC reports for (e.g. example.com)."),
|
|
21
|
+
limit: Optional[int] = typer.Option(None, "--limit", help="Maximum reports to return."),
|
|
22
|
+
cursor: Optional[str] = typer.Option(None, "--cursor", help="Pagination cursor."),
|
|
23
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
24
|
+
) -> None:
|
|
25
|
+
"""List DMARC aggregate reports for a domain. GET /v1/dmarc/reports."""
|
|
26
|
+
state: AppState = ctx.obj or AppState()
|
|
27
|
+
if not state.has_any_auth():
|
|
28
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
29
|
+
|
|
30
|
+
client = CommuneClient.from_state(state)
|
|
31
|
+
try:
|
|
32
|
+
r = client.get("/v1/dmarc/reports", params={
|
|
33
|
+
"domain": domain,
|
|
34
|
+
"limit": limit,
|
|
35
|
+
"cursor": cursor,
|
|
36
|
+
})
|
|
37
|
+
except Exception as exc:
|
|
38
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
39
|
+
|
|
40
|
+
if not r.is_success:
|
|
41
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
42
|
+
|
|
43
|
+
print_list(
|
|
44
|
+
r.json(),
|
|
45
|
+
json_output=json_output or state.should_json(),
|
|
46
|
+
title=f"DMARC Reports: {domain}",
|
|
47
|
+
columns=[
|
|
48
|
+
("ID", "id"),
|
|
49
|
+
("Reporter", "reporterOrg"),
|
|
50
|
+
("Start", "dateRangeBegin"),
|
|
51
|
+
("End", "dateRangeEnd"),
|
|
52
|
+
("Records", "recordCount"),
|
|
53
|
+
],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command("summary")
|
|
58
|
+
def dmarc_summary(
|
|
59
|
+
ctx: typer.Context,
|
|
60
|
+
domain: str = typer.Argument(..., help="Domain name to summarize (e.g. example.com)."),
|
|
61
|
+
days: Optional[int] = typer.Option(None, "--days", help="Number of days to include (default: 30)."),
|
|
62
|
+
json_output: bool = typer.Option(False, "--json", help="Output JSON."),
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Get a DMARC compliance summary for a domain. GET /v1/dmarc/summary."""
|
|
65
|
+
state: AppState = ctx.obj or AppState()
|
|
66
|
+
if not state.has_any_auth():
|
|
67
|
+
auth_required_error(json_output=json_output or state.should_json())
|
|
68
|
+
|
|
69
|
+
client = CommuneClient.from_state(state)
|
|
70
|
+
try:
|
|
71
|
+
r = client.get("/v1/dmarc/summary", params={
|
|
72
|
+
"domain": domain,
|
|
73
|
+
"days": days,
|
|
74
|
+
})
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
network_error(exc, json_output=json_output or state.should_json())
|
|
77
|
+
|
|
78
|
+
if not r.is_success:
|
|
79
|
+
api_error(r, json_output=json_output or state.should_json())
|
|
80
|
+
|
|
81
|
+
print_record(r.json(), json_output=json_output or state.should_json(), title=f"DMARC Summary: {domain}")
|