connect-flow 0.1.1__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.
- connect_flow/__init__.py +1 -0
- connect_flow/__main__.py +4 -0
- connect_flow/args.py +647 -0
- connect_flow/arn.py +177 -0
- connect_flow/autoresolve.py +259 -0
- connect_flow/aws.py +414 -0
- connect_flow/cli.py +612 -0
- connect_flow/diff.py +262 -0
- connect_flow/digest.py +135 -0
- connect_flow/exits.py +27 -0
- connect_flow/flow.py +121 -0
- connect_flow/graph.py +147 -0
- connect_flow/layout.py +185 -0
- connect_flow/lint.py +89 -0
- connect_flow/load.py +151 -0
- connect_flow/mapping.py +343 -0
- connect_flow/metrics.py +94 -0
- connect_flow/pipeline.py +917 -0
- connect_flow/py.typed +0 -0
- connect_flow/remote_write.py +177 -0
- connect_flow/render.py +66 -0
- connect_flow/report.py +184 -0
- connect_flow/resolve.py +324 -0
- connect_flow/targets.py +167 -0
- connect_flow/writer.py +211 -0
- connect_flow-0.1.1.dist-info/METADATA +716 -0
- connect_flow-0.1.1.dist-info/RECORD +30 -0
- connect_flow-0.1.1.dist-info/WHEEL +4 -0
- connect_flow-0.1.1.dist-info/entry_points.txt +2 -0
- connect_flow-0.1.1.dist-info/licenses/LICENSE +21 -0
connect_flow/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.1"
|
connect_flow/__main__.py
ADDED
connect_flow/args.py
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
"""Argument parser construction for the ``connect-flow`` console script, plus
|
|
2
|
+
the two cross-flag validators that belong with it.
|
|
3
|
+
|
|
4
|
+
Split out of cli.py: parser construction is ~500 lines of pure declaration
|
|
5
|
+
with no behaviour, and it depends on nothing else in the CLI beyond an exit
|
|
6
|
+
code. Keeping it here leaves cli.py as the verb dispatch it is named for.
|
|
7
|
+
See docs/adr/0011-cli-module-split.md.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from connect_flow import __version__
|
|
16
|
+
from connect_flow.exits import EXIT_USAGE_ERROR
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _ArgumentParser(argparse.ArgumentParser):
|
|
20
|
+
"""argparse exits with status 2 on its own usage errors; we reserve 2 for
|
|
21
|
+
safety violations and want 3 for bad usage (see module docstring)."""
|
|
22
|
+
|
|
23
|
+
def error(self, message: str) -> None:
|
|
24
|
+
self.print_usage(sys.stderr)
|
|
25
|
+
self.exit(EXIT_USAGE_ERROR, f"{self.prog}: error: {message}\n")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _add_common_output_arguments(
|
|
29
|
+
parser: argparse.ArgumentParser, *, suppress_defaults: bool = False
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Flags shared by every ``connect-flow`` subcommand, and by the top-level
|
|
32
|
+
parser itself so they can also be given before the subcommand, aws-cli
|
|
33
|
+
style (``connect-flow --profile p --region r list``).
|
|
34
|
+
|
|
35
|
+
A subparser's ``parse_known_args`` is handed a *fresh* sub-namespace, not
|
|
36
|
+
the parent's - once it returns, argparse copies every attribute that
|
|
37
|
+
sub-namespace holds onto the parent namespace unconditionally, clobbering
|
|
38
|
+
whatever the top-level parsing already set. ``suppress_defaults=True``
|
|
39
|
+
(used only for the per-subcommand copies of these flags) makes an
|
|
40
|
+
unsupplied flag simply absent from that sub-namespace instead of present
|
|
41
|
+
with a default of ``None``/``False``, so the merge has nothing to
|
|
42
|
+
clobber with and a value given before the subcommand survives.
|
|
43
|
+
"""
|
|
44
|
+
str_default = argparse.SUPPRESS if suppress_defaults else None
|
|
45
|
+
bool_default = argparse.SUPPRESS if suppress_defaults else False
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--profile", metavar="NAME", default=str_default, help="AWS profile to use"
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--region",
|
|
51
|
+
metavar="NAME",
|
|
52
|
+
default=str_default,
|
|
53
|
+
help="AWS region; inferred from a target ARN if omitted, or checked for a "
|
|
54
|
+
"conflict if a target ARN disagrees. Where a region must be known up "
|
|
55
|
+
"front (--instance addressing, list) and none is given, falls back "
|
|
56
|
+
"to your AWS profile/config default",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--json",
|
|
60
|
+
action="store_true",
|
|
61
|
+
default=bool_default,
|
|
62
|
+
help="machine-readable report on stdout",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--quiet", action="store_true", default=bool_default, help="errors only"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _add_instance_argument(parser: argparse.ArgumentParser) -> None:
|
|
70
|
+
"""--instance: a Connect instance id or alias, auto-detected via
|
|
71
|
+
aws.is_id_like() - a name is resolved via ListInstances (exact match).
|
|
72
|
+
Added everywhere instance addressing is needed, so this is the one place
|
|
73
|
+
the flag is defined. See docs/adr/0008-unified-instance-flow-addressing.md."""
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--instance",
|
|
76
|
+
metavar="ID-OR-ALIAS",
|
|
77
|
+
default=None,
|
|
78
|
+
help="Connect instance id, or alias resolved via ListInstances (exact match)",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _add_flow_arguments(
|
|
83
|
+
parser: argparse.ArgumentParser, *, flow_help: str, module_help: str
|
|
84
|
+
) -> None:
|
|
85
|
+
"""--flow/--flow-module: each value may be a full ARN, a bare id, a bare
|
|
86
|
+
name (resolved against --instance), or INSTANCE/NAME-OR-ID embedding its
|
|
87
|
+
own instance (id or alias, resolved independently of --instance). Added
|
|
88
|
+
everywhere this addressing is needed, so this is the one place these
|
|
89
|
+
flags are defined. See docs/adr/0008-unified-instance-flow-addressing.md."""
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"--flow",
|
|
92
|
+
metavar="[INSTANCE/]ID-OR-NAME-OR-ARN",
|
|
93
|
+
action="append",
|
|
94
|
+
default=[],
|
|
95
|
+
help=flow_help,
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--flow-module",
|
|
99
|
+
metavar="[INSTANCE/]ID-OR-NAME-OR-ARN",
|
|
100
|
+
action="append",
|
|
101
|
+
default=[],
|
|
102
|
+
help=module_help,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _add_prettify_arguments(parser: argparse.ArgumentParser) -> None:
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"targets",
|
|
109
|
+
nargs="*",
|
|
110
|
+
metavar="TARGET",
|
|
111
|
+
help="local flow JSON file(s), or flow/flow-module ARN(s)",
|
|
112
|
+
)
|
|
113
|
+
_add_instance_argument(parser)
|
|
114
|
+
_add_flow_arguments(
|
|
115
|
+
parser,
|
|
116
|
+
flow_help="contact flow to target (repeatable): a bare id, a name (resolved via "
|
|
117
|
+
"ListContactFlows against --instance), a full ARN, or INSTANCE/NAME-OR-ID embedding "
|
|
118
|
+
"its own instance",
|
|
119
|
+
module_help="flow module to target (repeatable): a bare id, a name (resolved via "
|
|
120
|
+
"ListContactFlowModules against --instance), a full ARN, or INSTANCE/NAME-OR-ID "
|
|
121
|
+
"embedding its own instance",
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"--stage",
|
|
125
|
+
choices=["published", "saved"],
|
|
126
|
+
default=None,
|
|
127
|
+
help="which remote content to read/write (default: published for reads, "
|
|
128
|
+
"saved for writes)",
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument(
|
|
131
|
+
"--in-place",
|
|
132
|
+
action="store_true",
|
|
133
|
+
help="write the result back to its source (an upload, for a remote target)",
|
|
134
|
+
)
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
"--output",
|
|
137
|
+
"-o",
|
|
138
|
+
metavar="FILE",
|
|
139
|
+
default=None,
|
|
140
|
+
help="write the prettified flow to this exact path (single target only)",
|
|
141
|
+
)
|
|
142
|
+
parser.add_argument(
|
|
143
|
+
"--output-dir",
|
|
144
|
+
metavar="DIR",
|
|
145
|
+
default=None,
|
|
146
|
+
help="write each prettified flow to DIR/STEM.json",
|
|
147
|
+
)
|
|
148
|
+
parser.add_argument(
|
|
149
|
+
"--stdout",
|
|
150
|
+
action="store_true",
|
|
151
|
+
help="emit the rewritten JSON to stdout instead of writing anywhere (single target only)",
|
|
152
|
+
)
|
|
153
|
+
parser.add_argument(
|
|
154
|
+
"--check",
|
|
155
|
+
action="store_true",
|
|
156
|
+
help="never write; exit 1 if any target would change (CI / pre-commit gate)",
|
|
157
|
+
)
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
"--dry-run", action="store_true", help="never write; report what would change"
|
|
160
|
+
)
|
|
161
|
+
parser.add_argument(
|
|
162
|
+
"--diff", action="store_true", help="print a unified diff of changes"
|
|
163
|
+
)
|
|
164
|
+
parser.add_argument(
|
|
165
|
+
"--no-backup",
|
|
166
|
+
action="store_true",
|
|
167
|
+
help="skip writing a .bak file (local targets only)",
|
|
168
|
+
)
|
|
169
|
+
parser.add_argument(
|
|
170
|
+
"--render",
|
|
171
|
+
action="store_true",
|
|
172
|
+
help="also write STEM.before.png and STEM.after.png for each target "
|
|
173
|
+
"(next to a local input, or into --render-dir)",
|
|
174
|
+
)
|
|
175
|
+
parser.add_argument(
|
|
176
|
+
"--render-dir",
|
|
177
|
+
metavar="DIR",
|
|
178
|
+
default=None,
|
|
179
|
+
help="directory to write --render PNGs into (required for a remote target)",
|
|
180
|
+
)
|
|
181
|
+
_add_remote_write_arguments(parser)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _add_remote_write_arguments(parser: argparse.ArgumentParser) -> None:
|
|
185
|
+
"""Flags that only make sense alongside a remote write (--in-place on a
|
|
186
|
+
RemoteTarget, or push). Shared between the prettify and push subparsers."""
|
|
187
|
+
parser.add_argument(
|
|
188
|
+
"--publish",
|
|
189
|
+
action="store_true",
|
|
190
|
+
help="write the live content instead of leaving it a saved draft",
|
|
191
|
+
)
|
|
192
|
+
parser.add_argument(
|
|
193
|
+
"--snapshot",
|
|
194
|
+
choices=["before", "after", "both", "none"],
|
|
195
|
+
default="before",
|
|
196
|
+
help="create an immutable version snapshot around the upload (default: before)",
|
|
197
|
+
)
|
|
198
|
+
parser.add_argument(
|
|
199
|
+
"--snapshot-description",
|
|
200
|
+
metavar="TEXT",
|
|
201
|
+
default=None,
|
|
202
|
+
help=f"description for the snapshot (default: connect-flow {__version__}: pre-format snapshot)",
|
|
203
|
+
)
|
|
204
|
+
parser.add_argument(
|
|
205
|
+
"--local-backup",
|
|
206
|
+
metavar="DIR",
|
|
207
|
+
default=None,
|
|
208
|
+
help="write the pre-change content to DIR/STEM.json before uploading - a rollback "
|
|
209
|
+
"copy that doesn't depend on Create*Version, which Connect only supports for "
|
|
210
|
+
"Campaign-type contact flows",
|
|
211
|
+
)
|
|
212
|
+
parser.add_argument(
|
|
213
|
+
"--yes",
|
|
214
|
+
action="store_true",
|
|
215
|
+
help="skip the interactive confirmation before a remote write",
|
|
216
|
+
)
|
|
217
|
+
parser.add_argument(
|
|
218
|
+
"--force",
|
|
219
|
+
action="store_true",
|
|
220
|
+
help="proceed despite a sha conflict or an ARCHIVED resource",
|
|
221
|
+
)
|
|
222
|
+
parser.add_argument(
|
|
223
|
+
"--no-verify",
|
|
224
|
+
action="store_true",
|
|
225
|
+
help="skip the post-upload read-back digest check",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _add_multi_target_arguments(parser: argparse.ArgumentParser) -> None:
|
|
230
|
+
"""TARGET positionals plus the --instance/--flow/--flow-module
|
|
231
|
+
desugaring, shared by lint and digest (same shape as prettify's)."""
|
|
232
|
+
parser.add_argument(
|
|
233
|
+
"targets",
|
|
234
|
+
nargs="*",
|
|
235
|
+
metavar="TARGET",
|
|
236
|
+
help="local flow JSON file(s), or flow/flow-module ARN(s)",
|
|
237
|
+
)
|
|
238
|
+
_add_instance_argument(parser)
|
|
239
|
+
_add_flow_arguments(
|
|
240
|
+
parser,
|
|
241
|
+
flow_help="contact flow (repeatable): a bare id, a name (resolved via "
|
|
242
|
+
"ListContactFlows against --instance), a full ARN, or INSTANCE/NAME-OR-ID embedding "
|
|
243
|
+
"its own instance",
|
|
244
|
+
module_help="flow module (repeatable): a bare id, a name (resolved via "
|
|
245
|
+
"ListContactFlowModules against --instance), a full ARN, or INSTANCE/NAME-OR-ID "
|
|
246
|
+
"embedding its own instance",
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _add_translate_common_arguments(parser: argparse.ArgumentParser) -> None:
|
|
251
|
+
"""Flags shared by export/import/normalize: the environment map, the
|
|
252
|
+
discovery bootstrap, and the same destination-flag set prettify uses -
|
|
253
|
+
any of the three can also re-layout via --prettify, reusing prettify()'s
|
|
254
|
+
own engine rather than a second implementation."""
|
|
255
|
+
parser.add_argument(
|
|
256
|
+
"--map",
|
|
257
|
+
metavar="PATH",
|
|
258
|
+
action="append",
|
|
259
|
+
default=[],
|
|
260
|
+
help="environment map file (repeatable); a local path, or (export/import "
|
|
261
|
+
"only) an s3://bucket/key URI",
|
|
262
|
+
)
|
|
263
|
+
parser.add_argument(
|
|
264
|
+
"--emit-map",
|
|
265
|
+
metavar="FILE",
|
|
266
|
+
default=None,
|
|
267
|
+
help="write a map of every literal discovered in TARGET (single target "
|
|
268
|
+
"only) instead of translating it, auto-naming Lambda references from "
|
|
269
|
+
"their ARN; merges into FILE if it already exists, so a loop of "
|
|
270
|
+
"invocations against the same FILE accumulates one shared map",
|
|
271
|
+
)
|
|
272
|
+
parser.add_argument(
|
|
273
|
+
"--prettify",
|
|
274
|
+
action="store_true",
|
|
275
|
+
help="also rewrite block positions into a readable layout",
|
|
276
|
+
)
|
|
277
|
+
parser.add_argument(
|
|
278
|
+
"--in-place",
|
|
279
|
+
action="store_true",
|
|
280
|
+
help="write the result back to its source (an upload, for a remote target)",
|
|
281
|
+
)
|
|
282
|
+
parser.add_argument(
|
|
283
|
+
"--output",
|
|
284
|
+
"-o",
|
|
285
|
+
metavar="FILE",
|
|
286
|
+
default=None,
|
|
287
|
+
help="write the result to this exact path (single target only)",
|
|
288
|
+
)
|
|
289
|
+
parser.add_argument(
|
|
290
|
+
"--output-dir",
|
|
291
|
+
metavar="DIR",
|
|
292
|
+
default=None,
|
|
293
|
+
help="write each result to DIR/STEM.json",
|
|
294
|
+
)
|
|
295
|
+
parser.add_argument(
|
|
296
|
+
"--stdout",
|
|
297
|
+
action="store_true",
|
|
298
|
+
help="emit the rewritten JSON to stdout instead of writing anywhere (single target only)",
|
|
299
|
+
)
|
|
300
|
+
parser.add_argument(
|
|
301
|
+
"--check",
|
|
302
|
+
action="store_true",
|
|
303
|
+
help="never write; exit 1 if any target would change (CI / pre-commit gate)",
|
|
304
|
+
)
|
|
305
|
+
parser.add_argument(
|
|
306
|
+
"--dry-run", action="store_true", help="never write; report what would change"
|
|
307
|
+
)
|
|
308
|
+
parser.add_argument(
|
|
309
|
+
"--diff", action="store_true", help="print a unified diff of changes"
|
|
310
|
+
)
|
|
311
|
+
parser.add_argument(
|
|
312
|
+
"--no-backup",
|
|
313
|
+
action="store_true",
|
|
314
|
+
help="skip writing a .bak file (local targets only)",
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def build_connect_flow_parser() -> argparse.ArgumentParser:
|
|
319
|
+
"""``connect-flow``'s parser: git-style subcommands, shared output flags,
|
|
320
|
+
long-form-only (no short flags anywhere)."""
|
|
321
|
+
parser = _ArgumentParser(
|
|
322
|
+
prog="connect-flow",
|
|
323
|
+
description="Promote Amazon Connect contact flow JSON between environments and "
|
|
324
|
+
"auto-arrange its layout, without changing behaviour - for local files and live "
|
|
325
|
+
"Connect instances alike.",
|
|
326
|
+
)
|
|
327
|
+
parser.add_argument(
|
|
328
|
+
"--version", action="version", version=f"connect-flow {__version__}"
|
|
329
|
+
)
|
|
330
|
+
# Also accepted before the subcommand, aws-cli style (`connect-flow --profile
|
|
331
|
+
# p --region r list`), in addition to after it. argparse only applies a
|
|
332
|
+
# default when the namespace attribute isn't already set, so whichever
|
|
333
|
+
# position actually supplies a flag wins; the two placements are never in
|
|
334
|
+
# conflict with each other.
|
|
335
|
+
_add_common_output_arguments(parser)
|
|
336
|
+
subparsers = parser.add_subparsers(dest="command", required=True, metavar="COMMAND")
|
|
337
|
+
|
|
338
|
+
prettify_parser = subparsers.add_parser(
|
|
339
|
+
"prettify",
|
|
340
|
+
help="rewrite block positions into a readable layout (the core verb)",
|
|
341
|
+
)
|
|
342
|
+
_add_common_output_arguments(prettify_parser, suppress_defaults=True)
|
|
343
|
+
_add_prettify_arguments(prettify_parser)
|
|
344
|
+
|
|
345
|
+
copy_parser = subparsers.add_parser(
|
|
346
|
+
"copy",
|
|
347
|
+
help="promote a flow from --from to --to (1:1); a live --to shows the diff "
|
|
348
|
+
"and prompts before writing",
|
|
349
|
+
)
|
|
350
|
+
_add_common_output_arguments(copy_parser, suppress_defaults=True)
|
|
351
|
+
copy_parser.add_argument(
|
|
352
|
+
"--from",
|
|
353
|
+
dest="from_",
|
|
354
|
+
metavar="SOURCE",
|
|
355
|
+
required=True,
|
|
356
|
+
help="a local flow JSON file, '-' (stdin), a full ARN, or INSTANCE/NAME-OR-ID "
|
|
357
|
+
"(see docs/adr/0009-copy-verb.md for the disambiguation rule)",
|
|
358
|
+
)
|
|
359
|
+
copy_parser.add_argument(
|
|
360
|
+
"--to",
|
|
361
|
+
metavar="DEST",
|
|
362
|
+
required=True,
|
|
363
|
+
help="a local flow JSON file, a full ARN, or INSTANCE/NAME-OR-ID; a live --to is "
|
|
364
|
+
"written as a saved draft (add --publish to go live) after showing the diff "
|
|
365
|
+
"and prompting, or not written at all with --output/--stdout",
|
|
366
|
+
)
|
|
367
|
+
copy_parser.add_argument(
|
|
368
|
+
"--kind",
|
|
369
|
+
choices=["flow", "flow-module"],
|
|
370
|
+
default="flow",
|
|
371
|
+
help="resource kind addressed by an INSTANCE/NAME --from/--to value (default: flow)",
|
|
372
|
+
)
|
|
373
|
+
copy_parser.add_argument(
|
|
374
|
+
"--from-map",
|
|
375
|
+
metavar="PATH",
|
|
376
|
+
action="append",
|
|
377
|
+
default=[],
|
|
378
|
+
help="environment map (repeatable) normalising --from's real identifiers to "
|
|
379
|
+
"placeholders; a local path or s3://bucket/key URI",
|
|
380
|
+
)
|
|
381
|
+
copy_parser.add_argument(
|
|
382
|
+
"--to-map",
|
|
383
|
+
metavar="PATH",
|
|
384
|
+
action="append",
|
|
385
|
+
default=[],
|
|
386
|
+
help="environment map (repeatable) resolving placeholders to --to's real "
|
|
387
|
+
"identifiers; a local path or s3://bucket/key URI",
|
|
388
|
+
)
|
|
389
|
+
copy_parser.add_argument(
|
|
390
|
+
"--emit-map",
|
|
391
|
+
metavar="FILE",
|
|
392
|
+
default=None,
|
|
393
|
+
help="write a map of every literal discovered in --from instead of "
|
|
394
|
+
"copying, auto-naming ContactFlow/FlowModule/Queue/Lambda references "
|
|
395
|
+
"(a live --from name lookup, disabled by --no-auto-resolve); merges "
|
|
396
|
+
"into FILE if it already exists, so a loop of invocations against the "
|
|
397
|
+
"same FILE accumulates one shared map",
|
|
398
|
+
)
|
|
399
|
+
copy_parser.add_argument(
|
|
400
|
+
"--prettify",
|
|
401
|
+
action="store_true",
|
|
402
|
+
help="also rewrite block positions into a readable layout",
|
|
403
|
+
)
|
|
404
|
+
copy_parser.add_argument(
|
|
405
|
+
"--stage",
|
|
406
|
+
choices=["published", "saved"],
|
|
407
|
+
default=None,
|
|
408
|
+
help="which remote content to read/write (default: published for reads, "
|
|
409
|
+
"saved for writes)",
|
|
410
|
+
)
|
|
411
|
+
# A live --to is written by default (after a diff and a prompt), so --apply
|
|
412
|
+
# no longer gates anything. Kept as an accepted no-op rather than removed:
|
|
413
|
+
# it shipped in the help text and in ADR 0009's examples, and an unknown
|
|
414
|
+
# flag would abort a script that has it hard-coded, which is a worse
|
|
415
|
+
# outcome than one warning line. See docs/adr/0015-copy-writes-by-default.md.
|
|
416
|
+
copy_parser.add_argument("--apply", action="store_true", help=argparse.SUPPRESS)
|
|
417
|
+
emit_group = copy_parser.add_mutually_exclusive_group()
|
|
418
|
+
emit_group.add_argument(
|
|
419
|
+
"--output",
|
|
420
|
+
metavar="FILE",
|
|
421
|
+
default=None,
|
|
422
|
+
help="resolve against a live --to and write the result to FILE instead of "
|
|
423
|
+
"uploading it",
|
|
424
|
+
)
|
|
425
|
+
emit_group.add_argument(
|
|
426
|
+
"--stdout",
|
|
427
|
+
action="store_true",
|
|
428
|
+
help="resolve against a live --to and print the result instead of uploading it",
|
|
429
|
+
)
|
|
430
|
+
copy_parser.add_argument(
|
|
431
|
+
"--no-backup",
|
|
432
|
+
action="store_true",
|
|
433
|
+
help="skip writing a .bak file (local --to only)",
|
|
434
|
+
)
|
|
435
|
+
copy_parser.add_argument(
|
|
436
|
+
"--no-auto-resolve",
|
|
437
|
+
action="store_true",
|
|
438
|
+
help="don't carry sibling flow/module/queue references across by name "
|
|
439
|
+
"between two live instances; only --from-map/--to-map entries apply "
|
|
440
|
+
"(also disables --emit-map's live name lookup) - see "
|
|
441
|
+
"docs/adr/0013-copy-name-auto-resolution.md",
|
|
442
|
+
)
|
|
443
|
+
copy_parser.add_argument(
|
|
444
|
+
"--exit-code",
|
|
445
|
+
action="store_true",
|
|
446
|
+
help="with --output/--stdout, exit 1 if the destination would change, like "
|
|
447
|
+
"diff(1); without it a successful run exits 0 - see "
|
|
448
|
+
"docs/adr/0014-copy-preview-exit-code.md",
|
|
449
|
+
)
|
|
450
|
+
_add_remote_write_arguments(copy_parser)
|
|
451
|
+
|
|
452
|
+
normalize_parser = subparsers.add_parser(
|
|
453
|
+
"normalize",
|
|
454
|
+
help="offline: local files only, no AWS - translate identifiers via --map, either direction",
|
|
455
|
+
)
|
|
456
|
+
_add_common_output_arguments(normalize_parser, suppress_defaults=True)
|
|
457
|
+
normalize_parser.add_argument(
|
|
458
|
+
"targets", nargs="+", metavar="TARGET", help="local flow JSON file(s)"
|
|
459
|
+
)
|
|
460
|
+
_add_translate_common_arguments(normalize_parser)
|
|
461
|
+
normalize_parser.add_argument(
|
|
462
|
+
"--reverse",
|
|
463
|
+
action="store_true",
|
|
464
|
+
help="resolve placeholders to real values instead of normalising real values to placeholders",
|
|
465
|
+
)
|
|
466
|
+
normalize_parser.add_argument(
|
|
467
|
+
"--force",
|
|
468
|
+
action="store_true",
|
|
469
|
+
help="proceed despite unresolved identifiers (warn instead of erroring)",
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
list_parser = subparsers.add_parser(
|
|
473
|
+
"list", help="list instances, or flows / flow modules within one (read-only)"
|
|
474
|
+
)
|
|
475
|
+
_add_common_output_arguments(list_parser, suppress_defaults=True)
|
|
476
|
+
list_parser.add_argument(
|
|
477
|
+
"--instance",
|
|
478
|
+
metavar="ID-OR-ALIAS",
|
|
479
|
+
default=None,
|
|
480
|
+
help="Connect instance id, or alias resolved via ListInstances (exact match); "
|
|
481
|
+
"omit to list instances instead",
|
|
482
|
+
)
|
|
483
|
+
_add_flow_arguments(
|
|
484
|
+
list_parser,
|
|
485
|
+
flow_help="describe this contact flow instead of listing (repeatable, but only "
|
|
486
|
+
"one flow may resolve in total): a bare id, a name (resolved via ListContactFlows "
|
|
487
|
+
"against --instance), a full ARN, or INSTANCE/NAME-OR-ID embedding its own instance",
|
|
488
|
+
module_help="describe this flow module instead of listing (repeatable, but only "
|
|
489
|
+
"one flow may resolve in total): a bare id, a name (resolved via "
|
|
490
|
+
"ListContactFlowModules against --instance), a full ARN, or INSTANCE/NAME-OR-ID "
|
|
491
|
+
"embedding its own instance",
|
|
492
|
+
)
|
|
493
|
+
kind_group = list_parser.add_mutually_exclusive_group()
|
|
494
|
+
kind_group.add_argument(
|
|
495
|
+
"--flows", action="store_true", help="list contact flows (default)"
|
|
496
|
+
)
|
|
497
|
+
kind_group.add_argument(
|
|
498
|
+
"--flow-modules", action="store_true", help="list flow modules"
|
|
499
|
+
)
|
|
500
|
+
list_parser.add_argument(
|
|
501
|
+
"--state",
|
|
502
|
+
choices=["ACTIVE", "ARCHIVED"],
|
|
503
|
+
default=None,
|
|
504
|
+
help="filter by resource state",
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
diff_parser = subparsers.add_parser(
|
|
508
|
+
"diff", help="semantic diff between two targets (local file and/or live ARN)"
|
|
509
|
+
)
|
|
510
|
+
_add_common_output_arguments(diff_parser, suppress_defaults=True)
|
|
511
|
+
diff_parser.add_argument(
|
|
512
|
+
"target_a",
|
|
513
|
+
metavar="TARGET_A",
|
|
514
|
+
help="a local flow JSON file, a full ARN, or INSTANCE/NAME-OR-ID "
|
|
515
|
+
"(see docs/adr/0009-copy-verb.md for the disambiguation rule)",
|
|
516
|
+
)
|
|
517
|
+
diff_parser.add_argument(
|
|
518
|
+
"target_b",
|
|
519
|
+
metavar="TARGET_B",
|
|
520
|
+
nargs="?",
|
|
521
|
+
default=None,
|
|
522
|
+
help="a local flow JSON file, a full ARN, or INSTANCE/NAME-OR-ID "
|
|
523
|
+
"(omit with --both-stages)",
|
|
524
|
+
)
|
|
525
|
+
# See docs/adr/0010-diff-instance-addressing.md: reuses copy's --kind
|
|
526
|
+
# rather than diff's own --instance/--flow/--flow-module flags.
|
|
527
|
+
diff_parser.add_argument(
|
|
528
|
+
"--kind",
|
|
529
|
+
choices=["flow", "flow-module"],
|
|
530
|
+
default="flow",
|
|
531
|
+
help="resource kind addressed by an INSTANCE/NAME-OR-ID TARGET_A/TARGET_B "
|
|
532
|
+
"value (default: flow)",
|
|
533
|
+
)
|
|
534
|
+
diff_parser.add_argument(
|
|
535
|
+
"--stage",
|
|
536
|
+
choices=["published", "saved"],
|
|
537
|
+
default=None,
|
|
538
|
+
help="which remote content to read for a remote target with no explicit "
|
|
539
|
+
":$SAVED (or :SAVED) qualifier (default: published)",
|
|
540
|
+
)
|
|
541
|
+
diff_parser.add_argument(
|
|
542
|
+
"--both-stages",
|
|
543
|
+
action="store_true",
|
|
544
|
+
help="diff TARGET_A's saved draft against its published content - TARGET_A "
|
|
545
|
+
"must be a remote ARN, and TARGET_B must be omitted",
|
|
546
|
+
)
|
|
547
|
+
diff_parser.add_argument(
|
|
548
|
+
"--functional",
|
|
549
|
+
action="store_true",
|
|
550
|
+
help="compare only what the flow does at runtime - drop ActionMetadata, its "
|
|
551
|
+
"key order, Annotations and snapToGrid, which the console rewrites on every "
|
|
552
|
+
"save and which therefore differ between two snapshots of a flow nobody "
|
|
553
|
+
"edited; combine with --normalize-opaque to compare across instances",
|
|
554
|
+
)
|
|
555
|
+
diff_parser.add_argument(
|
|
556
|
+
"--normalize-opaque",
|
|
557
|
+
action="store_true",
|
|
558
|
+
help="replace ${...} templatefile placeholders and ARN-shaped literals with "
|
|
559
|
+
"<opaque> before comparing, so a local .tftpl can compare cleanly against live "
|
|
560
|
+
"content; for a remote target, also resolves a referenced ContactFlowId/"
|
|
561
|
+
"FlowModuleId/QueueId to its Name so the same sibling resource in a different "
|
|
562
|
+
"instance doesn't show as a diff - hides genuine wrong-reference changes, so "
|
|
563
|
+
"it's off by default",
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
lint_parser = subparsers.add_parser(
|
|
567
|
+
"lint",
|
|
568
|
+
help="structural checks: dangling transitions, unreachable actions, empty branches",
|
|
569
|
+
)
|
|
570
|
+
_add_common_output_arguments(lint_parser, suppress_defaults=True)
|
|
571
|
+
_add_multi_target_arguments(lint_parser)
|
|
572
|
+
lint_parser.add_argument(
|
|
573
|
+
"--stage",
|
|
574
|
+
choices=["published", "saved"],
|
|
575
|
+
default=None,
|
|
576
|
+
help="which remote content to check (default: published)",
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
digest_parser = subparsers.add_parser(
|
|
580
|
+
"digest", help="print each target's semantic digest"
|
|
581
|
+
)
|
|
582
|
+
_add_common_output_arguments(digest_parser, suppress_defaults=True)
|
|
583
|
+
_add_multi_target_arguments(digest_parser)
|
|
584
|
+
digest_parser.add_argument(
|
|
585
|
+
"--stage",
|
|
586
|
+
choices=["published", "saved"],
|
|
587
|
+
default=None,
|
|
588
|
+
help="which remote content to digest (default: published)",
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
return parser
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
_DESTINATION_FLAGS = ("in_place", "output", "output_dir", "stdout", "check", "dry_run")
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _validate_destination_args(
|
|
598
|
+
args: argparse.Namespace, target_count: int, *, allow_default_stdout: bool = False
|
|
599
|
+
) -> str | None:
|
|
600
|
+
"""Returns an error message, or None if args are fine. Shared by every
|
|
601
|
+
subcommand that writes somewhere (prettify, export, import, normalize) -
|
|
602
|
+
they all use the same mutually-exclusive destination flag set.
|
|
603
|
+
|
|
604
|
+
``target_count`` is the *resolved* target count (positionals plus any
|
|
605
|
+
--instance/--flow desugaring), not len(args.targets) - a TARGET can
|
|
606
|
+
be entirely absent when --instance/--flow supplies it instead, and
|
|
607
|
+
checking the raw positional list would wrongly reject that combination
|
|
608
|
+
with --stdout/--output.
|
|
609
|
+
|
|
610
|
+
``allow_default_stdout``: for prettify/export/normalize, no destination
|
|
611
|
+
flag at all with exactly one target is treated as ``--stdout`` rather
|
|
612
|
+
than an error - their result is inspected as often as written. This also
|
|
613
|
+
implies ``--quiet``, since the point is the content itself, not a report
|
|
614
|
+
about it; an explicit ``--stdout`` is unaffected and still reports
|
|
615
|
+
normally unless ``--quiet`` is passed too. ``import`` always writes
|
|
616
|
+
something real (a local destination or ``--to`` a live ARN), so it keeps
|
|
617
|
+
requiring an explicit destination flag. See
|
|
618
|
+
docs/adr/0005-default-to-stdout.md."""
|
|
619
|
+
given = [name for name in _DESTINATION_FLAGS if getattr(args, name)]
|
|
620
|
+
if not given:
|
|
621
|
+
if allow_default_stdout and target_count == 1:
|
|
622
|
+
args.stdout = True
|
|
623
|
+
args.quiet = (
|
|
624
|
+
True # implicit stdout: the content itself is the point, not a report
|
|
625
|
+
)
|
|
626
|
+
return None
|
|
627
|
+
return (
|
|
628
|
+
"a destination is required: one of --in-place, --output FILE, "
|
|
629
|
+
"--output-dir DIR, --stdout, --check or --dry-run"
|
|
630
|
+
)
|
|
631
|
+
if len(given) > 1:
|
|
632
|
+
readable = ", ".join(f"--{n.replace('_', '-')}" for n in given)
|
|
633
|
+
return f"{readable} are mutually exclusive - pass exactly one destination"
|
|
634
|
+
if args.stdout and target_count != 1:
|
|
635
|
+
return "--stdout requires exactly one TARGET"
|
|
636
|
+
if args.output and target_count != 1:
|
|
637
|
+
return "--output FILE requires exactly one TARGET - use --output-dir DIR for more than one"
|
|
638
|
+
return None
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _validate_prettify_args(args: argparse.Namespace, target_count: int) -> str | None:
|
|
642
|
+
error = _validate_destination_args(args, target_count, allow_default_stdout=True)
|
|
643
|
+
if error is not None:
|
|
644
|
+
return error
|
|
645
|
+
if args.render_dir is not None and not args.render:
|
|
646
|
+
return "--render-dir requires --render"
|
|
647
|
+
return None
|