guzli-cli 0.2.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.
- guzli/__init__.py +3 -0
- guzli/__main__.py +4 -0
- guzli/auth.py +182 -0
- guzli/cli.py +84 -0
- guzli/commands/__init__.py +1 -0
- guzli/commands/admin.py +421 -0
- guzli/commands/auth.py +502 -0
- guzli/commands/campaign_launch.py +188 -0
- guzli/commands/campaigns.py +571 -0
- guzli/commands/common.py +42 -0
- guzli/commands/conversations.py +404 -0
- guzli/commands/integrations.py +48 -0
- guzli/commands/telephony.py +274 -0
- guzli/commands/voice_calls.py +270 -0
- guzli/config.py +137 -0
- guzli/config_store.py +218 -0
- guzli/errors.py +47 -0
- guzli/http_client.py +213 -0
- guzli/mcp/__init__.py +1 -0
- guzli/mcp/server.py +320 -0
- guzli/models.py +173 -0
- guzli/oauth_callback.py +126 -0
- guzli/output.py +79 -0
- guzli/runtime.py +159 -0
- guzli/services/__init__.py +29 -0
- guzli/services/api_keys.py +105 -0
- guzli/services/calls.py +48 -0
- guzli/services/campaigns.py +193 -0
- guzli/services/cli_auth.py +103 -0
- guzli/services/conversations.py +112 -0
- guzli/services/integrations.py +18 -0
- guzli/services/replies.py +21 -0
- guzli/services/telephony.py +80 -0
- guzli/services/voice_profiles.py +56 -0
- guzli/services/webhooks.py +169 -0
- guzli/types.py +15 -0
- guzli/validators.py +83 -0
- guzli/workflows/__init__.py +13 -0
- guzli/workflows/campaign_launch.py +501 -0
- guzli_cli-0.2.0.data/data/share/guzli/CHANGELOG.md +16 -0
- guzli_cli-0.2.0.data/data/share/guzli/PROVENANCE.md +23 -0
- guzli_cli-0.2.0.data/data/share/guzli/SECURITY.md +18 -0
- guzli_cli-0.2.0.data/data/share/guzli/SECURITY_AUDIT.md +47 -0
- guzli_cli-0.2.0.data/data/share/guzli/SKILL.md +101 -0
- guzli_cli-0.2.0.data/data/share/guzli/examples/compliance-advisory-us.json +15 -0
- guzli_cli-0.2.0.data/data/share/guzli/examples/pizza-order-extraction.json +65 -0
- guzli_cli-0.2.0.data/data/share/guzli/references/api.md +245 -0
- guzli_cli-0.2.0.dist-info/METADATA +341 -0
- guzli_cli-0.2.0.dist-info/RECORD +53 -0
- guzli_cli-0.2.0.dist-info/WHEEL +5 -0
- guzli_cli-0.2.0.dist-info/entry_points.txt +3 -0
- guzli_cli-0.2.0.dist-info/licenses/LICENSE +201 -0
- guzli_cli-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Dict, Mapping
|
|
6
|
+
|
|
7
|
+
from guzli.commands.campaign_launch import add_campaign_launch_parser
|
|
8
|
+
from guzli.commands.common import parse_bool_flag as _parse_bool_flag
|
|
9
|
+
from guzli.errors import ConfigError, ValidationError
|
|
10
|
+
from guzli.runtime import RuntimeContext
|
|
11
|
+
from guzli.validators import parse_json, parse_json_or_file, require_non_empty
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def handle_campaign_list(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
15
|
+
params: Dict[str, object] = {}
|
|
16
|
+
if args.status:
|
|
17
|
+
params["status"] = args.status
|
|
18
|
+
if args.search:
|
|
19
|
+
params["search"] = args.search
|
|
20
|
+
if args.limit:
|
|
21
|
+
params["limit"] = args.limit
|
|
22
|
+
payload = context.campaigns.list_campaigns(params)
|
|
23
|
+
context.renderer.print_json(payload)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def handle_campaign_get(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
27
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
28
|
+
payload = context.campaigns.get_campaign(campaign_id)
|
|
29
|
+
context.renderer.print_json(payload)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def handle_campaign_preview(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
33
|
+
body = parse_json_or_file(args.config_json, args.config_file, "campaign preview")
|
|
34
|
+
if not isinstance(body, Mapping):
|
|
35
|
+
raise ValidationError("campaign preview requires a JSON object")
|
|
36
|
+
context.renderer.print_json(context.campaigns.preview_campaign(dict(body)))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def handle_campaign_create(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
40
|
+
body = parse_json_or_file(args.config_json, args.config_file, "campaign config")
|
|
41
|
+
if not isinstance(body, Mapping):
|
|
42
|
+
raise ValidationError("campaign config must be a JSON object")
|
|
43
|
+
body = dict(body)
|
|
44
|
+
if args.name:
|
|
45
|
+
body["name"] = args.name
|
|
46
|
+
if "name" not in body or not str(body.get("name", "")).strip():
|
|
47
|
+
raise ValidationError('name is required (pass --name or include "name" in the config)')
|
|
48
|
+
if not str(body.get("organization_id", "")).strip():
|
|
49
|
+
if not context.organization_id:
|
|
50
|
+
raise ConfigError(
|
|
51
|
+
"organization_id is required for campaign create. "
|
|
52
|
+
"Set it via `guzli auth login --organization-id …` or include it in the config."
|
|
53
|
+
)
|
|
54
|
+
body["organization_id"] = context.organization_id
|
|
55
|
+
if not str(body.get("agent_id", "")).strip() and context.agent_id:
|
|
56
|
+
body["agent_id"] = context.agent_id
|
|
57
|
+
payload = context.campaigns.create_campaign(body)
|
|
58
|
+
context.renderer.print_json(payload)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def handle_campaign_update(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
62
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
63
|
+
body = parse_json_or_file(args.config_json, args.config_file, "campaign update")
|
|
64
|
+
if not isinstance(body, Mapping):
|
|
65
|
+
raise ValidationError("campaign update must be a JSON object (matches CampaignUpdate)")
|
|
66
|
+
payload = context.campaigns.update_campaign(campaign_id, dict(body))
|
|
67
|
+
context.renderer.print_json(payload)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def handle_campaign_readiness(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
71
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
72
|
+
payload = context.campaigns.get_readiness(campaign_id)
|
|
73
|
+
context.renderer.print_json(payload)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def handle_campaign_activate(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
77
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
78
|
+
payload = context.campaigns.activate_campaign(campaign_id)
|
|
79
|
+
context.renderer.print_json(payload)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def handle_campaign_pause(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
83
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
84
|
+
payload = context.campaigns.pause_campaign(campaign_id)
|
|
85
|
+
context.renderer.print_json(payload)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def handle_campaign_archive(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
89
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
90
|
+
context.renderer.print_json(context.campaigns.archive_campaign(campaign_id))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def handle_campaign_run(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
94
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
95
|
+
body: Dict[str, object] = {}
|
|
96
|
+
metadata = parse_json(args.metadata, "metadata")
|
|
97
|
+
if metadata is not None:
|
|
98
|
+
if not isinstance(metadata, Mapping):
|
|
99
|
+
raise ValidationError("metadata must be a JSON object")
|
|
100
|
+
body["metadata"] = dict(metadata)
|
|
101
|
+
audience = parse_json(args.audience_override, "audience_override")
|
|
102
|
+
if audience is not None:
|
|
103
|
+
if not isinstance(audience, Mapping):
|
|
104
|
+
raise ValidationError("audience_override must be a JSON object")
|
|
105
|
+
body["audience_override"] = dict(audience)
|
|
106
|
+
payload = context.campaigns.trigger_manual_run(campaign_id, body)
|
|
107
|
+
context.renderer.print_json(payload)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def handle_campaign_audience_import(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
111
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
112
|
+
body = parse_json_or_file(args.config_json, args.config_file, "audience import")
|
|
113
|
+
if body is None:
|
|
114
|
+
raise ValidationError("audience import requires --config-json or --config-file")
|
|
115
|
+
# Accept either a bare list of items or a full import request object.
|
|
116
|
+
if isinstance(body, list):
|
|
117
|
+
request: Dict[str, object] = {"items": list(body)}
|
|
118
|
+
elif isinstance(body, Mapping):
|
|
119
|
+
request = dict(body)
|
|
120
|
+
else:
|
|
121
|
+
raise ValidationError(
|
|
122
|
+
'audience import body must be a JSON array of items, or an object with an "items" array'
|
|
123
|
+
)
|
|
124
|
+
items = request.get("items")
|
|
125
|
+
if not isinstance(items, list) or not items:
|
|
126
|
+
raise ValidationError('audience import requires a non-empty "items" array')
|
|
127
|
+
if args.operation_id:
|
|
128
|
+
request["operation_id"] = args.operation_id
|
|
129
|
+
if not str(request.get("operation_id", "") or "").strip():
|
|
130
|
+
request["operation_id"] = f"cli-import-{uuid.uuid4().hex[:16]}"
|
|
131
|
+
if args.no_update_existing:
|
|
132
|
+
request["update_existing"] = False
|
|
133
|
+
elif "update_existing" not in request:
|
|
134
|
+
request["update_existing"] = True
|
|
135
|
+
payload = context.campaigns.import_audience_members(campaign_id, request)
|
|
136
|
+
context.renderer.print_json(payload)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def handle_campaign_audience_list(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
140
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
141
|
+
params: Dict[str, object] = {}
|
|
142
|
+
if args.lifecycle_status:
|
|
143
|
+
params["lifecycle_status"] = args.lifecycle_status
|
|
144
|
+
if args.phone:
|
|
145
|
+
params["target_phone_e164"] = args.phone
|
|
146
|
+
if args.cursor:
|
|
147
|
+
params["cursor"] = args.cursor
|
|
148
|
+
if args.limit:
|
|
149
|
+
params["limit"] = args.limit
|
|
150
|
+
payload = context.campaigns.list_audience_members(campaign_id, params)
|
|
151
|
+
context.renderer.print_json(payload)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def handle_campaign_audience_queue_eligible(
|
|
155
|
+
context: RuntimeContext, args: argparse.Namespace
|
|
156
|
+
) -> None:
|
|
157
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
158
|
+
body: Dict[str, object] = {"operation_id": args.operation_id or str(uuid.uuid4())}
|
|
159
|
+
if args.limit:
|
|
160
|
+
body["limit"] = args.limit
|
|
161
|
+
if args.include_line_type_lookup:
|
|
162
|
+
body["include_line_type_lookup"] = True
|
|
163
|
+
payload = context.campaigns.queue_eligible_audience_members(campaign_id, body)
|
|
164
|
+
context.renderer.print_json(payload)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def handle_compliance_get(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
168
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
169
|
+
payload = context.campaigns.get_compliance(campaign_id)
|
|
170
|
+
context.renderer.print_json(payload)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
_COMPLIANCE_DEFAULTS: Dict[str, object] = {
|
|
174
|
+
"policy_region": "US",
|
|
175
|
+
"compliance_mode": "enforced",
|
|
176
|
+
"call_purpose": "support",
|
|
177
|
+
"recipient_type": "consumer",
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _apply_compliance_flags(body: Dict[str, object], args: argparse.Namespace) -> Dict[str, object]:
|
|
182
|
+
if getattr(args, "compliance_mode", None):
|
|
183
|
+
body["compliance_mode"] = args.compliance_mode
|
|
184
|
+
if getattr(args, "ai_notice", None) is not None:
|
|
185
|
+
body["automated_voice_notice_required"] = args.ai_notice
|
|
186
|
+
if getattr(args, "opt_out", None) is not None:
|
|
187
|
+
body["spoken_opt_out_enabled"] = args.opt_out
|
|
188
|
+
if getattr(args, "recording_notice", None) is not None:
|
|
189
|
+
body["recording_notice_required"] = args.recording_notice
|
|
190
|
+
return body
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def handle_compliance_set(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
194
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
195
|
+
base = parse_json_or_file(args.config_json, args.config_file, "compliance settings")
|
|
196
|
+
if base is None:
|
|
197
|
+
body: Dict[str, object] = dict(_COMPLIANCE_DEFAULTS)
|
|
198
|
+
elif isinstance(base, Mapping):
|
|
199
|
+
body = dict(base)
|
|
200
|
+
else:
|
|
201
|
+
raise ValidationError(
|
|
202
|
+
"compliance settings must be a JSON object (CampaignComplianceSettings)"
|
|
203
|
+
)
|
|
204
|
+
_apply_compliance_flags(body, args)
|
|
205
|
+
payload = context.campaigns.save_compliance(campaign_id, body)
|
|
206
|
+
context.renderer.print_json(payload)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def handle_compliance_approve(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
210
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
211
|
+
payload = context.campaigns.approve_compliance(campaign_id)
|
|
212
|
+
context.renderer.print_json(payload)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def handle_compliance_disable(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
216
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
217
|
+
payload = context.campaigns.disable_compliance(campaign_id)
|
|
218
|
+
context.renderer.print_json(payload)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def handle_campaign_runs_list(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
222
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
223
|
+
params: Dict[str, object] = {}
|
|
224
|
+
if args.status:
|
|
225
|
+
params["status"] = args.status
|
|
226
|
+
if args.cursor:
|
|
227
|
+
params["cursor"] = args.cursor
|
|
228
|
+
if args.limit:
|
|
229
|
+
params["limit"] = args.limit
|
|
230
|
+
payload = context.campaigns.list_runs(campaign_id, params)
|
|
231
|
+
context.renderer.print_json(payload)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def handle_campaign_run_get(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
235
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
236
|
+
run_id = require_non_empty(args.run_id, "run_id")
|
|
237
|
+
context.renderer.print_json(context.campaigns.get_run(campaign_id, run_id))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def handle_campaign_run_cancel(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
241
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
242
|
+
run_id = require_non_empty(args.run_id, "run_id")
|
|
243
|
+
context.renderer.print_json(context.campaigns.cancel_run(campaign_id, run_id))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def handle_campaign_results_list(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
247
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
248
|
+
params: Dict[str, object] = {}
|
|
249
|
+
for name in ("status", "campaign_run_id", "schema_name", "limit"):
|
|
250
|
+
value = getattr(args, name, None)
|
|
251
|
+
if value is not None:
|
|
252
|
+
params[name] = value
|
|
253
|
+
context.renderer.print_json(context.campaigns.list_results(campaign_id, params))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def handle_campaign_result_get(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
257
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
258
|
+
result_id = require_non_empty(args.result_id, "result_id")
|
|
259
|
+
context.renderer.print_json(context.campaigns.get_result(campaign_id, result_id))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _compliance_check_body(args: argparse.Namespace, label: str) -> Dict[str, object]:
|
|
263
|
+
body = parse_json_or_file(args.config_json, args.config_file, label)
|
|
264
|
+
if body is None:
|
|
265
|
+
return {}
|
|
266
|
+
if not isinstance(body, Mapping):
|
|
267
|
+
raise ValidationError(f"{label} must be a JSON object")
|
|
268
|
+
return dict(body)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def handle_compliance_preview(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
272
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
273
|
+
body = _compliance_check_body(args, "compliance preview")
|
|
274
|
+
context.renderer.print_json(context.campaigns.preview_compliance(campaign_id, body))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def handle_compliance_preflight(context: RuntimeContext, args: argparse.Namespace) -> None:
|
|
278
|
+
campaign_id = require_non_empty(args.campaign_id, "campaign_id")
|
|
279
|
+
body = _compliance_check_body(args, "compliance preflight")
|
|
280
|
+
context.renderer.print_json(context.campaigns.preflight_compliance(campaign_id, body))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _add_campaign_list(sub: argparse._SubParsersAction) -> None:
|
|
284
|
+
parser = sub.add_parser("list", help="List campaigns")
|
|
285
|
+
parser.add_argument(
|
|
286
|
+
"--status", help="Filter by status (DRAFT, ACTIVE, PAUSED, ARCHIVED, COMPLETED)"
|
|
287
|
+
)
|
|
288
|
+
parser.add_argument("--search", help="Substring search on campaign name")
|
|
289
|
+
parser.add_argument("--limit", type=int)
|
|
290
|
+
parser.set_defaults(handler=handle_campaign_list)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _add_campaign_get(sub: argparse._SubParsersAction) -> None:
|
|
294
|
+
parser = sub.add_parser("get", help="Fetch a campaign definition")
|
|
295
|
+
parser.add_argument("campaign_id")
|
|
296
|
+
parser.set_defaults(handler=handle_campaign_get)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _add_campaign_preview(sub: argparse._SubParsersAction) -> None:
|
|
300
|
+
parser = sub.add_parser("preview", help="Validate and preview a campaign definition")
|
|
301
|
+
parser.add_argument("--config-json", dest="config_json", help="Inline JSON body")
|
|
302
|
+
parser.add_argument("--config-file", dest="config_file", help="Path to JSON body")
|
|
303
|
+
parser.set_defaults(handler=handle_campaign_preview)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _add_campaign_create(sub: argparse._SubParsersAction) -> None:
|
|
307
|
+
parser = sub.add_parser(
|
|
308
|
+
"create",
|
|
309
|
+
help="Create a campaign from JSON (matches CampaignCreate schema)",
|
|
310
|
+
)
|
|
311
|
+
parser.add_argument("--name", help="Convenience override for the campaign name")
|
|
312
|
+
parser.add_argument("--config-json", dest="config_json", help="Inline JSON body")
|
|
313
|
+
parser.add_argument("--config-file", dest="config_file", help="Path to JSON file with body")
|
|
314
|
+
parser.set_defaults(handler=handle_campaign_create)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _add_campaign_update(sub: argparse._SubParsersAction) -> None:
|
|
318
|
+
parser = sub.add_parser("update", help="Patch a campaign (partial CampaignUpdate body)")
|
|
319
|
+
parser.add_argument("campaign_id")
|
|
320
|
+
parser.add_argument("--config-json", dest="config_json", help="Inline JSON partial body")
|
|
321
|
+
parser.add_argument(
|
|
322
|
+
"--config-file", dest="config_file", help="Path to JSON file with partial body"
|
|
323
|
+
)
|
|
324
|
+
parser.set_defaults(handler=handle_campaign_update)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _add_campaign_readiness(sub: argparse._SubParsersAction) -> None:
|
|
328
|
+
parser = sub.add_parser("readiness", help="Check whether a campaign is ready to activate")
|
|
329
|
+
parser.add_argument("campaign_id")
|
|
330
|
+
parser.set_defaults(handler=handle_campaign_readiness)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _add_campaign_activate(sub: argparse._SubParsersAction) -> None:
|
|
334
|
+
parser = sub.add_parser("activate", help="Transition a campaign to ACTIVE")
|
|
335
|
+
parser.add_argument("campaign_id")
|
|
336
|
+
parser.set_defaults(handler=handle_campaign_activate)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _add_campaign_pause(sub: argparse._SubParsersAction) -> None:
|
|
340
|
+
parser = sub.add_parser("pause", help="Pause a running campaign")
|
|
341
|
+
parser.add_argument("campaign_id")
|
|
342
|
+
parser.set_defaults(handler=handle_campaign_pause)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _add_campaign_archive(sub: argparse._SubParsersAction) -> None:
|
|
346
|
+
parser = sub.add_parser("archive", help="Archive a campaign")
|
|
347
|
+
parser.add_argument("campaign_id")
|
|
348
|
+
parser.set_defaults(handler=handle_campaign_archive)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _add_campaign_run(sub: argparse._SubParsersAction) -> None:
|
|
352
|
+
parser = sub.add_parser("run", help="Trigger a manual outbound run for a campaign")
|
|
353
|
+
parser.add_argument("campaign_id")
|
|
354
|
+
parser.add_argument("--metadata", help="Inline JSON object passed as run metadata")
|
|
355
|
+
parser.add_argument(
|
|
356
|
+
"--audience-override",
|
|
357
|
+
dest="audience_override",
|
|
358
|
+
help="Inline JSON CampaignAudienceDefinition to override the campaign audience for this run",
|
|
359
|
+
)
|
|
360
|
+
parser.set_defaults(handler=handle_campaign_run)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _add_campaign_audience_import(sub: argparse._SubParsersAction) -> None:
|
|
364
|
+
parser = sub.add_parser(
|
|
365
|
+
"import",
|
|
366
|
+
help="Import audience members (supports per-target campaign_metadata)",
|
|
367
|
+
)
|
|
368
|
+
parser.add_argument("campaign_id")
|
|
369
|
+
parser.add_argument(
|
|
370
|
+
"--config-json",
|
|
371
|
+
dest="config_json",
|
|
372
|
+
help="Inline JSON: an array of import items, or {operation_id, update_existing, items[]}",
|
|
373
|
+
)
|
|
374
|
+
parser.add_argument(
|
|
375
|
+
"--config-file",
|
|
376
|
+
dest="config_file",
|
|
377
|
+
help="Path to a JSON file with the same shape as --config-json",
|
|
378
|
+
)
|
|
379
|
+
parser.add_argument(
|
|
380
|
+
"--operation-id",
|
|
381
|
+
dest="operation_id",
|
|
382
|
+
help="Idempotency batch id (re-using one reprocesses that batch); generated if omitted",
|
|
383
|
+
)
|
|
384
|
+
parser.add_argument(
|
|
385
|
+
"--no-update-existing",
|
|
386
|
+
dest="no_update_existing",
|
|
387
|
+
action="store_true",
|
|
388
|
+
help="Preserve existing members instead of updating pending ones (default: update)",
|
|
389
|
+
)
|
|
390
|
+
parser.set_defaults(handler=handle_campaign_audience_import)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _add_campaign_audience_list(sub: argparse._SubParsersAction) -> None:
|
|
394
|
+
parser = sub.add_parser("list", help="List a campaign's audience members")
|
|
395
|
+
parser.add_argument("campaign_id")
|
|
396
|
+
parser.add_argument("--lifecycle-status", dest="lifecycle_status")
|
|
397
|
+
parser.add_argument("--phone", help="Filter by target phone (E.164)")
|
|
398
|
+
parser.add_argument("--cursor")
|
|
399
|
+
parser.add_argument("--limit", type=int)
|
|
400
|
+
parser.set_defaults(handler=handle_campaign_audience_list)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _add_campaign_audience_queue_eligible(sub: argparse._SubParsersAction) -> None:
|
|
404
|
+
parser = sub.add_parser(
|
|
405
|
+
"queue-eligible",
|
|
406
|
+
help="Queue eligible imported members for dialing — this is what actually dials members WITH their campaign_metadata (campaign must be active)",
|
|
407
|
+
)
|
|
408
|
+
parser.add_argument("campaign_id")
|
|
409
|
+
parser.add_argument(
|
|
410
|
+
"--limit", type=int, help="Max members to queue this call (1-200, default 50)"
|
|
411
|
+
)
|
|
412
|
+
parser.add_argument(
|
|
413
|
+
"--operation-id", dest="operation_id", help="Idempotency id (generated if omitted)"
|
|
414
|
+
)
|
|
415
|
+
parser.add_argument(
|
|
416
|
+
"--include-line-type-lookup",
|
|
417
|
+
dest="include_line_type_lookup",
|
|
418
|
+
action="store_true",
|
|
419
|
+
help="Run a line-type lookup on each number before queueing",
|
|
420
|
+
)
|
|
421
|
+
parser.set_defaults(handler=handle_campaign_audience_queue_eligible)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _add_compliance_get(sub: argparse._SubParsersAction) -> None:
|
|
425
|
+
parser = sub.add_parser("get", help="Fetch campaign compliance settings")
|
|
426
|
+
parser.add_argument("campaign_id")
|
|
427
|
+
parser.set_defaults(handler=handle_compliance_get)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _add_compliance_set(sub: argparse._SubParsersAction) -> None:
|
|
431
|
+
parser = sub.add_parser(
|
|
432
|
+
"set",
|
|
433
|
+
help="Save compliance settings. Without --config-* a sensible default body is used; shorthand flags override.",
|
|
434
|
+
)
|
|
435
|
+
parser.add_argument("campaign_id")
|
|
436
|
+
parser.add_argument("--config-json", dest="config_json", help="Inline JSON body")
|
|
437
|
+
parser.add_argument("--config-file", dest="config_file", help="Path to JSON file with body")
|
|
438
|
+
parser.add_argument(
|
|
439
|
+
"--compliance-mode",
|
|
440
|
+
dest="compliance_mode",
|
|
441
|
+
choices=["enforced", "advisory"],
|
|
442
|
+
help="Set compliance_mode",
|
|
443
|
+
)
|
|
444
|
+
parser.add_argument(
|
|
445
|
+
"--ai-notice",
|
|
446
|
+
dest="ai_notice",
|
|
447
|
+
type=_parse_bool_flag,
|
|
448
|
+
metavar="on|off",
|
|
449
|
+
help="Toggle automated_voice_notice_required",
|
|
450
|
+
)
|
|
451
|
+
parser.add_argument(
|
|
452
|
+
"--opt-out",
|
|
453
|
+
dest="opt_out",
|
|
454
|
+
type=_parse_bool_flag,
|
|
455
|
+
metavar="on|off",
|
|
456
|
+
help="Toggle spoken_opt_out_enabled",
|
|
457
|
+
)
|
|
458
|
+
parser.add_argument(
|
|
459
|
+
"--recording-notice",
|
|
460
|
+
dest="recording_notice",
|
|
461
|
+
type=_parse_bool_flag,
|
|
462
|
+
metavar="on|off",
|
|
463
|
+
help="Toggle recording_notice_required",
|
|
464
|
+
)
|
|
465
|
+
parser.set_defaults(handler=handle_compliance_set)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _add_compliance_approve(sub: argparse._SubParsersAction) -> None:
|
|
469
|
+
parser = sub.add_parser("approve", help="Approve compliance settings (required before dialing)")
|
|
470
|
+
parser.add_argument("campaign_id")
|
|
471
|
+
parser.set_defaults(handler=handle_compliance_approve)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _add_compliance_disable(sub: argparse._SubParsersAction) -> None:
|
|
475
|
+
parser = sub.add_parser("disable", help="Disable compliance enforcement for a campaign")
|
|
476
|
+
parser.add_argument("campaign_id")
|
|
477
|
+
parser.set_defaults(handler=handle_compliance_disable)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _add_compliance_preview(sub: argparse._SubParsersAction) -> None:
|
|
481
|
+
parser = sub.add_parser("preview", help="Preview compliance decisions without dialing")
|
|
482
|
+
parser.add_argument("campaign_id")
|
|
483
|
+
parser.add_argument("--config-json", dest="config_json", help="Inline preview request")
|
|
484
|
+
parser.add_argument("--config-file", dest="config_file", help="Path to preview request")
|
|
485
|
+
parser.set_defaults(handler=handle_compliance_preview)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _add_compliance_preflight(sub: argparse._SubParsersAction) -> None:
|
|
489
|
+
parser = sub.add_parser("preflight", help="Run the campaign compliance preflight")
|
|
490
|
+
parser.add_argument("campaign_id")
|
|
491
|
+
parser.add_argument("--config-json", dest="config_json", help="Inline preflight request")
|
|
492
|
+
parser.add_argument("--config-file", dest="config_file", help="Path to preflight request")
|
|
493
|
+
parser.set_defaults(handler=handle_compliance_preflight)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _add_campaign_runs_list(sub: argparse._SubParsersAction) -> None:
|
|
497
|
+
parser = sub.add_parser("runs", help="List runs for a campaign")
|
|
498
|
+
parser.add_argument("campaign_id")
|
|
499
|
+
parser.add_argument("--status")
|
|
500
|
+
parser.add_argument("--cursor")
|
|
501
|
+
parser.add_argument("--limit", type=int)
|
|
502
|
+
parser.set_defaults(handler=handle_campaign_runs_list)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _add_campaign_run_get(sub: argparse._SubParsersAction) -> None:
|
|
506
|
+
parser = sub.add_parser("run-get", help="Fetch a campaign run")
|
|
507
|
+
parser.add_argument("campaign_id")
|
|
508
|
+
parser.add_argument("run_id")
|
|
509
|
+
parser.set_defaults(handler=handle_campaign_run_get)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _add_campaign_run_cancel(sub: argparse._SubParsersAction) -> None:
|
|
513
|
+
parser = sub.add_parser("run-cancel", help="Cancel a campaign run")
|
|
514
|
+
parser.add_argument("campaign_id")
|
|
515
|
+
parser.add_argument("run_id")
|
|
516
|
+
parser.set_defaults(handler=handle_campaign_run_cancel)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _add_campaign_results(sub: argparse._SubParsersAction) -> None:
|
|
520
|
+
parser = sub.add_parser("results", help="Structured post-call extraction results")
|
|
521
|
+
results_sub = parser.add_subparsers(dest="results_command", required=True)
|
|
522
|
+
list_parser = results_sub.add_parser("list", help="List campaign results")
|
|
523
|
+
list_parser.add_argument("campaign_id")
|
|
524
|
+
list_parser.add_argument("--status")
|
|
525
|
+
list_parser.add_argument("--campaign-run-id", dest="campaign_run_id")
|
|
526
|
+
list_parser.add_argument("--schema-name", dest="schema_name")
|
|
527
|
+
list_parser.add_argument("--limit", type=int)
|
|
528
|
+
list_parser.set_defaults(handler=handle_campaign_results_list)
|
|
529
|
+
get_parser = results_sub.add_parser("get", help="Fetch one campaign result")
|
|
530
|
+
get_parser.add_argument("campaign_id")
|
|
531
|
+
get_parser.add_argument("result_id")
|
|
532
|
+
get_parser.set_defaults(handler=handle_campaign_result_get)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def register(subparsers: argparse._SubParsersAction) -> None:
|
|
536
|
+
campaigns = subparsers.add_parser("campaigns", help="Voice campaign operations")
|
|
537
|
+
camp_sub = campaigns.add_subparsers(dest="campaigns_command", required=True)
|
|
538
|
+
_add_campaign_list(camp_sub)
|
|
539
|
+
_add_campaign_get(camp_sub)
|
|
540
|
+
_add_campaign_preview(camp_sub)
|
|
541
|
+
_add_campaign_create(camp_sub)
|
|
542
|
+
_add_campaign_update(camp_sub)
|
|
543
|
+
_add_campaign_readiness(camp_sub)
|
|
544
|
+
_add_campaign_activate(camp_sub)
|
|
545
|
+
_add_campaign_pause(camp_sub)
|
|
546
|
+
_add_campaign_archive(camp_sub)
|
|
547
|
+
_add_campaign_run(camp_sub)
|
|
548
|
+
_add_campaign_runs_list(camp_sub)
|
|
549
|
+
_add_campaign_run_get(camp_sub)
|
|
550
|
+
_add_campaign_run_cancel(camp_sub)
|
|
551
|
+
_add_campaign_results(camp_sub)
|
|
552
|
+
add_campaign_launch_parser(camp_sub)
|
|
553
|
+
|
|
554
|
+
add_campaign_launch_parser(subparsers, command_name="call")
|
|
555
|
+
|
|
556
|
+
compliance = camp_sub.add_parser("compliance", help="Campaign compliance settings")
|
|
557
|
+
comp_sub = compliance.add_subparsers(dest="compliance_command", required=True)
|
|
558
|
+
_add_compliance_get(comp_sub)
|
|
559
|
+
_add_compliance_set(comp_sub)
|
|
560
|
+
_add_compliance_approve(comp_sub)
|
|
561
|
+
_add_compliance_disable(comp_sub)
|
|
562
|
+
_add_compliance_preview(comp_sub)
|
|
563
|
+
_add_compliance_preflight(comp_sub)
|
|
564
|
+
|
|
565
|
+
audience = camp_sub.add_parser(
|
|
566
|
+
"audience", help="Campaign audience members (import with per-target metadata)"
|
|
567
|
+
)
|
|
568
|
+
aud_sub = audience.add_subparsers(dest="audience_command", required=True)
|
|
569
|
+
_add_campaign_audience_import(aud_sub)
|
|
570
|
+
_add_campaign_audience_list(aud_sub)
|
|
571
|
+
_add_campaign_audience_queue_eligible(aud_sub)
|
guzli/commands/common.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from guzli.errors import ValidationError
|
|
7
|
+
from guzli.runtime import RuntimeContext
|
|
8
|
+
from guzli.validators import parse_bool
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_json_flag(parser: argparse.ArgumentParser) -> None:
|
|
12
|
+
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_bool_flag(value: str) -> bool:
|
|
16
|
+
parsed = parse_bool(value, "boolean flag")
|
|
17
|
+
if parsed is None:
|
|
18
|
+
raise ValidationError("Boolean flag is required")
|
|
19
|
+
return parsed
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def require_org_id(context: RuntimeContext, args: argparse.Namespace) -> str:
|
|
23
|
+
organization_id = getattr(args, "organization_id_arg", None) or context.organization_id
|
|
24
|
+
if not organization_id:
|
|
25
|
+
raise ValidationError(
|
|
26
|
+
"organization-id required: pass --org / --organization-id, set "
|
|
27
|
+
"GUZLI_ORGANIZATION_ID, or store it in your profile (guzli auth use ...)."
|
|
28
|
+
)
|
|
29
|
+
return organization_id
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def add_org_arg(parser: argparse.ArgumentParser) -> None:
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--org",
|
|
35
|
+
"--organization-id",
|
|
36
|
+
dest="organization_id_arg",
|
|
37
|
+
help="Organization UUID (falls back to GUZLI_ORGANIZATION_ID / active profile)",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def print_error(message: str) -> None:
|
|
42
|
+
print(message, file=sys.stderr)
|