cloudsmith-cli 1.10.2__py2.py3-none-any.whl → 1.10.4__py2.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 (37) hide show
  1. cloudsmith_cli/cli/command.py +66 -0
  2. cloudsmith_cli/cli/commands/auth.py +25 -12
  3. cloudsmith_cli/cli/commands/check.py +18 -4
  4. cloudsmith_cli/cli/commands/copy.py +8 -2
  5. cloudsmith_cli/cli/commands/delete.py +9 -3
  6. cloudsmith_cli/cli/commands/dependencies.py +1 -1
  7. cloudsmith_cli/cli/commands/download.py +89 -8
  8. cloudsmith_cli/cli/commands/entitlements.py +11 -7
  9. cloudsmith_cli/cli/commands/list_.py +2 -2
  10. cloudsmith_cli/cli/commands/login.py +21 -13
  11. cloudsmith_cli/cli/commands/metrics/entitlements.py +2 -2
  12. cloudsmith_cli/cli/commands/metrics/packages.py +1 -1
  13. cloudsmith_cli/cli/commands/move.py +10 -3
  14. cloudsmith_cli/cli/commands/policy/deny.py +8 -6
  15. cloudsmith_cli/cli/commands/policy/license.py +5 -3
  16. cloudsmith_cli/cli/commands/policy/vulnerability.py +6 -3
  17. cloudsmith_cli/cli/commands/push.py +146 -64
  18. cloudsmith_cli/cli/commands/quarantine.py +7 -4
  19. cloudsmith_cli/cli/commands/quota/history.py +1 -1
  20. cloudsmith_cli/cli/commands/quota/quota.py +1 -1
  21. cloudsmith_cli/cli/commands/repos.py +5 -3
  22. cloudsmith_cli/cli/commands/resync.py +7 -2
  23. cloudsmith_cli/cli/commands/status.py +21 -3
  24. cloudsmith_cli/cli/commands/tags.py +20 -5
  25. cloudsmith_cli/cli/commands/tokens.py +37 -25
  26. cloudsmith_cli/cli/commands/upstream.py +2 -3
  27. cloudsmith_cli/cli/commands/whoami.py +4 -5
  28. cloudsmith_cli/cli/exceptions.py +86 -49
  29. cloudsmith_cli/cli/utils.py +17 -2
  30. cloudsmith_cli/cli/webserver.py +1 -1
  31. cloudsmith_cli/data/VERSION +1 -1
  32. {cloudsmith_cli-1.10.2.dist-info → cloudsmith_cli-1.10.4.dist-info}/METADATA +1 -1
  33. {cloudsmith_cli-1.10.2.dist-info → cloudsmith_cli-1.10.4.dist-info}/RECORD +37 -37
  34. {cloudsmith_cli-1.10.2.dist-info → cloudsmith_cli-1.10.4.dist-info}/WHEEL +0 -0
  35. {cloudsmith_cli-1.10.2.dist-info → cloudsmith_cli-1.10.4.dist-info}/entry_points.txt +0 -0
  36. {cloudsmith_cli-1.10.2.dist-info → cloudsmith_cli-1.10.4.dist-info}/licenses/LICENSE +0 -0
  37. {cloudsmith_cli-1.10.2.dist-info → cloudsmith_cli-1.10.4.dist-info}/top_level.txt +0 -0
@@ -3,7 +3,7 @@
3
3
  import click
4
4
 
5
5
  from ...core.api.packages import get_package_status
6
- from .. import decorators, validators
6
+ from .. import decorators, utils, validators
7
7
  from ..exceptions import handle_api_exceptions
8
8
  from ..utils import maybe_spinner
9
9
  from .main import main
@@ -32,6 +32,8 @@ def status(ctx, opts, owner_repo_package):
32
32
  """
33
33
  owner, repo, slug = owner_repo_package
34
34
 
35
+ use_stderr = utils.should_use_stderr(opts)
36
+
35
37
  click.echo(
36
38
  "Getting status of %(package)s in %(owner)s/%(repo)s ... "
37
39
  % {
@@ -40,6 +42,7 @@ def status(ctx, opts, owner_repo_package):
40
42
  "package": click.style(slug, bold=True),
41
43
  },
42
44
  nl=False,
45
+ err=use_stderr,
43
46
  )
44
47
 
45
48
  context_msg = "Failed to get status of package!"
@@ -48,7 +51,7 @@ def status(ctx, opts, owner_repo_package):
48
51
  res = get_package_status(owner, repo, slug)
49
52
  ok, failed, _, status_str, stage_str, reason = res
50
53
 
51
- click.secho("OK", fg="green")
54
+ click.secho("OK", fg="green", err=use_stderr)
52
55
 
53
56
  if not stage_str:
54
57
  package_status = status_str
@@ -62,13 +65,28 @@ def status(ctx, opts, owner_repo_package):
62
65
  else:
63
66
  status_colour = "magenta"
64
67
 
68
+ if utils.maybe_print_as_json(
69
+ opts,
70
+ {
71
+ "ok": ok,
72
+ "failed": failed,
73
+ "status": status_str,
74
+ "stage": stage_str,
75
+ "reason": reason,
76
+ "slug": slug,
77
+ },
78
+ ):
79
+ return
80
+
65
81
  click.secho(
66
82
  "The package status is: %(status)s"
67
- % {"status": click.style(package_status, fg=status_colour)}
83
+ % {"status": click.style(package_status, fg=status_colour)},
84
+ err=use_stderr,
68
85
  )
69
86
 
70
87
  if reason:
71
88
  click.secho(
72
89
  f"Reason given: {click.style(reason, fg='yellow')}",
73
90
  fg=status_colour,
91
+ err=use_stderr,
74
92
  )
@@ -95,10 +95,13 @@ def list_tags(ctx, opts, owner_repo_package):
95
95
  """
96
96
  owner, repo, package = owner_repo_package
97
97
 
98
+ use_stderr = utils.should_use_stderr(opts)
99
+
98
100
  click.echo(
99
101
  "Listing tags for the '%(package)s' package ... "
100
102
  % {"package": click.style(package, bold=True)},
101
103
  nl=False,
104
+ err=use_stderr,
102
105
  )
103
106
 
104
107
  context_msg = "Failed to list tags for the package!"
@@ -108,7 +111,7 @@ def list_tags(ctx, opts, owner_repo_package):
108
111
  owner=owner, repo=repo, identifier=package
109
112
  )
110
113
 
111
- click.secho("OK", fg="green")
114
+ click.secho("OK", fg="green", err=use_stderr)
112
115
 
113
116
  _print_tags(opts, package_tags, package_tags_immutable)
114
117
 
@@ -158,6 +161,8 @@ def add_tags(ctx, opts, owner_repo_package, tags, immutable):
158
161
  owner, repo, package = owner_repo_package
159
162
  tags = _parse_tags(tags)
160
163
 
164
+ use_stderr = utils.should_use_stderr(opts)
165
+
161
166
  click.echo(
162
167
  "Adding '%(tags)s' tag%(s)s to the '%(package)s' package ... "
163
168
  % {
@@ -166,6 +171,7 @@ def add_tags(ctx, opts, owner_repo_package, tags, immutable):
166
171
  "s": "s" if len(tags) != 1 else "",
167
172
  },
168
173
  nl=False,
174
+ err=use_stderr,
169
175
  )
170
176
 
171
177
  context_msg = "Failed to add tags to package!"
@@ -178,7 +184,7 @@ def add_tags(ctx, opts, owner_repo_package, tags, immutable):
178
184
  data={"action": "add", "tags": tags, "is_immutable": immutable},
179
185
  )
180
186
 
181
- click.secho("OK", fg="green")
187
+ click.secho("OK", fg="green", err=use_stderr)
182
188
 
183
189
  _print_tags(opts, package_tags, package_tags_immutable)
184
190
 
@@ -212,10 +218,13 @@ def clear_tags(ctx, opts, owner_repo_package):
212
218
  """
213
219
  owner, repo, package = owner_repo_package
214
220
 
221
+ use_stderr = utils.should_use_stderr(opts)
222
+
215
223
  click.echo(
216
224
  "Clearing tags on the '%(package)s' package ... "
217
225
  % {"package": click.style(package, bold=True)},
218
226
  nl=False,
227
+ err=use_stderr,
219
228
  )
220
229
 
221
230
  context_msg = "Failed to clear tags on package!"
@@ -225,7 +234,7 @@ def clear_tags(ctx, opts, owner_repo_package):
225
234
  owner=owner, repo=repo, identifier=package, data={"action": "clear"}
226
235
  )
227
236
 
228
- click.secho("OK", fg="green")
237
+ click.secho("OK", fg="green", err=use_stderr)
229
238
 
230
239
  _print_tags(opts, package_tags, package_tags_immutable)
231
240
 
@@ -265,6 +274,8 @@ def remove_tags(ctx, opts, owner_repo_package, tags):
265
274
  owner, repo, package = owner_repo_package
266
275
  tags = _parse_tags(tags)
267
276
 
277
+ use_stderr = utils.should_use_stderr(opts)
278
+
268
279
  click.echo(
269
280
  "Removing '%(tags)s' tag%(s)s from the '%(package)s' package ... "
270
281
  % {
@@ -273,6 +284,7 @@ def remove_tags(ctx, opts, owner_repo_package, tags):
273
284
  "s": "s" if len(tags) != 1 else "",
274
285
  },
275
286
  nl=False,
287
+ err=use_stderr,
276
288
  )
277
289
 
278
290
  context_msg = "Failed to remove tags from package!"
@@ -285,7 +297,7 @@ def remove_tags(ctx, opts, owner_repo_package, tags):
285
297
  data={"action": "remove", "tags": tags},
286
298
  )
287
299
 
288
- click.secho("OK", fg="green")
300
+ click.secho("OK", fg="green", err=use_stderr)
289
301
 
290
302
  _print_tags(opts, package_tags, package_tags_immutable)
291
303
 
@@ -335,6 +347,8 @@ def replace_tags(ctx, opts, owner_repo_package, tags, immutable):
335
347
  owner, repo, package = owner_repo_package
336
348
  tags = _parse_tags(tags)
337
349
 
350
+ use_stderr = utils.should_use_stderr(opts)
351
+
338
352
  click.echo(
339
353
  "Replacing existing with '%(tags)s' tag%(s)s on the '%(package)s' package ... "
340
354
  % {
@@ -343,6 +357,7 @@ def replace_tags(ctx, opts, owner_repo_package, tags, immutable):
343
357
  "s": "s" if len(tags) != 1 else "",
344
358
  },
345
359
  nl=False,
360
+ err=use_stderr,
346
361
  )
347
362
 
348
363
  context_msg = "Failed to replace tags on package!"
@@ -355,6 +370,6 @@ def replace_tags(ctx, opts, owner_repo_package, tags, immutable):
355
370
  data={"action": "replace", "tags": tags, "is_immutable": immutable},
356
371
  )
357
372
 
358
- click.secho("OK", fg="green")
373
+ click.secho("OK", fg="green", err=use_stderr)
359
374
 
360
375
  _print_tags(opts, package_tags, package_tags_immutable)
@@ -2,9 +2,8 @@ import click
2
2
 
3
3
  from ...core.api import exceptions, user as api
4
4
  from ...core.config import create_config_files, new_config_messaging
5
- from .. import command, decorators
5
+ from .. import command, decorators, utils
6
6
  from ..exceptions import handle_api_exceptions
7
- from ..utils import maybe_print_as_json, maybe_spinner
8
7
  from .main import main
9
8
 
10
9
 
@@ -23,6 +22,7 @@ def handle_duplicate_token_error(exc, ctx, opts, save_config, force, json):
23
22
  if not click.confirm(
24
23
  "User already has a token. Would you like to recreate it?",
25
24
  abort=False,
25
+ err=json,
26
26
  ):
27
27
  return None
28
28
  return refresh_existing_token_interactive(
@@ -47,17 +47,17 @@ def tokens(ctx, opts):
47
47
  @click.pass_context
48
48
  def list_tokens(ctx, opts):
49
49
  """List all user API tokens."""
50
- use_stderr = opts.output in ("json", "pretty_json")
50
+ use_stderr = utils.should_use_stderr(opts)
51
51
 
52
52
  click.echo("Retrieving API tokens... ", nl=False, err=use_stderr)
53
53
 
54
54
  context_msg = "Failed to retrieve API tokens!"
55
55
  with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
56
- with maybe_spinner(opts):
56
+ with utils.maybe_spinner(opts):
57
57
  tokens = api.list_user_tokens()
58
58
  click.secho("OK", fg="green", err=use_stderr)
59
59
 
60
- if maybe_print_as_json(opts, tokens):
60
+ if utils.maybe_print_as_json(opts, tokens):
61
61
  return
62
62
 
63
63
  print_tokens(tokens)
@@ -123,7 +123,7 @@ def refresh(ctx, opts, token_slug, force, save_config):
123
123
  )
124
124
 
125
125
  if new_token:
126
- if maybe_print_as_json(opts, new_token):
126
+ if utils.maybe_print_as_json(opts, new_token):
127
127
  return new_token
128
128
 
129
129
 
@@ -140,6 +140,8 @@ def refresh_existing_token_interactive(
140
140
  ctx, opts, token_slug=None, save_config=False, force=False, json=False
141
141
  ):
142
142
  """Refresh an existing API token with interactive token selection, or create new if none exist."""
143
+ json = json or utils.should_use_stderr(opts)
144
+ use_stderr = json
143
145
  context_msg = "Failed to refresh the token!"
144
146
 
145
147
  if not token_slug:
@@ -151,13 +153,17 @@ def refresh_existing_token_interactive(
151
153
  if opts.debug:
152
154
  click.echo(f"Debug: Failed to list tokens with error: {exc}", err=True)
153
155
  click.echo(
154
- "Unable to list existing tokens. Creating a new token instead..."
156
+ "Unable to list existing tokens. Creating a new token instead...",
157
+ err=use_stderr,
155
158
  )
156
- return _create(ctx, opts, save_config=save_config, force=force)
159
+ return _create(ctx, opts, save_config=save_config, force=force, json=json)
157
160
 
158
161
  if not api_tokens:
159
- click.echo("No existing tokens found. Creating a new token instead...")
160
- return _create(ctx, opts, save_config=save_config, force=force)
162
+ click.echo(
163
+ "No existing tokens found. Creating a new token instead...",
164
+ err=use_stderr,
165
+ )
166
+ return _create(ctx, opts, save_config=save_config, force=force, json=json)
161
167
 
162
168
  if not json:
163
169
  click.echo("Current tokens:")
@@ -165,7 +171,8 @@ def refresh_existing_token_interactive(
165
171
 
166
172
  if not force:
167
173
  token_slug = click.prompt(
168
- "Please enter the slug_perm of the token you would like to refresh"
174
+ "Please enter the slug_perm of the token you would like to refresh",
175
+ err=use_stderr,
169
176
  )
170
177
  else:
171
178
  # Use the first available slug_perm when force is enabled
@@ -175,9 +182,13 @@ def refresh_existing_token_interactive(
175
182
 
176
183
  if not json:
177
184
  click.echo(f"Refreshing token {token_slug}... ", nl=False)
185
+ else:
186
+ # In JSON mode, print info to stderr
187
+ click.echo(f"Refreshing token {token_slug}... ", nl=False, err=json)
188
+
178
189
  try:
179
190
  with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
180
- with maybe_spinner(opts):
191
+ with utils.maybe_spinner(opts):
181
192
  new_token = api.refresh_user_token(token_slug)
182
193
 
183
194
  if save_config:
@@ -186,7 +197,7 @@ def refresh_existing_token_interactive(
186
197
  )
187
198
  new_config_messaging(has_errors, opts, create, api_key=new_token.key)
188
199
 
189
- if maybe_print_as_json(opts, new_token):
200
+ if utils.maybe_print_as_json(opts, new_token):
190
201
  return
191
202
 
192
203
  if not json:
@@ -198,35 +209,36 @@ def refresh_existing_token_interactive(
198
209
  # If refresh fails due to API error, offer to create a new token instead
199
210
  if opts.debug:
200
211
  click.echo(f"\nDebug: Refresh failed with error: {exc}", err=True)
201
- click.echo("\nRefresh failed. Creating a new token instead...")
202
- return _create(ctx, opts, save_config=save_config, force=force)
212
+ click.echo("\nRefresh failed. Creating a new token instead...", err=use_stderr)
213
+ return _create(ctx, opts, save_config=save_config, force=force, json=json)
203
214
 
204
215
 
205
216
  def _create(ctx, opts, save_config=False, force=False, json=False):
206
217
  """Create a new API token."""
218
+ json = json or utils.should_use_stderr(opts)
219
+
207
220
  try:
208
221
  new_token = api.create_user_token_saml()
209
222
 
210
223
  if new_token:
211
- if json:
224
+ # NOTE: This mutation is necessary during the deprecation period of the --json flag.
225
+ if json and opts.output not in ("json", "pretty_json"):
212
226
  opts.output = "json"
213
- if maybe_print_as_json(opts, new_token):
214
- return
215
- if maybe_print_as_json(opts, new_token):
227
+
228
+ if utils.maybe_print_as_json(opts, new_token):
216
229
  return
217
- if not json:
218
- click.echo(
219
- f"New token value: {click.style(new_token.key, fg='magenta')}"
220
- )
230
+
231
+ click.echo(f"New token value: {click.style(new_token.key, fg='magenta')}")
221
232
  return new_token
222
233
 
223
234
  return new_token
224
235
 
225
236
  except exceptions.ApiException as exc:
226
237
  if exc.status == 401:
227
- click.echo(f"{exc.detail}")
238
+ click.echo(f"{exc.detail}", err=True)
228
239
  return
229
- if json:
240
+ # NOTE: This mutation is necessary during the deprecation period of the --json flag.
241
+ if json and opts.output not in ("json", "pretty_json"):
230
242
  opts.output = "json"
231
243
  if exc.status == 400:
232
244
  new_token = handle_duplicate_token_error(
@@ -173,8 +173,7 @@ def build_upstream_list_command(upstream_fmt):
173
173
  def func(ctx, opts, owner_repo, page, page_size, page_all):
174
174
  owner, repo = owner_repo
175
175
 
176
- # Use stderr for messages if the output is something else (e.g. # JSON)
177
- use_stderr = opts.output != "pretty"
176
+ use_stderr = utils.should_use_stderr(opts)
178
177
 
179
178
  if not use_stderr:
180
179
  click.echo("Getting upstreams... ", nl=False, err=use_stderr)
@@ -418,7 +417,7 @@ def build_upstream_delete_command(upstream_fmt):
418
417
  "delete the %(slug_perm)s upstream from the %(owner)s/%(repo)s repository"
419
418
  % delete_args
420
419
  )
421
- if not utils.confirm_operation(prompt, assume_yes=yes):
420
+ if not utils.confirm_operation(prompt, assume_yes=yes, err=use_stderr):
422
421
  return
423
422
 
424
423
  if not use_stderr:
@@ -3,9 +3,8 @@
3
3
  import click
4
4
 
5
5
  from ...core.api.user import get_user_brief
6
- from .. import decorators
6
+ from .. import decorators, utils
7
7
  from ..exceptions import handle_api_exceptions
8
- from ..utils import maybe_print_as_json, maybe_spinner
9
8
  from .main import main
10
9
 
11
10
 
@@ -17,7 +16,7 @@ from .main import main
17
16
  @click.pass_context
18
17
  def whoami(ctx, opts):
19
18
  """Retrieve your current authentication status."""
20
- use_stderr = opts.output in ("json", "pretty_json")
19
+ use_stderr = utils.should_use_stderr(opts)
21
20
 
22
21
  click.echo(
23
22
  "Retrieving your authentication status from the API ... ",
@@ -27,7 +26,7 @@ def whoami(ctx, opts):
27
26
 
28
27
  context_msg = "Failed to retrieve your authentication status!"
29
28
  with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
30
- with maybe_spinner(opts):
29
+ with utils.maybe_spinner(opts):
31
30
  is_auth, username, email, name = get_user_brief()
32
31
  click.secho("OK", fg="green", err=use_stderr)
33
32
 
@@ -38,7 +37,7 @@ def whoami(ctx, opts):
38
37
  "name": name,
39
38
  }
40
39
 
41
- if maybe_print_as_json(opts, data):
40
+ if utils.maybe_print_as_json(opts, data):
42
41
  return
43
42
 
44
43
  click.echo("You are authenticated as:")
@@ -16,67 +16,103 @@ def handle_api_exceptions(
16
16
  ):
17
17
  """Context manager that handles API exceptions."""
18
18
  # flake8: ignore=C901
19
+
19
20
  # Use stderr for messages if the output is something else (e.g. # JSON)
20
- use_stderr = opts.output != "pretty"
21
+ is_json_output = getattr(opts, "output", None) in ("json", "pretty_json")
22
+ use_stderr = is_json_output
21
23
 
22
24
  try:
23
25
  yield
24
26
  except ApiException as exc:
25
- if nl:
26
- click.echo(err=use_stderr)
27
- click.secho("ERROR: ", fg="red", nl=False, err=use_stderr)
28
- else:
29
- click.secho("ERROR", fg="red", err=use_stderr)
30
-
31
27
  context_msg = context_msg or "Failed to perform operation!"
32
- click.secho(
33
- "%(context)s (status: %(code)s - %(code_text)s)"
34
- % {
35
- "context": context_msg,
36
- "code": exc.status,
37
- "code_text": exc.status_description,
38
- },
39
- fg="red",
40
- err=use_stderr,
41
- )
42
-
43
28
  detail, fields = get_details(exc)
44
- if detail or fields:
45
- click.echo(err=use_stderr)
46
-
47
- if detail:
48
- click.secho(
49
- "Detail: %(detail)s"
50
- % {"detail": click.style(detail, fg="red", bold=False)},
51
- bold=True,
52
- err=use_stderr,
53
- )
29
+ hint = get_error_hint(ctx, opts, exc)
30
+
31
+ if is_json_output:
32
+ # Construct JSON error object
33
+ error_data = {
34
+ "detail": detail or exc.status_description,
35
+ "help": {
36
+ "context": context_msg,
37
+ "hint": hint,
38
+ },
39
+ "meta": {
40
+ "code": exc.status,
41
+ "description": exc.status_description,
42
+ },
43
+ }
54
44
 
55
45
  if fields:
56
- for k, v in fields.items():
57
- field = "%s Field" % k.capitalize()
58
- click.secho(
59
- "%(field)s: %(message)s"
60
- % {
61
- "field": click.style(field, bold=True),
62
- "message": click.style(v, fg="red"),
63
- },
64
- err=use_stderr,
65
- )
46
+ error_data["fields"] = fields
47
+
48
+ # Print to stdout
49
+ import json
66
50
 
67
- hint = get_error_hint(ctx, opts, exc)
68
- if hint:
69
51
  click.echo(
70
- f"Hint: {click.style(hint, fg='yellow')}",
52
+ json.dumps(
53
+ error_data, indent=4 if opts.output == "pretty_json" else None
54
+ )
55
+ )
56
+
57
+ else:
58
+ # Standard CLI output to stderr (or interleaved if output != pretty, but we force use_stderr now)
59
+ if nl:
60
+ click.echo(err=use_stderr)
61
+ click.secho("ERROR: ", fg="red", nl=False, err=use_stderr)
62
+ else:
63
+ click.secho("ERROR", fg="red", err=use_stderr)
64
+
65
+ click.secho(
66
+ "%(context)s (status: %(code)s - %(code_text)s)"
67
+ % {
68
+ "context": context_msg,
69
+ "code": exc.status,
70
+ "code_text": exc.status_description,
71
+ },
72
+ fg="red",
71
73
  err=use_stderr,
72
74
  )
73
75
 
74
- if opts.verbose and not opts.debug:
75
- if exc.headers:
76
+ if detail or fields:
76
77
  click.echo(err=use_stderr)
77
- click.echo("Headers in Reply:", err=use_stderr)
78
- for k, v in exc.headers.items():
79
- click.echo(f"{k} = {v}", err=use_stderr)
78
+
79
+ if detail:
80
+ click.secho(
81
+ "Detail: %(detail)s"
82
+ % {"detail": click.style(detail, fg="red", bold=False)},
83
+ bold=True,
84
+ err=use_stderr,
85
+ )
86
+
87
+ if fields:
88
+ for k, v in fields.items():
89
+ field = "%s Field" % k.capitalize()
90
+
91
+ # Flatten list/tuple error messages for text output
92
+ if isinstance(v, (list, tuple)):
93
+ v = " ".join(v)
94
+
95
+ click.secho(
96
+ "%(field)s: %(message)s"
97
+ % {
98
+ "field": click.style(field, bold=True),
99
+ "message": click.style(v, fg="red"),
100
+ },
101
+ err=use_stderr,
102
+ )
103
+
104
+ if hint:
105
+ click.echo(
106
+ f"Hint: {click.style(hint, fg='yellow')}",
107
+ err=use_stderr,
108
+ )
109
+
110
+ if opts.verbose and not opts.debug:
111
+ if exc.headers:
112
+ click.echo(err=use_stderr)
113
+ click.echo("Headers in Reply:", err=use_stderr)
114
+ for k, v in exc.headers.items():
115
+ click.echo(f"{k} = {v}", err=use_stderr)
80
116
 
81
117
  if reraise_on_error:
82
118
  raise
@@ -100,10 +136,11 @@ def get_details(exc):
100
136
  except (TypeError, KeyError):
101
137
  field_detail = v
102
138
 
103
- if isinstance(field_detail, (list, tuple)):
104
- field_detail = " ".join(field_detail)
105
-
106
139
  if k == "non_field_errors":
140
+ # Ensure we handle list/tuple for non_field_errors details joining
141
+ if isinstance(field_detail, (list, tuple)):
142
+ field_detail = " ".join(field_detail)
143
+
107
144
  if detail:
108
145
  detail += " " + field_detail
109
146
  else:
@@ -191,10 +191,25 @@ def confirm_operation(prompt, prefix=None, assume_yes=False, err=False):
191
191
 
192
192
  @contextmanager
193
193
  def maybe_spinner(opts):
194
- """Only activate the spinner if not in debug mode."""
195
- if opts.debug:
194
+ """Only activate the spinner if not in debug mode or using json output."""
195
+ if should_use_stderr(opts) or get_output_format(opts) in ("json", "pretty_json"):
196
196
  # No spinner
197
197
  yield
198
198
  else:
199
199
  with spinner() as spin:
200
200
  yield spin
201
+
202
+
203
+ def get_output_format(opts):
204
+ """Get the output format from opts."""
205
+ return getattr(opts, "output", None)
206
+
207
+
208
+ def should_use_stderr(opts):
209
+ """Check if stdout should be avoided for informational messages."""
210
+ return get_output_format(opts) in ("json", "pretty_json")
211
+
212
+
213
+ def maybe_print_status_json(opts, status_dict):
214
+ """Maybe print a status dict as JSON."""
215
+ return maybe_print_as_json(opts, status_dict)
@@ -214,7 +214,7 @@ class AuthenticationWebRequestHandler(BaseHTTPRequestHandler):
214
214
 
215
215
  if two_factor_token:
216
216
  totp_token = click.prompt(
217
- "Please enter your 2FA token", hide_input=True, type=str
217
+ "Please enter your 2FA token", hide_input=True, type=str, err=True
218
218
  )
219
219
 
220
220
  access_token, refresh_token = exchange_2fa_token(
@@ -1 +1 @@
1
- 1.10.2
1
+ 1.10.4
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudsmith-cli
3
- Version: 1.10.2
3
+ Version: 1.10.4
4
4
  Summary: Cloudsmith Command-Line Interface (CLI)
5
5
  Home-page: https://github.com/cloudsmith-io/cloudsmith-cli
6
6
  Author: Cloudsmith Ltd