ermes-cli 1.0.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.
- ermes_cli/__init__.py +1 -0
- ermes_cli/admin_api.py +285 -0
- ermes_cli/auth0.py +220 -0
- ermes_cli/client.py +100 -0
- ermes_cli/commands/__init__.py +0 -0
- ermes_cli/commands/auth_cmds.py +386 -0
- ermes_cli/commands/campaigns.py +321 -0
- ermes_cli/commands/config_cmd.py +105 -0
- ermes_cli/commands/data_paths.py +103 -0
- ermes_cli/commands/export.py +238 -0
- ermes_cli/commands/ingest.py +101 -0
- ermes_cli/commands/journeys.py +182 -0
- ermes_cli/commands/keys.py +113 -0
- ermes_cli/commands/lists.py +137 -0
- ermes_cli/commands/metrics.py +398 -0
- ermes_cli/commands/projects.py +286 -0
- ermes_cli/commands/providers.py +184 -0
- ermes_cli/commands/subscriptions.py +75 -0
- ermes_cli/commands/tags.py +86 -0
- ermes_cli/commands/templates.py +133 -0
- ermes_cli/commands/users.py +133 -0
- ermes_cli/commands/write_cmds.py +87 -0
- ermes_cli/config.py +393 -0
- ermes_cli/errors.py +34 -0
- ermes_cli/export_utils.py +263 -0
- ermes_cli/keyring_store.py +145 -0
- ermes_cli/main.py +193 -0
- ermes_cli/manifest.py +218 -0
- ermes_cli/mcp_server.py +167 -0
- ermes_cli/output.py +58 -0
- ermes_cli/routes.yaml +163 -0
- ermes_cli/rules.py +54 -0
- ermes_cli/write_mode.py +178 -0
- ermes_cli-1.0.0.dist-info/METADATA +281 -0
- ermes_cli-1.0.0.dist-info/RECORD +37 -0
- ermes_cli-1.0.0.dist-info/WHEEL +4 -0
- ermes_cli-1.0.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import json as json_lib
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import typer
|
|
5
|
+
from ermes_cli.client import ErmesAPIError
|
|
6
|
+
|
|
7
|
+
templates_app = typer.Typer(help="Manage templates", no_args_is_help=True)
|
|
8
|
+
COLUMNS = ["id", "type", "locale", "campaign_id"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_deps():
|
|
12
|
+
from ermes_cli.main import get_client, state
|
|
13
|
+
from ermes_cli.output import render, render_detail, render_message
|
|
14
|
+
return get_client(), state, render, render_detail, render_message
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@templates_app.command("list")
|
|
18
|
+
def templates_list(template_type: Optional[str] = typer.Option(None, "--type", help="email|text|push|webhook"), limit: int = typer.Option(100, "--limit")):
|
|
19
|
+
"""List templates."""
|
|
20
|
+
client, st, render, _, _ = _get_deps()
|
|
21
|
+
params = {}
|
|
22
|
+
if template_type:
|
|
23
|
+
params["filter[type]"] = template_type
|
|
24
|
+
try:
|
|
25
|
+
data = client.get_paginated("/templates", params=params, limit=limit)
|
|
26
|
+
render(data, columns=COLUMNS, use_json=st.use_json)
|
|
27
|
+
except ErmesAPIError as e:
|
|
28
|
+
typer.echo(str(e), err=True)
|
|
29
|
+
raise typer.Exit(code=1)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@templates_app.command("get")
|
|
33
|
+
def templates_get(template_id: int = typer.Argument(...)):
|
|
34
|
+
"""Get template details."""
|
|
35
|
+
client, st, _, render_detail, _ = _get_deps()
|
|
36
|
+
try:
|
|
37
|
+
data = client.get(f"/templates/{template_id}")
|
|
38
|
+
render_detail(data, use_json=st.use_json)
|
|
39
|
+
except ErmesAPIError as e:
|
|
40
|
+
typer.echo(str(e), err=True)
|
|
41
|
+
raise typer.Exit(code=1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@templates_app.command("create")
|
|
45
|
+
def templates_create(
|
|
46
|
+
template_type: str = typer.Option(..., "--type", help="email|text|push|webhook"),
|
|
47
|
+
campaign_id: int = typer.Option(..., "--campaign", help="Campaign ID to attach to"),
|
|
48
|
+
locale: str = typer.Option(..., "--locale", help="Template locale (e.g. en, de)"),
|
|
49
|
+
data: Optional[str] = typer.Option(None, "--data", help="Template data as JSON (from, subject, html, etc.)"),
|
|
50
|
+
):
|
|
51
|
+
"""Create a template for a campaign."""
|
|
52
|
+
client, st, _, render_detail, _ = _get_deps()
|
|
53
|
+
body: dict = {
|
|
54
|
+
"type": template_type,
|
|
55
|
+
"campaign_id": campaign_id,
|
|
56
|
+
"locale": locale,
|
|
57
|
+
}
|
|
58
|
+
if data:
|
|
59
|
+
try:
|
|
60
|
+
body["data"] = json_lib.loads(data)
|
|
61
|
+
except json_lib.JSONDecodeError as e:
|
|
62
|
+
typer.echo(f"Invalid JSON for --data: {e}", err=True)
|
|
63
|
+
raise typer.Exit(code=1)
|
|
64
|
+
try:
|
|
65
|
+
result = client.post("/templates", json=body)
|
|
66
|
+
render_detail(result, use_json=st.use_json)
|
|
67
|
+
except ErmesAPIError as e:
|
|
68
|
+
typer.echo(str(e), err=True)
|
|
69
|
+
raise typer.Exit(code=1)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@templates_app.command("update")
|
|
73
|
+
def templates_update(
|
|
74
|
+
template_id: int = typer.Argument(...),
|
|
75
|
+
template_type: str = typer.Option(..., "--type", help="email|text|push|webhook"),
|
|
76
|
+
data: str = typer.Option(..., "--data", help="Template data as JSON (from, subject, html, etc.)"),
|
|
77
|
+
):
|
|
78
|
+
"""Update a template's content."""
|
|
79
|
+
client, st, _, render_detail, _ = _get_deps()
|
|
80
|
+
try:
|
|
81
|
+
body_data = json_lib.loads(data)
|
|
82
|
+
except json_lib.JSONDecodeError as e:
|
|
83
|
+
typer.echo(f"Invalid JSON for --data: {e}", err=True)
|
|
84
|
+
raise typer.Exit(code=1)
|
|
85
|
+
try:
|
|
86
|
+
result = client.patch(f"/templates/{template_id}", json={"type": template_type, "data": body_data})
|
|
87
|
+
render_detail(result, use_json=st.use_json)
|
|
88
|
+
except ErmesAPIError as e:
|
|
89
|
+
typer.echo(str(e), err=True)
|
|
90
|
+
raise typer.Exit(code=1)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@templates_app.command("delete")
|
|
94
|
+
def templates_delete(template_id: int = typer.Argument(...)):
|
|
95
|
+
"""Delete a template."""
|
|
96
|
+
client, st, _, _, render_message = _get_deps()
|
|
97
|
+
try:
|
|
98
|
+
client.delete(f"/templates/{template_id}")
|
|
99
|
+
render_message(f"Template {template_id} deleted.", use_json=st.use_json)
|
|
100
|
+
except ErmesAPIError as e:
|
|
101
|
+
typer.echo(str(e), err=True)
|
|
102
|
+
raise typer.Exit(code=1)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@templates_app.command("preview")
|
|
106
|
+
def templates_preview(template_id: int = typer.Argument(...), data: Optional[str] = typer.Option(None, "--data", help="Preview data as JSON")):
|
|
107
|
+
"""Preview a template."""
|
|
108
|
+
client, st, _, render_detail, _ = _get_deps()
|
|
109
|
+
body = {}
|
|
110
|
+
if data:
|
|
111
|
+
try:
|
|
112
|
+
body = json_lib.loads(data)
|
|
113
|
+
except json_lib.JSONDecodeError as e:
|
|
114
|
+
typer.echo(f"Invalid JSON: {e}", err=True)
|
|
115
|
+
raise typer.Exit(code=1)
|
|
116
|
+
try:
|
|
117
|
+
result = client.post(f"/templates/{template_id}/preview", json=body)
|
|
118
|
+
render_detail(result, use_json=st.use_json)
|
|
119
|
+
except ErmesAPIError as e:
|
|
120
|
+
typer.echo(str(e), err=True)
|
|
121
|
+
raise typer.Exit(code=1)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@templates_app.command("proof")
|
|
125
|
+
def templates_proof(template_id: int = typer.Argument(...), to: str = typer.Option(..., "--to", help="Recipient email")):
|
|
126
|
+
"""Send a test/proof of a template."""
|
|
127
|
+
client, st, _, _, render_message = _get_deps()
|
|
128
|
+
try:
|
|
129
|
+
client.post(f"/templates/{template_id}/proof", json={"to": to})
|
|
130
|
+
render_message(f"Proof sent to {to}.", use_json=st.use_json)
|
|
131
|
+
except ErmesAPIError as e:
|
|
132
|
+
typer.echo(str(e), err=True)
|
|
133
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import json as json_lib
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import typer
|
|
5
|
+
from ermes_cli.client import ErmesAPIError
|
|
6
|
+
|
|
7
|
+
users_app = typer.Typer(help="Manage users", no_args_is_help=True)
|
|
8
|
+
COLUMNS = ["id", "external_id", "email", "phone", "timezone", "locale"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_deps():
|
|
12
|
+
from ermes_cli.main import get_client, state
|
|
13
|
+
from ermes_cli.output import render, render_detail, render_message
|
|
14
|
+
return get_client(), state, render, render_detail, render_message
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _normalize_collection(data):
|
|
18
|
+
if isinstance(data, dict) and "results" in data:
|
|
19
|
+
data = data["results"]
|
|
20
|
+
elif (
|
|
21
|
+
isinstance(data, list)
|
|
22
|
+
and len(data) == 1
|
|
23
|
+
and isinstance(data[0], dict)
|
|
24
|
+
and "results" in data[0]
|
|
25
|
+
):
|
|
26
|
+
data = data[0]["results"]
|
|
27
|
+
if not isinstance(data, list):
|
|
28
|
+
return [data] if data else []
|
|
29
|
+
return data
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@users_app.command("list")
|
|
33
|
+
def users_list(q: Optional[str] = typer.Option(None, "--q", help="Search query"), limit: int = typer.Option(100, "--limit")):
|
|
34
|
+
"""List users."""
|
|
35
|
+
client, st, render, _, _ = _get_deps()
|
|
36
|
+
params = {}
|
|
37
|
+
if q:
|
|
38
|
+
params["q"] = q
|
|
39
|
+
try:
|
|
40
|
+
data = client.get_paginated("/users", params=params, limit=limit)
|
|
41
|
+
render(data, columns=COLUMNS, use_json=st.use_json)
|
|
42
|
+
except ErmesAPIError as e:
|
|
43
|
+
typer.echo(str(e), err=True)
|
|
44
|
+
raise typer.Exit(code=1)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@users_app.command("get")
|
|
48
|
+
def users_get(user_id: str = typer.Argument(..., help="User external_id (hash string)")):
|
|
49
|
+
"""Get user details."""
|
|
50
|
+
client, st, _, render_detail, _ = _get_deps()
|
|
51
|
+
try:
|
|
52
|
+
data = client.get(f"/users/{user_id}")
|
|
53
|
+
render_detail(data, use_json=st.use_json)
|
|
54
|
+
except ErmesAPIError as e:
|
|
55
|
+
typer.echo(str(e), err=True)
|
|
56
|
+
raise typer.Exit(code=1)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@users_app.command("delete")
|
|
60
|
+
def users_delete(user_id: str = typer.Argument(..., help="User external_id")):
|
|
61
|
+
"""Delete a user."""
|
|
62
|
+
client, st, _, _, render_message = _get_deps()
|
|
63
|
+
try:
|
|
64
|
+
client.delete(f"/users/{user_id}")
|
|
65
|
+
render_message(f"User {user_id} deleted.", use_json=st.use_json)
|
|
66
|
+
except ErmesAPIError as e:
|
|
67
|
+
typer.echo(str(e), err=True)
|
|
68
|
+
raise typer.Exit(code=1)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@users_app.command("events")
|
|
72
|
+
def users_events(user_id: str = typer.Argument(..., help="User external_id"), limit: int = typer.Option(100, "--limit")):
|
|
73
|
+
"""List user events."""
|
|
74
|
+
client, st, render, _, _ = _get_deps()
|
|
75
|
+
try:
|
|
76
|
+
data = client.get_paginated(f"/users/{user_id}/events", limit=limit)
|
|
77
|
+
render(data, columns=["id", "name", "created_at"], use_json=st.use_json)
|
|
78
|
+
except ErmesAPIError as e:
|
|
79
|
+
typer.echo(str(e), err=True)
|
|
80
|
+
raise typer.Exit(code=1)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@users_app.command("lists")
|
|
84
|
+
def users_lists(user_id: str = typer.Argument(..., help="User external_id")):
|
|
85
|
+
"""List user's list memberships."""
|
|
86
|
+
client, st, render, _, _ = _get_deps()
|
|
87
|
+
try:
|
|
88
|
+
data = _normalize_collection(client.get(f"/users/{user_id}/lists"))
|
|
89
|
+
render(data, columns=["id", "name", "type", "state"], use_json=st.use_json)
|
|
90
|
+
except ErmesAPIError as e:
|
|
91
|
+
typer.echo(str(e), err=True)
|
|
92
|
+
raise typer.Exit(code=1)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@users_app.command("subscriptions")
|
|
96
|
+
def users_subscriptions(user_id: str = typer.Argument(..., help="User external_id")):
|
|
97
|
+
"""List user's subscriptions."""
|
|
98
|
+
client, st, render, _, _ = _get_deps()
|
|
99
|
+
try:
|
|
100
|
+
data = _normalize_collection(client.get(f"/users/{user_id}/subscriptions"))
|
|
101
|
+
render(data, columns=["subscription_id", "name", "channel", "state"], use_json=st.use_json)
|
|
102
|
+
except ErmesAPIError as e:
|
|
103
|
+
typer.echo(str(e), err=True)
|
|
104
|
+
raise typer.Exit(code=1)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@users_app.command("subscriptions-update")
|
|
108
|
+
def users_subscriptions_update(user_id: str = typer.Argument(..., help="User external_id"), data: str = typer.Argument(..., help="Subscription updates as JSON array")):
|
|
109
|
+
"""Update user's subscriptions."""
|
|
110
|
+
client, st, _, _, render_message = _get_deps()
|
|
111
|
+
try:
|
|
112
|
+
body = json_lib.loads(data)
|
|
113
|
+
except json_lib.JSONDecodeError as e:
|
|
114
|
+
typer.echo(f"Invalid JSON: {e}", err=True)
|
|
115
|
+
raise typer.Exit(code=1)
|
|
116
|
+
try:
|
|
117
|
+
client.patch(f"/users/{user_id}/subscriptions", json=body)
|
|
118
|
+
render_message(f"Subscriptions updated for user {user_id}.", use_json=st.use_json)
|
|
119
|
+
except ErmesAPIError as e:
|
|
120
|
+
typer.echo(str(e), err=True)
|
|
121
|
+
raise typer.Exit(code=1)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@users_app.command("journeys")
|
|
125
|
+
def users_journeys(user_id: str = typer.Argument(..., help="User external_id")):
|
|
126
|
+
"""List user's journey participations."""
|
|
127
|
+
client, st, render, _, _ = _get_deps()
|
|
128
|
+
try:
|
|
129
|
+
data = _normalize_collection(client.get(f"/users/{user_id}/journeys"))
|
|
130
|
+
render(data, columns=["journey_id", "type", "step_id", "delay_until"], use_json=st.use_json)
|
|
131
|
+
except ErmesAPIError as e:
|
|
132
|
+
typer.echo(str(e), err=True)
|
|
133
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""`ermes write allow|deny|status` — control the write-mode guard.
|
|
2
|
+
|
|
3
|
+
The guard blocks every POST/PATCH/PUT/DELETE issued through `client.py`
|
|
4
|
+
unless the operator has explicitly opened a write window. This is the
|
|
5
|
+
"are you sure?" speed bump for the CLI surface. See `write_mode.py` for
|
|
6
|
+
the underlying state machine.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from ermes_cli import write_mode
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
write_app = typer.Typer(
|
|
20
|
+
help="Control the write-mode guard (blocks mutations by default)",
|
|
21
|
+
no_args_is_help=True,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _print_status(s: dict) -> None:
|
|
26
|
+
if s["allowed"]:
|
|
27
|
+
if s["mode"] == "persistent":
|
|
28
|
+
console.print(
|
|
29
|
+
"[green]Writes ALLOWED[/green] (persistent — until you run "
|
|
30
|
+
"`ermes write deny`)."
|
|
31
|
+
)
|
|
32
|
+
elif s["mode"] == "env":
|
|
33
|
+
console.print(
|
|
34
|
+
"[green]Writes ALLOWED[/green] via ERMES_ALLOW_WRITES env override."
|
|
35
|
+
)
|
|
36
|
+
else:
|
|
37
|
+
console.print(
|
|
38
|
+
f"[green]Writes ALLOWED[/green] until [bold]{s['until']}[/bold] "
|
|
39
|
+
"(UTC). Run `ermes write deny` to close early."
|
|
40
|
+
)
|
|
41
|
+
else:
|
|
42
|
+
msg = "[red]Writes BLOCKED[/red] (default)."
|
|
43
|
+
if s["until"]:
|
|
44
|
+
msg += f" Last window expired at {s['until']}."
|
|
45
|
+
console.print(msg)
|
|
46
|
+
console.print(
|
|
47
|
+
"[dim]Open a write window with:[/dim]\n"
|
|
48
|
+
" ermes write allow # 30 minutes\n"
|
|
49
|
+
" ermes write allow --minutes 5 # custom window\n"
|
|
50
|
+
" ermes write allow --persistent # no expiry\n"
|
|
51
|
+
"[dim]Or set ERMES_ALLOW_WRITES=1 in the environment.[/dim]"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@write_app.command("status")
|
|
56
|
+
def write_status() -> None:
|
|
57
|
+
"""Show the current write-mode state."""
|
|
58
|
+
_print_status(write_mode.status())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@write_app.command("allow")
|
|
62
|
+
def write_allow(
|
|
63
|
+
minutes: int = typer.Option(
|
|
64
|
+
30,
|
|
65
|
+
"--minutes",
|
|
66
|
+
"-m",
|
|
67
|
+
min=1,
|
|
68
|
+
max=24 * 60,
|
|
69
|
+
help="Open a time-limited window (default 30, max 1440 = 24h).",
|
|
70
|
+
),
|
|
71
|
+
persistent: bool = typer.Option(
|
|
72
|
+
False,
|
|
73
|
+
"--persistent",
|
|
74
|
+
"-p",
|
|
75
|
+
help="Open indefinitely. Ignores --minutes.",
|
|
76
|
+
),
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Allow mutating CLI calls (POST / PATCH / PUT / DELETE)."""
|
|
79
|
+
s = write_mode.allow(minutes=minutes, persistent=persistent)
|
|
80
|
+
_print_status(s)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@write_app.command("deny")
|
|
84
|
+
def write_deny() -> None:
|
|
85
|
+
"""Block all mutating CLI calls (default state)."""
|
|
86
|
+
s = write_mode.deny()
|
|
87
|
+
_print_status(s)
|