cloudsmith-cli 1.20.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 (130) hide show
  1. cloudsmith_cli/__init__.py +10 -0
  2. cloudsmith_cli/__main__.py +8 -0
  3. cloudsmith_cli/cli/__init__.py +1 -0
  4. cloudsmith_cli/cli/command.py +160 -0
  5. cloudsmith_cli/cli/commands/__init__.py +33 -0
  6. cloudsmith_cli/cli/commands/auth.py +173 -0
  7. cloudsmith_cli/cli/commands/check.py +129 -0
  8. cloudsmith_cli/cli/commands/copy.py +98 -0
  9. cloudsmith_cli/cli/commands/credential_helper/__init__.py +39 -0
  10. cloudsmith_cli/cli/commands/credential_helper/docker.py +66 -0
  11. cloudsmith_cli/cli/commands/credential_helper/manage.py +299 -0
  12. cloudsmith_cli/cli/commands/delete.py +67 -0
  13. cloudsmith_cli/cli/commands/dependencies.py +108 -0
  14. cloudsmith_cli/cli/commands/docs.py +16 -0
  15. cloudsmith_cli/cli/commands/download.py +620 -0
  16. cloudsmith_cli/cli/commands/entitlements.py +819 -0
  17. cloudsmith_cli/cli/commands/help_.py +12 -0
  18. cloudsmith_cli/cli/commands/list_.py +317 -0
  19. cloudsmith_cli/cli/commands/login.py +99 -0
  20. cloudsmith_cli/cli/commands/logout.py +151 -0
  21. cloudsmith_cli/cli/commands/main.py +73 -0
  22. cloudsmith_cli/cli/commands/mcp.py +523 -0
  23. cloudsmith_cli/cli/commands/metadata.py +503 -0
  24. cloudsmith_cli/cli/commands/metrics/__init__.py +2 -0
  25. cloudsmith_cli/cli/commands/metrics/command.py +20 -0
  26. cloudsmith_cli/cli/commands/metrics/entitlements.py +148 -0
  27. cloudsmith_cli/cli/commands/metrics/packages.py +134 -0
  28. cloudsmith_cli/cli/commands/move.py +111 -0
  29. cloudsmith_cli/cli/commands/policy/__init__.py +3 -0
  30. cloudsmith_cli/cli/commands/policy/command.py +20 -0
  31. cloudsmith_cli/cli/commands/policy/deny.py +248 -0
  32. cloudsmith_cli/cli/commands/policy/license.py +335 -0
  33. cloudsmith_cli/cli/commands/policy/vulnerability.py +322 -0
  34. cloudsmith_cli/cli/commands/push.py +1323 -0
  35. cloudsmith_cli/cli/commands/quarantine.py +148 -0
  36. cloudsmith_cli/cli/commands/quota/__init__.py +2 -0
  37. cloudsmith_cli/cli/commands/quota/command.py +20 -0
  38. cloudsmith_cli/cli/commands/quota/history.py +122 -0
  39. cloudsmith_cli/cli/commands/quota/quota.py +107 -0
  40. cloudsmith_cli/cli/commands/repos.py +330 -0
  41. cloudsmith_cli/cli/commands/resync.py +90 -0
  42. cloudsmith_cli/cli/commands/status.py +92 -0
  43. cloudsmith_cli/cli/commands/tags.py +375 -0
  44. cloudsmith_cli/cli/commands/tokens.py +318 -0
  45. cloudsmith_cli/cli/commands/upstream.py +479 -0
  46. cloudsmith_cli/cli/commands/vulnerabilities.py +141 -0
  47. cloudsmith_cli/cli/commands/whoami.py +188 -0
  48. cloudsmith_cli/cli/config.py +635 -0
  49. cloudsmith_cli/cli/decorators.py +624 -0
  50. cloudsmith_cli/cli/exceptions.py +215 -0
  51. cloudsmith_cli/cli/metadata_common.py +146 -0
  52. cloudsmith_cli/cli/saml.py +109 -0
  53. cloudsmith_cli/cli/table.py +59 -0
  54. cloudsmith_cli/cli/types.py +14 -0
  55. cloudsmith_cli/cli/utils.py +267 -0
  56. cloudsmith_cli/cli/validators.py +378 -0
  57. cloudsmith_cli/cli/webserver.py +263 -0
  58. cloudsmith_cli/core/__init__.py +1 -0
  59. cloudsmith_cli/core/api/__init__.py +1 -0
  60. cloudsmith_cli/core/api/distros.py +31 -0
  61. cloudsmith_cli/core/api/entitlements.py +130 -0
  62. cloudsmith_cli/core/api/exceptions.py +57 -0
  63. cloudsmith_cli/core/api/files.py +131 -0
  64. cloudsmith_cli/core/api/init.py +109 -0
  65. cloudsmith_cli/core/api/metadata.py +217 -0
  66. cloudsmith_cli/core/api/metrics.py +78 -0
  67. cloudsmith_cli/core/api/orgs.py +201 -0
  68. cloudsmith_cli/core/api/packages.py +309 -0
  69. cloudsmith_cli/core/api/quota.py +64 -0
  70. cloudsmith_cli/core/api/rates.py +28 -0
  71. cloudsmith_cli/core/api/repos.py +81 -0
  72. cloudsmith_cli/core/api/status.py +27 -0
  73. cloudsmith_cli/core/api/upstreams.py +72 -0
  74. cloudsmith_cli/core/api/user.py +109 -0
  75. cloudsmith_cli/core/api/version.py +15 -0
  76. cloudsmith_cli/core/api/vulnerabilities.py +230 -0
  77. cloudsmith_cli/core/cache_utils.py +160 -0
  78. cloudsmith_cli/core/config.py +140 -0
  79. cloudsmith_cli/core/credentials/__init__.py +0 -0
  80. cloudsmith_cli/core/credentials/chain.py +69 -0
  81. cloudsmith_cli/core/credentials/models.py +44 -0
  82. cloudsmith_cli/core/credentials/oidc/__init__.py +6 -0
  83. cloudsmith_cli/core/credentials/oidc/cache.py +220 -0
  84. cloudsmith_cli/core/credentials/oidc/detectors/__init__.py +122 -0
  85. cloudsmith_cli/core/credentials/oidc/detectors/aws.py +85 -0
  86. cloudsmith_cli/core/credentials/oidc/detectors/azure_devops.py +70 -0
  87. cloudsmith_cli/core/credentials/oidc/detectors/base.py +26 -0
  88. cloudsmith_cli/core/credentials/oidc/detectors/bitbucket_pipelines.py +35 -0
  89. cloudsmith_cli/core/credentials/oidc/detectors/circleci.py +40 -0
  90. cloudsmith_cli/core/credentials/oidc/detectors/generic.py +42 -0
  91. cloudsmith_cli/core/credentials/oidc/detectors/github_actions.py +64 -0
  92. cloudsmith_cli/core/credentials/oidc/detectors/gitlab_ci.py +49 -0
  93. cloudsmith_cli/core/credentials/oidc/exchange.py +87 -0
  94. cloudsmith_cli/core/credentials/provider.py +17 -0
  95. cloudsmith_cli/core/credentials/providers/__init__.py +15 -0
  96. cloudsmith_cli/core/credentials/providers/cli_flag.py +24 -0
  97. cloudsmith_cli/core/credentials/providers/credentials_file.py +24 -0
  98. cloudsmith_cli/core/credentials/providers/env_var.py +24 -0
  99. cloudsmith_cli/core/credentials/providers/keyring_provider.py +59 -0
  100. cloudsmith_cli/core/credentials/providers/oidc_provider.py +115 -0
  101. cloudsmith_cli/core/download.py +594 -0
  102. cloudsmith_cli/core/keyring.py +171 -0
  103. cloudsmith_cli/core/mcp/__init__.py +0 -0
  104. cloudsmith_cli/core/mcp/data.py +17 -0
  105. cloudsmith_cli/core/mcp/server.py +786 -0
  106. cloudsmith_cli/core/pagination.py +131 -0
  107. cloudsmith_cli/core/ratelimits.py +88 -0
  108. cloudsmith_cli/core/rest.py +255 -0
  109. cloudsmith_cli/core/utils.py +95 -0
  110. cloudsmith_cli/core/version.py +20 -0
  111. cloudsmith_cli/credential_helpers/__init__.py +7 -0
  112. cloudsmith_cli/credential_helpers/backends.py +41 -0
  113. cloudsmith_cli/credential_helpers/common.py +111 -0
  114. cloudsmith_cli/credential_helpers/custom_domains.py +281 -0
  115. cloudsmith_cli/credential_helpers/docker/__init__.py +4 -0
  116. cloudsmith_cli/credential_helpers/docker/installer.py +349 -0
  117. cloudsmith_cli/credential_helpers/docker/runtime.py +117 -0
  118. cloudsmith_cli/credential_helpers/launchers.py +175 -0
  119. cloudsmith_cli/data/VERSION +1 -0
  120. cloudsmith_cli/data/config.ini +23 -0
  121. cloudsmith_cli/data/credentials.ini +14 -0
  122. cloudsmith_cli/templates/__init__.py +3 -0
  123. cloudsmith_cli/templates/auth_error.html +45 -0
  124. cloudsmith_cli/templates/auth_success.html +37 -0
  125. cloudsmith_cli-1.20.0.dist-info/METADATA +610 -0
  126. cloudsmith_cli-1.20.0.dist-info/RECORD +130 -0
  127. cloudsmith_cli-1.20.0.dist-info/WHEEL +5 -0
  128. cloudsmith_cli-1.20.0.dist-info/entry_points.txt +2 -0
  129. cloudsmith_cli-1.20.0.dist-info/licenses/LICENSE +201 -0
  130. cloudsmith_cli-1.20.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,375 @@
1
+ """CLI/Commands - List objects."""
2
+
3
+ from operator import itemgetter
4
+
5
+ import click
6
+
7
+ from ...core.api.packages import (
8
+ get_package_tags as api_get_package_tags,
9
+ tag_package as api_tag_package,
10
+ )
11
+ from .. import command, decorators, utils, validators
12
+ from ..exceptions import handle_api_exceptions
13
+ from ..utils import maybe_spinner
14
+ from .main import main
15
+
16
+
17
+ def _parse_tags(tags):
18
+ """Parse tags from CSV into a list."""
19
+ return [x.strip() for x in (tags or "").split(",")]
20
+
21
+
22
+ def _print_tags(opts, all_tags, all_immutable_tags):
23
+ """Print the tags for a package."""
24
+ all_combined_tags = {"tags": all_tags, "tags_immutable": all_immutable_tags}
25
+
26
+ if utils.maybe_print_as_json(opts, all_combined_tags):
27
+ return
28
+
29
+ headers = ["Tag", "Type", "Immutable"]
30
+
31
+ rows = []
32
+ for tag_type, tags in sorted(all_tags.items(), key=itemgetter(0)):
33
+ immutable_tags = all_immutable_tags.get(tag_type) or []
34
+ for tag in sorted(tags):
35
+ immutable = "Yes" if tag in immutable_tags else "No"
36
+ rows.append(
37
+ [
38
+ click.style(tag, fg="cyan"),
39
+ click.style(tag_type, fg="yellow"),
40
+ click.style(immutable, fg="magenta"),
41
+ ]
42
+ )
43
+
44
+ if all_tags:
45
+ click.echo()
46
+ utils.pretty_print_table(headers, rows)
47
+
48
+ click.echo()
49
+
50
+ num_results = len(rows)
51
+ list_suffix = "tag%s" % ("s" if num_results != 1 else "")
52
+ utils.pretty_print_list_info(num_results=num_results, suffix=list_suffix)
53
+
54
+
55
+ @main.group(name="tags", cls=command.AliasGroup, aliases=["tag"])
56
+ @decorators.common_cli_config_options
57
+ @decorators.common_cli_output_options
58
+ @decorators.common_api_auth_options
59
+ @decorators.initialise_api
60
+ @click.pass_context
61
+ def tags_(ctx, opts): # pylint: disable=unused-argument
62
+ """
63
+ Manage the tags for a package in a repository.
64
+
65
+ See the help for subcommands for more information on each.
66
+ """
67
+
68
+
69
+ @tags_.command(name="list", aliases=["ls"])
70
+ @decorators.common_cli_config_options
71
+ @decorators.common_cli_output_options
72
+ @decorators.common_api_auth_options
73
+ @decorators.initialise_api
74
+ @click.argument(
75
+ "owner_repo_package",
76
+ metavar="OWNER/REPO/PACKAGE",
77
+ callback=validators.validate_owner_repo_package,
78
+ )
79
+ @click.pass_context
80
+ def list_tags(ctx, opts, owner_repo_package):
81
+ """
82
+ List tags for a package in a repository.
83
+
84
+ This requires appropriate (read) permissions for the package.
85
+
86
+ - OWNER/REPO/PACKAGE: Specify the OWNER namespace (i.e. user or org), the
87
+ REPO name where the package is stored, and the PACKAGE identifier of the
88
+ package itself. All separated by a slash.
89
+
90
+ Example: 'your-org/awesome-repo/better-pkg'.
91
+
92
+ Full CLI example:
93
+
94
+ $ cloudsmith tags list your-org/awesome-repo/better-pkg
95
+ """
96
+ owner, repo, package = owner_repo_package
97
+
98
+ use_stderr = utils.should_use_stderr(opts)
99
+
100
+ click.echo(
101
+ "Listing tags for the '%(package)s' package ... "
102
+ % {"package": click.style(package, bold=True)},
103
+ nl=False,
104
+ err=use_stderr,
105
+ )
106
+
107
+ context_msg = "Failed to list tags for the package!"
108
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
109
+ with maybe_spinner(opts):
110
+ package_tags, package_tags_immutable = api_get_package_tags(
111
+ owner=owner, repo=repo, identifier=package
112
+ )
113
+
114
+ click.secho("OK", fg="green", err=use_stderr)
115
+
116
+ _print_tags(opts, package_tags, package_tags_immutable)
117
+
118
+
119
+ @tags_.command(name="add")
120
+ @decorators.common_cli_config_options
121
+ @decorators.common_cli_output_options
122
+ @decorators.common_api_auth_options
123
+ @decorators.initialise_api
124
+ @click.argument(
125
+ "owner_repo_package",
126
+ metavar="OWNER/REPO/PACKAGE",
127
+ callback=validators.validate_owner_repo_package,
128
+ )
129
+ @click.argument("tags", metavar="TAGS")
130
+ @click.option(
131
+ "--immutable",
132
+ default=False,
133
+ is_flag=True,
134
+ help=(
135
+ "If true, the tags created will be immutable (cannot be changed). In "
136
+ "practice, this means the tags cannot be (easily) deleted. A repository "
137
+ "admin can explicitly remove immutable tags."
138
+ ),
139
+ )
140
+ @click.pass_context
141
+ def add_tags(ctx, opts, owner_repo_package, tags, immutable):
142
+ """
143
+ Add tags to a package in a repository.
144
+
145
+ This requires appropriate (write) permissions for the package.
146
+
147
+ - OWNER/REPO/PACKAGE: Specify the OWNER namespace (i.e. user or org), the
148
+ REPO name where the package is stored, and the PACKAGE identifier of the
149
+ package itself. All separated by a slash.
150
+
151
+ Example: 'your-org/awesome-repo/better-pkg'.
152
+
153
+ - TAGS: A comma-separated value list of the tags you want to add.
154
+
155
+ Example: foo,bar
156
+
157
+ Full CLI example:
158
+
159
+ $ cloudsmith tags add your-org/awesome-repo/better-pkg foo,bar
160
+ """
161
+ owner, repo, package = owner_repo_package
162
+ tags = _parse_tags(tags)
163
+
164
+ use_stderr = utils.should_use_stderr(opts)
165
+
166
+ click.echo(
167
+ "Adding '%(tags)s' tag%(s)s to the '%(package)s' package ... "
168
+ % {
169
+ "package": click.style(package, bold=True),
170
+ "tags": click.style(", ".join(tags or [])),
171
+ "s": "s" if len(tags) != 1 else "",
172
+ },
173
+ nl=False,
174
+ err=use_stderr,
175
+ )
176
+
177
+ context_msg = "Failed to add tags to package!"
178
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
179
+ with maybe_spinner(opts):
180
+ package_tags, package_tags_immutable = api_tag_package(
181
+ owner=owner,
182
+ repo=repo,
183
+ identifier=package,
184
+ data={"action": "add", "tags": tags, "is_immutable": immutable},
185
+ )
186
+
187
+ click.secho("OK", fg="green", err=use_stderr)
188
+
189
+ _print_tags(opts, package_tags, package_tags_immutable)
190
+
191
+
192
+ @tags_.command(name="clear")
193
+ @decorators.common_cli_config_options
194
+ @decorators.common_cli_output_options
195
+ @decorators.common_api_auth_options
196
+ @decorators.initialise_api
197
+ @click.argument(
198
+ "owner_repo_package",
199
+ metavar="OWNER/REPO/PACKAGE",
200
+ callback=validators.validate_owner_repo_package,
201
+ )
202
+ @click.pass_context
203
+ def clear_tags(ctx, opts, owner_repo_package):
204
+ """
205
+ Clear all existing (non-immutable) tags from a package in a repository.
206
+
207
+ This requires appropriate (write) permissions for the package.
208
+
209
+ - OWNER/REPO/PACKAGE: Specify the OWNER namespace (i.e. user or org), the
210
+ REPO name where the package is stored, and the PACKAGE identifier of the
211
+ package itself. All separated by a slash.
212
+
213
+ Example: 'your-org/awesome-repo/better-pkg'.
214
+
215
+ Full CLI example:
216
+
217
+ $ cloudsmith tags clear your-org/awesome-repo/better-pkg
218
+ """
219
+ owner, repo, package = owner_repo_package
220
+
221
+ use_stderr = utils.should_use_stderr(opts)
222
+
223
+ click.echo(
224
+ "Clearing tags on the '%(package)s' package ... "
225
+ % {"package": click.style(package, bold=True)},
226
+ nl=False,
227
+ err=use_stderr,
228
+ )
229
+
230
+ context_msg = "Failed to clear tags on package!"
231
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
232
+ with maybe_spinner(opts):
233
+ package_tags, package_tags_immutable = api_tag_package(
234
+ owner=owner, repo=repo, identifier=package, data={"action": "clear"}
235
+ )
236
+
237
+ click.secho("OK", fg="green", err=use_stderr)
238
+
239
+ _print_tags(opts, package_tags, package_tags_immutable)
240
+
241
+
242
+ @tags_.command(name="remove", aliases=["rm"])
243
+ @decorators.common_cli_config_options
244
+ @decorators.common_cli_output_options
245
+ @decorators.common_api_auth_options
246
+ @decorators.initialise_api
247
+ @click.argument(
248
+ "owner_repo_package",
249
+ metavar="OWNER/REPO/PACKAGE",
250
+ callback=validators.validate_owner_repo_package,
251
+ )
252
+ @click.argument("tags", metavar="TAGS")
253
+ @click.pass_context
254
+ def remove_tags(ctx, opts, owner_repo_package, tags):
255
+ """
256
+ Remove tags from a package in a repository.
257
+
258
+ This requires appropriate (write) permissions for the package.
259
+
260
+ - OWNER/REPO/PACKAGE: Specify the OWNER namespace (i.e. user or org), the
261
+ REPO name where the package is stored, and the PACKAGE identifier of the
262
+ package itself. All separated by a slash.
263
+
264
+ Example: 'your-org/awesome-repo/better-pkg'.
265
+
266
+ - TAGS: A comma-separated value list of the tags you want to remove.
267
+
268
+ Example: foo,bar
269
+
270
+ Full CLI example:
271
+
272
+ $ cloudsmith tags remove your-org/awesome-repo/better-pkg foo,bar
273
+ """
274
+ owner, repo, package = owner_repo_package
275
+ tags = _parse_tags(tags)
276
+
277
+ use_stderr = utils.should_use_stderr(opts)
278
+
279
+ click.echo(
280
+ "Removing '%(tags)s' tag%(s)s from the '%(package)s' package ... "
281
+ % {
282
+ "package": click.style(package, bold=True),
283
+ "tags": click.style(", ".join(tags or [])),
284
+ "s": "s" if len(tags) != 1 else "",
285
+ },
286
+ nl=False,
287
+ err=use_stderr,
288
+ )
289
+
290
+ context_msg = "Failed to remove tags from package!"
291
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
292
+ with maybe_spinner(opts):
293
+ package_tags, package_tags_immutable = api_tag_package(
294
+ owner=owner,
295
+ repo=repo,
296
+ identifier=package,
297
+ data={"action": "remove", "tags": tags},
298
+ )
299
+
300
+ click.secho("OK", fg="green", err=use_stderr)
301
+
302
+ _print_tags(opts, package_tags, package_tags_immutable)
303
+
304
+
305
+ @tags_.command(name="replace")
306
+ @decorators.common_cli_config_options
307
+ @decorators.common_cli_output_options
308
+ @decorators.common_api_auth_options
309
+ @decorators.initialise_api
310
+ @click.argument(
311
+ "owner_repo_package",
312
+ metavar="OWNER/REPO/PACKAGE",
313
+ callback=validators.validate_owner_repo_package,
314
+ )
315
+ @click.argument("tags", metavar="TAGS")
316
+ @click.option(
317
+ "--immutable",
318
+ default=False,
319
+ is_flag=True,
320
+ help=(
321
+ "If true, the tags created will be immutable (cannot be changed). In "
322
+ "practice, this means the tags cannot be (easily) deleted. A repository "
323
+ "admin can explicitly remove immutable tags."
324
+ ),
325
+ )
326
+ @click.pass_context
327
+ def replace_tags(ctx, opts, owner_repo_package, tags, immutable):
328
+ """
329
+ Replace all existing (non-immutable) tags on a package in a repository.
330
+
331
+ This requires appropriate (write) permissions for the package.
332
+
333
+ - OWNER/REPO/PACKAGE: Specify the OWNER namespace (i.e. user or org), the
334
+ REPO name where the package is stored, and the PACKAGE identifier of the
335
+ package itself. All separated by a slash.
336
+
337
+ Example: 'your-org/awesome-repo/better-pkg'.
338
+
339
+ - TAGS: A comma-separated value list of the tags you want to replace existing with.
340
+
341
+ Example: foo,bar
342
+
343
+ Full CLI example:
344
+
345
+ $ cloudsmith tags replace your-org/awesome-repo/better-pkg foo,bar
346
+ """
347
+ owner, repo, package = owner_repo_package
348
+ tags = _parse_tags(tags)
349
+
350
+ use_stderr = utils.should_use_stderr(opts)
351
+
352
+ click.echo(
353
+ "Replacing existing with '%(tags)s' tag%(s)s on the '%(package)s' package ... "
354
+ % {
355
+ "package": click.style(package, bold=True),
356
+ "tags": click.style(", ".join(tags or [])),
357
+ "s": "s" if len(tags) != 1 else "",
358
+ },
359
+ nl=False,
360
+ err=use_stderr,
361
+ )
362
+
363
+ context_msg = "Failed to replace tags on package!"
364
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
365
+ with maybe_spinner(opts):
366
+ package_tags, package_tags_immutable = api_tag_package(
367
+ owner=owner,
368
+ repo=repo,
369
+ identifier=package,
370
+ data={"action": "replace", "tags": tags, "is_immutable": immutable},
371
+ )
372
+
373
+ click.secho("OK", fg="green", err=use_stderr)
374
+
375
+ _print_tags(opts, package_tags, package_tags_immutable)
@@ -0,0 +1,318 @@
1
+ import click
2
+
3
+ from ...core.api import exceptions, user as api
4
+ from ...core.config import create_config_files, new_config_messaging
5
+ from .. import command, decorators, utils
6
+ from ..exceptions import handle_api_exceptions
7
+ from .main import main
8
+
9
+
10
+ def handle_duplicate_token_error(exc, ctx, opts, save_config, force, json):
11
+ """
12
+ Handle the case where user already has a token.
13
+
14
+ Returns the token from refresh if user confirms, otherwise exits.
15
+ """
16
+ if (
17
+ exc.status == 400
18
+ and exc.detail
19
+ and "User has already created an API key" in exc.detail
20
+ ):
21
+ if not force:
22
+ if not click.confirm(
23
+ "User already has a token. Would you like to recreate it?",
24
+ abort=False,
25
+ err=json,
26
+ ):
27
+ return None
28
+ return refresh_existing_token_interactive(
29
+ ctx, opts, save_config=save_config, force=force, json=json
30
+ )
31
+ raise exc
32
+
33
+
34
+ def request_api_key(ctx, opts, save_config=False):
35
+ """
36
+ Request an API key non-interactively.
37
+
38
+ This function creates a new token or rotates an existing one without any prompts.
39
+ Used by the --request-api-key flag in the auth command.
40
+
41
+ Returns the token object on success.
42
+ Raises ApiException on failure.
43
+ """
44
+ context_msg = "Failed to retrieve API token!"
45
+
46
+ try:
47
+ # Don't use handle_api_exceptions here so we can catch and handle
48
+ # the "already has token" error ourselves
49
+ with utils.maybe_spinner(opts):
50
+ new_token = api.create_user_token_saml()
51
+
52
+ if save_config:
53
+ create, has_errors = create_config_files(
54
+ ctx, opts, api_key=new_token.key, force=True
55
+ )
56
+ new_config_messaging(has_errors, opts, create, api_key=new_token.key)
57
+
58
+ return new_token
59
+
60
+ except exceptions.ApiException as exc:
61
+ if exc.status == 401:
62
+ # Unauthorized - re-raise with handler
63
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
64
+ raise
65
+
66
+ if (
67
+ exc.status == 400
68
+ and exc.detail
69
+ and "User has already created an API key" in exc.detail
70
+ ):
71
+ # Token exists - rotate it automatically
72
+ click.echo(
73
+ "Warning: Rotating existing API token. Your old key will be invalidated.",
74
+ err=True,
75
+ )
76
+
77
+ # List tokens and select the first one
78
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
79
+ with utils.maybe_spinner(opts):
80
+ api_tokens = api.list_user_tokens()
81
+
82
+ if not api_tokens:
83
+ raise click.ClickException("No existing tokens found to rotate.")
84
+
85
+ token_slug = api_tokens[0].slug_perm
86
+
87
+ # Refresh the token
88
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
89
+ with utils.maybe_spinner(opts):
90
+ new_token = api.refresh_user_token(token_slug)
91
+
92
+ if save_config:
93
+ create, has_errors = create_config_files(
94
+ ctx, opts, api_key=new_token.key, force=True
95
+ )
96
+ new_config_messaging(has_errors, opts, create, api_key=new_token.key)
97
+
98
+ return new_token
99
+
100
+ # Other errors - use the handler
101
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
102
+ raise exc
103
+
104
+
105
+ @main.group(cls=command.AliasGroup, name="tokens")
106
+ @decorators.common_cli_config_options
107
+ @decorators.common_cli_output_options
108
+ @click.pass_context
109
+ def tokens(ctx, opts):
110
+ """Manage your user API tokens."""
111
+
112
+
113
+ @tokens.command(name="list", aliases=["ls"])
114
+ @decorators.common_cli_config_options
115
+ @decorators.common_cli_output_options
116
+ @decorators.common_api_auth_options
117
+ @decorators.initialise_api
118
+ @click.pass_context
119
+ def list_tokens(ctx, opts):
120
+ """List all user API tokens."""
121
+ use_stderr = utils.should_use_stderr(opts)
122
+
123
+ click.echo("Retrieving API tokens... ", nl=False, err=use_stderr)
124
+
125
+ context_msg = "Failed to retrieve API tokens!"
126
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
127
+ with utils.maybe_spinner(opts):
128
+ tokens = api.list_user_tokens()
129
+ click.secho("OK", fg="green", err=use_stderr)
130
+
131
+ if utils.maybe_print_as_json(opts, tokens):
132
+ return
133
+
134
+ print_tokens(tokens)
135
+
136
+
137
+ @tokens.command()
138
+ @click.option(
139
+ "--save-config",
140
+ default=False,
141
+ is_flag=True,
142
+ help="Save the new API key to your configuration files.",
143
+ )
144
+ @click.option(
145
+ "-f",
146
+ "--force",
147
+ default=False,
148
+ is_flag=True,
149
+ help="Force creation of user API token without prompts.",
150
+ )
151
+ @decorators.common_cli_config_options
152
+ @decorators.common_cli_output_options
153
+ @decorators.common_api_auth_options
154
+ @decorators.initialise_api
155
+ @click.pass_context
156
+ def create(ctx, opts, save_config, force=False, json=False):
157
+ """Create a new API token."""
158
+ new_token = _create(ctx, opts, save_config, force, json)
159
+
160
+ if save_config:
161
+ create, has_errors = create_config_files(
162
+ ctx, opts, api_key=new_token.key, force=force
163
+ )
164
+ new_config_messaging(has_errors, opts, create, api_key=new_token.key)
165
+
166
+
167
+ @tokens.command()
168
+ @click.argument(
169
+ "token_slug",
170
+ required=False,
171
+ )
172
+ @click.option(
173
+ "--save-config",
174
+ default=False,
175
+ is_flag=True,
176
+ help="Save the new API key to your configuration files.",
177
+ )
178
+ @click.option(
179
+ "-f",
180
+ "--force",
181
+ default=False,
182
+ is_flag=True,
183
+ help="Force refresh of user API token without prompts.",
184
+ )
185
+ @decorators.common_cli_config_options
186
+ @decorators.common_cli_output_options
187
+ @decorators.common_api_auth_options
188
+ @decorators.initialise_api
189
+ @click.pass_context
190
+ def refresh(ctx, opts, token_slug, force, save_config):
191
+ """Refresh a specific API token by its slug."""
192
+ new_token = refresh_existing_token_interactive(
193
+ ctx, opts, token_slug, save_config, force
194
+ )
195
+
196
+ if new_token:
197
+ if utils.maybe_print_as_json(opts, new_token):
198
+ return new_token
199
+
200
+
201
+ def print_tokens(tokens):
202
+ for token in tokens:
203
+ click.echo(
204
+ f"Token: {click.style(token.key, fg='magenta')}, "
205
+ f"Created: {click.style(token.created, fg='green')}, "
206
+ f"slug_perm: {click.style(token.slug_perm, fg='cyan')}"
207
+ )
208
+
209
+
210
+ def refresh_existing_token_interactive(
211
+ ctx, opts, token_slug=None, save_config=False, force=False, json=False
212
+ ):
213
+ """Refresh an existing API token with interactive token selection, or create new if none exist."""
214
+ json = json or utils.should_use_stderr(opts)
215
+ use_stderr = json
216
+ context_msg = "Failed to refresh the token!"
217
+
218
+ if not token_slug:
219
+ try:
220
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
221
+ api_tokens = api.list_user_tokens()
222
+ except exceptions.ApiException as exc:
223
+ # If we can't list tokens due to API error, fall back to creating a new one
224
+ if opts.debug:
225
+ click.echo(f"Debug: Failed to list tokens with error: {exc}", err=True)
226
+ click.echo(
227
+ "Unable to list existing tokens. Creating a new token instead...",
228
+ err=use_stderr,
229
+ )
230
+ return _create(ctx, opts, save_config=save_config, force=force, json=json)
231
+
232
+ if not api_tokens:
233
+ click.echo(
234
+ "No existing tokens found. Creating a new token instead...",
235
+ err=use_stderr,
236
+ )
237
+ return _create(ctx, opts, save_config=save_config, force=force, json=json)
238
+
239
+ if not json:
240
+ click.echo("Current tokens:")
241
+ print_tokens(api_tokens)
242
+
243
+ if not force:
244
+ token_slug = click.prompt(
245
+ "Please enter the slug_perm of the token you would like to refresh",
246
+ err=use_stderr,
247
+ )
248
+ else:
249
+ # Use the first available slug_perm when force is enabled
250
+ token_slug = api_tokens[0].slug_perm
251
+ if not json:
252
+ click.echo(f"Using token {token_slug} (first available)")
253
+
254
+ if not json:
255
+ click.echo(f"Refreshing token {token_slug}... ", nl=False)
256
+ else:
257
+ # In JSON mode, print info to stderr
258
+ click.echo(f"Refreshing token {token_slug}... ", nl=False, err=json)
259
+
260
+ try:
261
+ with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
262
+ with utils.maybe_spinner(opts):
263
+ new_token = api.refresh_user_token(token_slug)
264
+
265
+ if save_config:
266
+ create, has_errors = create_config_files(
267
+ ctx, opts, api_key=new_token.key, force=force
268
+ )
269
+ new_config_messaging(has_errors, opts, create, api_key=new_token.key)
270
+
271
+ if utils.maybe_print_as_json(opts, new_token):
272
+ return
273
+
274
+ if not json:
275
+ click.secho("OK", fg="green")
276
+ click.echo(f"New token value: {click.style(new_token.key, fg='magenta')}")
277
+
278
+ return new_token
279
+ except exceptions.ApiException as exc:
280
+ # If refresh fails due to API error, offer to create a new token instead
281
+ if opts.debug:
282
+ click.echo(f"\nDebug: Refresh failed with error: {exc}", err=True)
283
+ click.echo("\nRefresh failed. Creating a new token instead...", err=use_stderr)
284
+ return _create(ctx, opts, save_config=save_config, force=force, json=json)
285
+
286
+
287
+ def _create(ctx, opts, save_config=False, force=False, json=False):
288
+ """Create a new API token."""
289
+ json = json or utils.should_use_stderr(opts)
290
+
291
+ try:
292
+ new_token = api.create_user_token_saml()
293
+
294
+ if new_token:
295
+ # NOTE: This mutation is necessary during the deprecation period of the --json flag.
296
+ if json and opts.output not in ("json", "pretty_json"):
297
+ opts.output = "json"
298
+
299
+ if utils.maybe_print_as_json(opts, new_token):
300
+ return
301
+
302
+ click.echo(f"New token value: {click.style(new_token.key, fg='magenta')}")
303
+ return new_token
304
+
305
+ return new_token
306
+
307
+ except exceptions.ApiException as exc:
308
+ if exc.status == 401:
309
+ click.echo(f"{exc.detail}", err=True)
310
+ return
311
+ # NOTE: This mutation is necessary during the deprecation period of the --json flag.
312
+ if json and opts.output not in ("json", "pretty_json"):
313
+ opts.output = "json"
314
+ if exc.status == 400:
315
+ new_token = handle_duplicate_token_error(
316
+ exc, ctx, opts, save_config=save_config, force=force, json=json
317
+ )
318
+ return new_token