protodial 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.
Files changed (79) hide show
  1. protodial/__init__.py +3 -0
  2. protodial/__main__.py +5 -0
  3. protodial/_rich_details.py +35 -0
  4. protodial/argument_parser/__init__.py +7 -0
  5. protodial/argument_parser/construction.py +406 -0
  6. protodial/argument_parser/parsing.py +115 -0
  7. protodial/argument_parser/resolution.py +162 -0
  8. protodial/argument_parser/target.py +227 -0
  9. protodial/argument_parser/validation.py +249 -0
  10. protodial/cli.py +662 -0
  11. protodial/commands/__init__.py +36 -0
  12. protodial/commands/models.py +131 -0
  13. protodial/commands/operations.py +29 -0
  14. protodial/errors.py +164 -0
  15. protodial/formats.py +14 -0
  16. protodial/message_io/__init__.py +61 -0
  17. protodial/message_io/dispatch.py +65 -0
  18. protodial/message_io/json_decoding.py +56 -0
  19. protodial/message_io/json_input.py +411 -0
  20. protodial/message_io/limits.py +59 -0
  21. protodial/message_io/models.py +90 -0
  22. protodial/message_io/output.py +94 -0
  23. protodial/message_io/protobuf_input.py +87 -0
  24. protodial/message_io/raw_input.py +125 -0
  25. protodial/message_io/text_input.py +114 -0
  26. protodial/presentation/__init__.py +51 -0
  27. protodial/presentation/_grapheme_data.py +1899 -0
  28. protodial/presentation/_graphemes.py +233 -0
  29. protodial/presentation/_truncation.py +198 -0
  30. protodial/presentation/descriptor.py +586 -0
  31. protodial/presentation/diagnostics/__init__.py +39 -0
  32. protodial/presentation/diagnostics/context.py +28 -0
  33. protodial/presentation/diagnostics/error_events.py +667 -0
  34. protodial/presentation/diagnostics/models.py +571 -0
  35. protodial/presentation/diagnostics/rendering.py +3161 -0
  36. protodial/presentation/rpc.py +195 -0
  37. protodial/presentation/sanitization.py +30 -0
  38. protodial/presentation/templates.py +131 -0
  39. protodial/py.typed +1 -0
  40. protodial/request_metadata/__init__.py +26 -0
  41. protodial/request_metadata/parsing.py +187 -0
  42. protodial/request_metadata/types.py +11 -0
  43. protodial/rich_status/__init__.py +36 -0
  44. protodial/rich_status/decoding.py +457 -0
  45. protodial/rich_status/models.py +43 -0
  46. protodial/rich_status/rendering.py +245 -0
  47. protodial/rpc.py +249 -0
  48. protodial/rpc_core/__init__.py +1 -0
  49. protodial/rpc_core/call.py +843 -0
  50. protodial/rpc_core/discovery.py +99 -0
  51. protodial/rpc_core/input.py +115 -0
  52. protodial/rpc_core/invoke.py +191 -0
  53. protodial/rpc_core/models.py +49 -0
  54. protodial/rpc_core/protocols.py +233 -0
  55. protodial/rpc_core/runtime.py +32 -0
  56. protodial/schema/__init__.py +1 -0
  57. protodial/schema/composition.py +95 -0
  58. protodial/schema/context.py +229 -0
  59. protodial/schema/contracts.py +170 -0
  60. protodial/schema/export.py +101 -0
  61. protodial/schema/factory.py +47 -0
  62. protodial/schema/local.py +607 -0
  63. protodial/schema/paths.py +54 -0
  64. protodial/schema/reflection.py +570 -0
  65. protodial/schema/registry.py +477 -0
  66. protodial/schema/source_locations.py +20 -0
  67. protodial/schema/source_protos.py +113 -0
  68. protodial/transport.py +103 -0
  69. protodial/transport_core/__init__.py +1 -0
  70. protodial/transport_core/channels.py +185 -0
  71. protodial/transport_core/insecure.py +584 -0
  72. protodial/transport_core/options.py +134 -0
  73. protodial/transport_core/protocols.py +82 -0
  74. protodial/transport_core/tls.py +335 -0
  75. protodial-1.0.0.dist-info/METADATA +306 -0
  76. protodial-1.0.0.dist-info/RECORD +79 -0
  77. protodial-1.0.0.dist-info/WHEEL +4 -0
  78. protodial-1.0.0.dist-info/entry_points.txt +2 -0
  79. protodial-1.0.0.dist-info/licenses/LICENSE +21 -0
protodial/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Python command-line client for arbitrary gRPC services."""
2
+
3
+ __version__ = "1.0.0"
protodial/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Module entry point for ``python -m protodial``."""
2
+
3
+ from .cli import main
4
+
5
+ raise SystemExit(main())
@@ -0,0 +1,35 @@
1
+ """Identify standard ``google.rpc`` rich-status detail messages.
2
+
3
+ This dependency-light module is intentionally outside ``rich_status`` so the
4
+ diagnostic bootstrap path can import it even when protobuf is unavailable.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ STANDARD_DETAIL_NAMES = frozenset(
10
+ {
11
+ "BadRequest",
12
+ "DebugInfo",
13
+ "ErrorInfo",
14
+ "Help",
15
+ "LocalizedMessage",
16
+ "PreconditionFailure",
17
+ "QuotaFailure",
18
+ "RequestInfo",
19
+ "ResourceInfo",
20
+ "RetryInfo",
21
+ }
22
+ )
23
+
24
+
25
+ def standard_detail_name(identity: object) -> str | None:
26
+ """Return a standard detail's short name from an exact Any identity."""
27
+
28
+ full_name = str(identity).rsplit("/", 1)[-1]
29
+ prefix = "google.rpc."
30
+ if not full_name.startswith(prefix):
31
+ return None
32
+ short_name = full_name.removeprefix(prefix)
33
+ if short_name not in STANDARD_DETAIL_NAMES:
34
+ return None
35
+ return short_name
@@ -0,0 +1,7 @@
1
+ """Focused command-line parsing implementation."""
2
+
3
+ from .construction import build_parser
4
+ from .parsing import parse_arguments
5
+ from .target import parse_target
6
+
7
+ __all__ = ["build_parser", "parse_arguments", "parse_target"]
@@ -0,0 +1,406 @@
1
+ """Construction of the protodial command-line parser."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from .. import __version__
8
+ from ..commands.models import (
9
+ DEFAULT_CHUNK_SIZE,
10
+ DEFAULT_CONNECT_TIMEOUT,
11
+ DEFAULT_INPUT_BUFFER,
12
+ DEFAULT_TIMEOUT,
13
+ )
14
+ from ..formats import MessageFormat
15
+ from ..schema.paths import default_proto_paths
16
+
17
+ _FORMATS = tuple(message_format.value for message_format in MessageFormat)
18
+ EXAMPLES = """\
19
+ Examples:
20
+ protodial [options...] grpc://HOST/SERVICE/METHOD
21
+ protodial [options...] --method SERVICE/METHOD TARGET
22
+ protodial list [options...] TARGET [SERVICE]
23
+ protodial describe [options...] TARGET SYMBOL
24
+ protodial describe [options...] grpc://HOST/SYMBOL
25
+ protodial describe [options...] grpc://HOST/SERVICE/METHOD
26
+
27
+ Run `protodial list --help` or `protodial describe --help` for discovery
28
+ options."""
29
+
30
+
31
+ def build_parser() -> argparse.ArgumentParser:
32
+ """Build the complete command-line argument parser."""
33
+ parser = argparse.ArgumentParser(
34
+ prog="protodial",
35
+ description=(
36
+ "Call and discover gRPC services using reflection or local schemas."
37
+ ),
38
+ epilog=EXAMPLES,
39
+ formatter_class=argparse.RawDescriptionHelpFormatter,
40
+ allow_abbrev=False,
41
+ )
42
+ parser.add_argument(
43
+ "--version", action="version", version=f"%(prog)s {__version__}"
44
+ )
45
+ subparsers = parser.add_subparsers(
46
+ dest="command_name", metavar="COMMAND", required=True
47
+ )
48
+
49
+ list_parser = subparsers.add_parser(
50
+ "list", help="list services or methods", allow_abbrev=False
51
+ )
52
+ _add_shared_arguments(list_parser)
53
+ list_parser.add_argument(
54
+ "--show-reflection",
55
+ action="store_true",
56
+ help="include reflection services in list output",
57
+ )
58
+ list_parser.add_argument("target", help="gRPC target or grpc[s]:// URL")
59
+ list_parser.add_argument(
60
+ "service",
61
+ nargs="?",
62
+ help="service name (may instead be included in the URL path)",
63
+ )
64
+ list_parser.set_defaults(get_template=False)
65
+
66
+ describe = subparsers.add_parser(
67
+ "describe", help="describe a schema symbol", allow_abbrev=False
68
+ )
69
+ _add_shared_arguments(describe)
70
+ describe.add_argument(
71
+ "--template",
72
+ action="store_true",
73
+ dest="get_template",
74
+ help=(
75
+ "print sample ProtoJSON for a message or method request instead "
76
+ "of its declaration"
77
+ ),
78
+ )
79
+ describe.add_argument(
80
+ "--template-depth",
81
+ type=int,
82
+ default=argparse.SUPPRESS,
83
+ help="maximum template nesting depth (default: 8)",
84
+ )
85
+ describe.add_argument(
86
+ "target",
87
+ help="gRPC target or grpc[s]:// URL containing the symbol or method",
88
+ )
89
+ describe.add_argument(
90
+ "symbol",
91
+ nargs="?",
92
+ help="schema symbol (when it is not included in TARGET)",
93
+ )
94
+ describe.set_defaults(show_reflection=False)
95
+ return parser
96
+
97
+
98
+ def build_invocation_parser() -> argparse.ArgumentParser:
99
+ """Build the parser for the implicit default invocation command."""
100
+ parser = argparse.ArgumentParser(
101
+ prog="protodial",
102
+ description=(
103
+ "Invoke a gRPC method, or use list/describe for schema discovery."
104
+ ),
105
+ epilog=EXAMPLES,
106
+ formatter_class=argparse.RawDescriptionHelpFormatter,
107
+ allow_abbrev=False,
108
+ )
109
+ parser.add_argument(
110
+ "--version", action="version", version=f"%(prog)s {__version__}"
111
+ )
112
+ _add_shared_arguments(parser)
113
+ _add_invocation_arguments(parser)
114
+ _add_rpc_metadata_argument(parser)
115
+ _add_response_diagnostic_arguments(parser)
116
+ parser.add_argument(
117
+ "--method",
118
+ help="SERVICE/METHOD for a resolver or Unix-socket target",
119
+ )
120
+ parser.add_argument(
121
+ "target",
122
+ help="grpc[s]:// invocation URL or native gRPC channel target",
123
+ )
124
+ parser.set_defaults(
125
+ command_name="invoke", get_template=False, show_reflection=False
126
+ )
127
+ return parser
128
+
129
+
130
+ def _add_shared_arguments(parser: argparse.ArgumentParser) -> None:
131
+ _add_schema_arguments(parser)
132
+ _add_transport_arguments(parser)
133
+ _add_connection_arguments(parser)
134
+ _add_metadata_arguments(parser)
135
+ _add_diagnostic_arguments(parser)
136
+
137
+
138
+ def _add_schema_arguments(parser: argparse.ArgumentParser) -> None:
139
+ proto_paths = default_proto_paths()
140
+ parser.add_argument(
141
+ "--proto", action="append", default=[], help="local .proto (repeatable)"
142
+ )
143
+ parser.add_argument(
144
+ "--protoset",
145
+ action="append",
146
+ default=[],
147
+ help="FileDescriptorSet (repeatable)",
148
+ )
149
+ parser.add_argument(
150
+ "--protoset-out", help="atomically write the complete descriptor set"
151
+ )
152
+ parser.add_argument(
153
+ "--use-reflection",
154
+ action="store_true",
155
+ help="use reflection for missing local symbols",
156
+ )
157
+ parser.add_argument(
158
+ "--proto-path",
159
+ action="append",
160
+ default=list(proto_paths),
161
+ help=(
162
+ "proto import root "
163
+ f"(repeatable; defaults: {', '.join(proto_paths)})"
164
+ ),
165
+ )
166
+
167
+
168
+ def _add_invocation_arguments(parser: argparse.ArgumentParser) -> None:
169
+ parser.add_argument(
170
+ "-d",
171
+ "--data",
172
+ default=argparse.SUPPRESS,
173
+ help="literal input; @path reads a file; - or @- reads stdin",
174
+ )
175
+ parser.add_argument(
176
+ "--input-format",
177
+ choices=_FORMATS,
178
+ default=argparse.SUPPRESS,
179
+ help="request encoding (default: json)",
180
+ )
181
+ parser.add_argument(
182
+ "--output-format",
183
+ choices=_FORMATS,
184
+ default=argparse.SUPPRESS,
185
+ help="response encoding (default: json)",
186
+ )
187
+ parser.add_argument(
188
+ "--field",
189
+ action="append",
190
+ default=argparse.SUPPRESS,
191
+ metavar="NAME=VALUE",
192
+ help="set a field on each raw input message (repeatable)",
193
+ )
194
+ parser.add_argument(
195
+ "--field:file",
196
+ dest="field_file",
197
+ action="append",
198
+ default=argparse.SUPPRESS,
199
+ metavar="NAME=PATH",
200
+ help="read a file (- for stdin) into a bytes field and use raw input",
201
+ )
202
+ parser.add_argument(
203
+ "--input-field",
204
+ default=argparse.SUPPRESS,
205
+ help="bytes field for raw input",
206
+ )
207
+ parser.add_argument(
208
+ "--output-field",
209
+ default=argparse.SUPPRESS,
210
+ help="bytes field for raw output",
211
+ )
212
+ parser.add_argument(
213
+ "--chunk-size",
214
+ type=int,
215
+ default=argparse.SUPPRESS,
216
+ help=f"raw stream chunk size in bytes (default: {DEFAULT_CHUNK_SIZE})",
217
+ )
218
+ parser.add_argument(
219
+ "--allow-unknown-fields",
220
+ action="store_true",
221
+ default=argparse.SUPPRESS,
222
+ help="ignore unknown ProtoJSON input fields",
223
+ )
224
+ parser.add_argument(
225
+ "--emit-defaults",
226
+ action="store_true",
227
+ default=argparse.SUPPRESS,
228
+ help="include fields with default values in ProtoJSON output",
229
+ )
230
+ parser.add_argument(
231
+ "--max-input-buffer",
232
+ type=int,
233
+ default=argparse.SUPPRESS,
234
+ help=f"maximum buffered input bytes (default: {DEFAULT_INPUT_BUFFER})",
235
+ )
236
+ parser.add_argument(
237
+ "--interactive",
238
+ action="store_true",
239
+ default=argparse.SUPPRESS,
240
+ help="unsupported; use --data - or --data @- for streaming stdin",
241
+ )
242
+ parser.add_argument(
243
+ "--wait-for-ready",
244
+ action="store_true",
245
+ default=argparse.SUPPRESS,
246
+ help=(
247
+ "let the RPC wait for readiness until --max-time; local schemas "
248
+ "skip the connection preflight"
249
+ ),
250
+ )
251
+
252
+
253
+ def _add_transport_arguments(parser: argparse.ArgumentParser) -> None:
254
+ transport = parser.add_mutually_exclusive_group()
255
+ transport.add_argument(
256
+ "--plaintext", action="store_true", help="disable TLS"
257
+ )
258
+ transport.add_argument(
259
+ "-k",
260
+ "--insecure",
261
+ action="store_true",
262
+ help="disable TLS peer verification (unsafe)",
263
+ )
264
+ parser.add_argument(
265
+ "--cacert",
266
+ action="append",
267
+ default=[],
268
+ help="PEM CA bundle (repeatable)",
269
+ )
270
+ parser.add_argument("--cert", help="PEM client certificate")
271
+ parser.add_argument("--key", help="PEM client private key")
272
+ parser.add_argument(
273
+ "--key-password",
274
+ help="client private-key password (may be visible in shell history)",
275
+ )
276
+ parser.add_argument(
277
+ "--servername", help="TLS SNI and verified server identity"
278
+ )
279
+ parser.add_argument("--authority", help="HTTP/2 :authority value")
280
+
281
+
282
+ def _add_connection_arguments(parser: argparse.ArgumentParser) -> None:
283
+ parser.add_argument(
284
+ "--connect-timeout",
285
+ type=float,
286
+ default=DEFAULT_CONNECT_TIMEOUT,
287
+ help=(
288
+ "channel readiness timeout in seconds "
289
+ f"(default: {DEFAULT_CONNECT_TIMEOUT:g})"
290
+ ),
291
+ )
292
+ parser.add_argument(
293
+ "-m",
294
+ "--max-time",
295
+ dest="timeout",
296
+ metavar="SECONDS",
297
+ type=float,
298
+ default=DEFAULT_TIMEOUT,
299
+ help="maximum RPC time in seconds",
300
+ )
301
+ parser.add_argument(
302
+ "--max-recv-message-size",
303
+ type=int,
304
+ help="maximum response message bytes",
305
+ )
306
+ parser.add_argument(
307
+ "--max-send-message-size",
308
+ type=int,
309
+ help="maximum request message bytes",
310
+ )
311
+ parser.add_argument(
312
+ "--compression",
313
+ choices=("none", "gzip", "deflate"),
314
+ default="none",
315
+ help="gRPC message compression (default: none)",
316
+ )
317
+ parser.add_argument(
318
+ "--keepalive-time",
319
+ type=float,
320
+ help="HTTP/2 keepalive interval in seconds",
321
+ )
322
+ parser.add_argument(
323
+ "-A", "--user-agent", help="gRPC primary user-agent value"
324
+ )
325
+
326
+
327
+ def _add_metadata_arguments(parser: argparse.ArgumentParser) -> None:
328
+ parser.add_argument(
329
+ "-H",
330
+ "--header",
331
+ action="append",
332
+ default=[],
333
+ help="shared metadata key:value",
334
+ )
335
+ parser.add_argument(
336
+ "--reflect-header",
337
+ action="append",
338
+ default=[],
339
+ help="reflection-only metadata",
340
+ )
341
+ parser.add_argument(
342
+ "--expand-headers", action="store_true", help="expand $NAME and ${NAME}"
343
+ )
344
+
345
+
346
+ def _add_rpc_metadata_argument(parser: argparse.ArgumentParser) -> None:
347
+ """Add metadata which is meaningful only for method invocation."""
348
+ parser.add_argument(
349
+ "--rpc-header",
350
+ action="append",
351
+ default=[],
352
+ help="application RPC-only metadata",
353
+ )
354
+
355
+
356
+ def _add_diagnostic_arguments(parser: argparse.ArgumentParser) -> None:
357
+ parser.add_argument(
358
+ "--color",
359
+ choices=("auto", "always", "never"),
360
+ default=argparse.SUPPRESS,
361
+ help="diagnostic color policy (default: auto)",
362
+ )
363
+ parser.add_argument(
364
+ "--diagnostic-format",
365
+ choices=("human", "json"),
366
+ default=argparse.SUPPRESS,
367
+ help="stderr diagnostic encoding (default: human)",
368
+ )
369
+ parser.add_argument(
370
+ "-v",
371
+ "--verbose",
372
+ action="count",
373
+ default=0,
374
+ help="increase stderr diagnostics",
375
+ )
376
+ parser.set_defaults(
377
+ show_headers=False,
378
+ show_trailers=False,
379
+ show_status=False,
380
+ format_error=False,
381
+ )
382
+
383
+
384
+ def _add_response_diagnostic_arguments(
385
+ parser: argparse.ArgumentParser,
386
+ ) -> None:
387
+ parser.add_argument(
388
+ "-i",
389
+ "--include",
390
+ dest="show_headers",
391
+ action="store_true",
392
+ help="include response metadata on stderr",
393
+ )
394
+ parser.add_argument(
395
+ "--show-trailers", action="store_true", help="show response trailers"
396
+ )
397
+ parser.add_argument(
398
+ "--show-status",
399
+ action="store_true",
400
+ help="show final status and counts",
401
+ )
402
+ parser.add_argument(
403
+ "--format-error",
404
+ action="store_true",
405
+ help="decode structured gRPC errors",
406
+ )
@@ -0,0 +1,115 @@
1
+ """Top-level command parsing and configuration assembly."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+ from collections.abc import Sequence
9
+
10
+ from ..commands.models import (
11
+ Command,
12
+ ConnectionOptions,
13
+ DiagnosticOptions,
14
+ InvokeOptions,
15
+ MetadataOptions,
16
+ ParsedTarget,
17
+ SchemaOptions,
18
+ TlsOptions,
19
+ )
20
+ from ..commands.operations import Operation
21
+ from .construction import build_invocation_parser, build_parser
22
+ from .resolution import resolve_command, resolve_transport_mode
23
+ from .validation import (
24
+ validate_common_options,
25
+ validate_invocation_options,
26
+ validate_operation_options,
27
+ )
28
+
29
+
30
+ def _build_command(
31
+ namespace: argparse.Namespace,
32
+ target: ParsedTarget,
33
+ operation: Operation,
34
+ invoke_options: InvokeOptions | None,
35
+ transport_mode: str,
36
+ ) -> Command:
37
+ verbosity = namespace.verbose
38
+ return Command(
39
+ target=target,
40
+ operation=operation,
41
+ schema=SchemaOptions(
42
+ proto=tuple(namespace.proto),
43
+ proto_path=tuple(namespace.proto_path),
44
+ protoset=tuple(namespace.protoset),
45
+ protoset_out=namespace.protoset_out,
46
+ use_reflection=namespace.use_reflection,
47
+ ),
48
+ transport=TlsOptions(
49
+ mode=transport_mode,
50
+ ca_paths=tuple(namespace.cacert),
51
+ cert=namespace.cert,
52
+ key=namespace.key,
53
+ key_password=namespace.key_password,
54
+ server_name=namespace.servername,
55
+ authority=namespace.authority,
56
+ ),
57
+ connection=ConnectionOptions(
58
+ connect_timeout=namespace.connect_timeout,
59
+ max_recv_message_size=namespace.max_recv_message_size,
60
+ max_send_message_size=namespace.max_send_message_size,
61
+ compression=namespace.compression,
62
+ keepalive_time=namespace.keepalive_time,
63
+ user_agent=namespace.user_agent,
64
+ ),
65
+ metadata=MetadataOptions(
66
+ header=tuple(namespace.header),
67
+ reflect_header=tuple(namespace.reflect_header),
68
+ rpc_header=tuple(getattr(namespace, "rpc_header", ())),
69
+ expand_headers=namespace.expand_headers,
70
+ ),
71
+ diagnostics=DiagnosticOptions(
72
+ show_headers=verbosity > 0 or namespace.show_headers,
73
+ show_trailers=verbosity > 0 or namespace.show_trailers,
74
+ show_status=verbosity > 0 or namespace.show_status,
75
+ verbosity=verbosity,
76
+ format_error=namespace.format_error or verbosity > 0,
77
+ diagnostic_format=namespace.diagnostic_format,
78
+ color=namespace.color,
79
+ ),
80
+ timeout=namespace.timeout,
81
+ invoke_options=invoke_options,
82
+ get_template=namespace.get_template,
83
+ template_depth=getattr(namespace, "template_depth", 8),
84
+ show_reflection=namespace.show_reflection,
85
+ )
86
+
87
+
88
+ def parse_arguments(argv: Sequence[str] | None = None) -> Command:
89
+ """Parse command-line arguments into validated command configuration."""
90
+ parser = build_parser()
91
+ arguments = list(sys.argv[1:] if argv is None else argv)
92
+ if not arguments or arguments[0] not in {"list", "describe"}:
93
+ # Invocation is the default mode. Discovery commands must still occupy
94
+ # argv[0], so `--option list ...` is never reinterpreted as discovery.
95
+ parser = build_invocation_parser()
96
+ namespace = parser.parse_args(arguments)
97
+ if not hasattr(namespace, "diagnostic_format"):
98
+ diagnostic_format = os.environ.get(
99
+ "PROTODIAL_DIAGNOSTIC_FORMAT", "human"
100
+ )
101
+ if diagnostic_format not in {"human", "json"}:
102
+ parser.error(
103
+ "PROTODIAL_DIAGNOSTIC_FORMAT must be one of: human, json"
104
+ )
105
+ namespace.diagnostic_format = diagnostic_format
106
+ if not hasattr(namespace, "color"):
107
+ namespace.color = "auto"
108
+ target, operation = resolve_command(parser, namespace)
109
+ invoke_options = validate_invocation_options(parser, namespace, operation)
110
+ validate_common_options(parser, namespace)
111
+ transport_mode = resolve_transport_mode(parser, namespace, target)
112
+ validate_operation_options(parser, namespace, operation)
113
+ return _build_command(
114
+ namespace, target, operation, invoke_options, transport_mode
115
+ )