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.
- cloudsmith_cli/__init__.py +10 -0
- cloudsmith_cli/__main__.py +8 -0
- cloudsmith_cli/cli/__init__.py +1 -0
- cloudsmith_cli/cli/command.py +160 -0
- cloudsmith_cli/cli/commands/__init__.py +33 -0
- cloudsmith_cli/cli/commands/auth.py +173 -0
- cloudsmith_cli/cli/commands/check.py +129 -0
- cloudsmith_cli/cli/commands/copy.py +98 -0
- cloudsmith_cli/cli/commands/credential_helper/__init__.py +39 -0
- cloudsmith_cli/cli/commands/credential_helper/docker.py +66 -0
- cloudsmith_cli/cli/commands/credential_helper/manage.py +299 -0
- cloudsmith_cli/cli/commands/delete.py +67 -0
- cloudsmith_cli/cli/commands/dependencies.py +108 -0
- cloudsmith_cli/cli/commands/docs.py +16 -0
- cloudsmith_cli/cli/commands/download.py +620 -0
- cloudsmith_cli/cli/commands/entitlements.py +819 -0
- cloudsmith_cli/cli/commands/help_.py +12 -0
- cloudsmith_cli/cli/commands/list_.py +317 -0
- cloudsmith_cli/cli/commands/login.py +99 -0
- cloudsmith_cli/cli/commands/logout.py +151 -0
- cloudsmith_cli/cli/commands/main.py +73 -0
- cloudsmith_cli/cli/commands/mcp.py +523 -0
- cloudsmith_cli/cli/commands/metadata.py +503 -0
- cloudsmith_cli/cli/commands/metrics/__init__.py +2 -0
- cloudsmith_cli/cli/commands/metrics/command.py +20 -0
- cloudsmith_cli/cli/commands/metrics/entitlements.py +148 -0
- cloudsmith_cli/cli/commands/metrics/packages.py +134 -0
- cloudsmith_cli/cli/commands/move.py +111 -0
- cloudsmith_cli/cli/commands/policy/__init__.py +3 -0
- cloudsmith_cli/cli/commands/policy/command.py +20 -0
- cloudsmith_cli/cli/commands/policy/deny.py +248 -0
- cloudsmith_cli/cli/commands/policy/license.py +335 -0
- cloudsmith_cli/cli/commands/policy/vulnerability.py +322 -0
- cloudsmith_cli/cli/commands/push.py +1323 -0
- cloudsmith_cli/cli/commands/quarantine.py +148 -0
- cloudsmith_cli/cli/commands/quota/__init__.py +2 -0
- cloudsmith_cli/cli/commands/quota/command.py +20 -0
- cloudsmith_cli/cli/commands/quota/history.py +122 -0
- cloudsmith_cli/cli/commands/quota/quota.py +107 -0
- cloudsmith_cli/cli/commands/repos.py +330 -0
- cloudsmith_cli/cli/commands/resync.py +90 -0
- cloudsmith_cli/cli/commands/status.py +92 -0
- cloudsmith_cli/cli/commands/tags.py +375 -0
- cloudsmith_cli/cli/commands/tokens.py +318 -0
- cloudsmith_cli/cli/commands/upstream.py +479 -0
- cloudsmith_cli/cli/commands/vulnerabilities.py +141 -0
- cloudsmith_cli/cli/commands/whoami.py +188 -0
- cloudsmith_cli/cli/config.py +635 -0
- cloudsmith_cli/cli/decorators.py +624 -0
- cloudsmith_cli/cli/exceptions.py +215 -0
- cloudsmith_cli/cli/metadata_common.py +146 -0
- cloudsmith_cli/cli/saml.py +109 -0
- cloudsmith_cli/cli/table.py +59 -0
- cloudsmith_cli/cli/types.py +14 -0
- cloudsmith_cli/cli/utils.py +267 -0
- cloudsmith_cli/cli/validators.py +378 -0
- cloudsmith_cli/cli/webserver.py +263 -0
- cloudsmith_cli/core/__init__.py +1 -0
- cloudsmith_cli/core/api/__init__.py +1 -0
- cloudsmith_cli/core/api/distros.py +31 -0
- cloudsmith_cli/core/api/entitlements.py +130 -0
- cloudsmith_cli/core/api/exceptions.py +57 -0
- cloudsmith_cli/core/api/files.py +131 -0
- cloudsmith_cli/core/api/init.py +109 -0
- cloudsmith_cli/core/api/metadata.py +217 -0
- cloudsmith_cli/core/api/metrics.py +78 -0
- cloudsmith_cli/core/api/orgs.py +201 -0
- cloudsmith_cli/core/api/packages.py +309 -0
- cloudsmith_cli/core/api/quota.py +64 -0
- cloudsmith_cli/core/api/rates.py +28 -0
- cloudsmith_cli/core/api/repos.py +81 -0
- cloudsmith_cli/core/api/status.py +27 -0
- cloudsmith_cli/core/api/upstreams.py +72 -0
- cloudsmith_cli/core/api/user.py +109 -0
- cloudsmith_cli/core/api/version.py +15 -0
- cloudsmith_cli/core/api/vulnerabilities.py +230 -0
- cloudsmith_cli/core/cache_utils.py +160 -0
- cloudsmith_cli/core/config.py +140 -0
- cloudsmith_cli/core/credentials/__init__.py +0 -0
- cloudsmith_cli/core/credentials/chain.py +69 -0
- cloudsmith_cli/core/credentials/models.py +44 -0
- cloudsmith_cli/core/credentials/oidc/__init__.py +6 -0
- cloudsmith_cli/core/credentials/oidc/cache.py +220 -0
- cloudsmith_cli/core/credentials/oidc/detectors/__init__.py +122 -0
- cloudsmith_cli/core/credentials/oidc/detectors/aws.py +85 -0
- cloudsmith_cli/core/credentials/oidc/detectors/azure_devops.py +70 -0
- cloudsmith_cli/core/credentials/oidc/detectors/base.py +26 -0
- cloudsmith_cli/core/credentials/oidc/detectors/bitbucket_pipelines.py +35 -0
- cloudsmith_cli/core/credentials/oidc/detectors/circleci.py +40 -0
- cloudsmith_cli/core/credentials/oidc/detectors/generic.py +42 -0
- cloudsmith_cli/core/credentials/oidc/detectors/github_actions.py +64 -0
- cloudsmith_cli/core/credentials/oidc/detectors/gitlab_ci.py +49 -0
- cloudsmith_cli/core/credentials/oidc/exchange.py +87 -0
- cloudsmith_cli/core/credentials/provider.py +17 -0
- cloudsmith_cli/core/credentials/providers/__init__.py +15 -0
- cloudsmith_cli/core/credentials/providers/cli_flag.py +24 -0
- cloudsmith_cli/core/credentials/providers/credentials_file.py +24 -0
- cloudsmith_cli/core/credentials/providers/env_var.py +24 -0
- cloudsmith_cli/core/credentials/providers/keyring_provider.py +59 -0
- cloudsmith_cli/core/credentials/providers/oidc_provider.py +115 -0
- cloudsmith_cli/core/download.py +594 -0
- cloudsmith_cli/core/keyring.py +171 -0
- cloudsmith_cli/core/mcp/__init__.py +0 -0
- cloudsmith_cli/core/mcp/data.py +17 -0
- cloudsmith_cli/core/mcp/server.py +786 -0
- cloudsmith_cli/core/pagination.py +131 -0
- cloudsmith_cli/core/ratelimits.py +88 -0
- cloudsmith_cli/core/rest.py +255 -0
- cloudsmith_cli/core/utils.py +95 -0
- cloudsmith_cli/core/version.py +20 -0
- cloudsmith_cli/credential_helpers/__init__.py +7 -0
- cloudsmith_cli/credential_helpers/backends.py +41 -0
- cloudsmith_cli/credential_helpers/common.py +111 -0
- cloudsmith_cli/credential_helpers/custom_domains.py +281 -0
- cloudsmith_cli/credential_helpers/docker/__init__.py +4 -0
- cloudsmith_cli/credential_helpers/docker/installer.py +349 -0
- cloudsmith_cli/credential_helpers/docker/runtime.py +117 -0
- cloudsmith_cli/credential_helpers/launchers.py +175 -0
- cloudsmith_cli/data/VERSION +1 -0
- cloudsmith_cli/data/config.ini +23 -0
- cloudsmith_cli/data/credentials.ini +14 -0
- cloudsmith_cli/templates/__init__.py +3 -0
- cloudsmith_cli/templates/auth_error.html +45 -0
- cloudsmith_cli/templates/auth_success.html +37 -0
- cloudsmith_cli-1.20.0.dist-info/METADATA +610 -0
- cloudsmith_cli-1.20.0.dist-info/RECORD +130 -0
- cloudsmith_cli-1.20.0.dist-info/WHEEL +5 -0
- cloudsmith_cli-1.20.0.dist-info/entry_points.txt +2 -0
- cloudsmith_cli-1.20.0.dist-info/licenses/LICENSE +201 -0
- cloudsmith_cli-1.20.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""CLI - Exceptions."""
|
|
2
|
+
|
|
3
|
+
import collections
|
|
4
|
+
import contextlib
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from ..core.api.exceptions import ApiException
|
|
10
|
+
from ..core.keyring import get_access_token
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@contextlib.contextmanager
|
|
14
|
+
def handle_api_exceptions(
|
|
15
|
+
ctx, opts, context_msg=None, nl=False, exit_on_error=True, reraise_on_error=False
|
|
16
|
+
):
|
|
17
|
+
"""Context manager that handles API exceptions."""
|
|
18
|
+
# flake8: ignore=C901
|
|
19
|
+
|
|
20
|
+
# Use stderr for messages if the output is something else (e.g. # JSON)
|
|
21
|
+
is_json_output = getattr(opts, "output", None) in ("json", "pretty_json")
|
|
22
|
+
use_stderr = is_json_output
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
yield
|
|
26
|
+
except ApiException as exc:
|
|
27
|
+
context_msg = context_msg or "Failed to perform operation!"
|
|
28
|
+
detail, fields = get_details(exc)
|
|
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
|
+
}
|
|
44
|
+
|
|
45
|
+
if fields:
|
|
46
|
+
error_data["fields"] = fields
|
|
47
|
+
|
|
48
|
+
# Surface push-time metadata context (validation/attach result)
|
|
49
|
+
# in the same JSON envelope so a downstream package-create or
|
|
50
|
+
# sync failure does not lose the earlier metadata signal.
|
|
51
|
+
metadata_context = getattr(opts, "push_metadata_info", None)
|
|
52
|
+
if metadata_context is not None:
|
|
53
|
+
error_data["metadata_attachment"] = metadata_context
|
|
54
|
+
|
|
55
|
+
# Print to stdout
|
|
56
|
+
import json
|
|
57
|
+
|
|
58
|
+
click.echo(
|
|
59
|
+
json.dumps(
|
|
60
|
+
error_data, indent=4 if opts.output == "pretty_json" else None
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
else:
|
|
65
|
+
# Standard CLI output to stderr (or interleaved if output != pretty, but we force use_stderr now)
|
|
66
|
+
if nl:
|
|
67
|
+
click.echo(err=use_stderr)
|
|
68
|
+
click.secho("ERROR: ", fg="red", nl=False, err=use_stderr)
|
|
69
|
+
else:
|
|
70
|
+
click.secho("ERROR", fg="red", err=use_stderr)
|
|
71
|
+
|
|
72
|
+
click.secho(
|
|
73
|
+
"%(context)s (status: %(code)s - %(code_text)s)"
|
|
74
|
+
% {
|
|
75
|
+
"context": context_msg,
|
|
76
|
+
"code": exc.status,
|
|
77
|
+
"code_text": exc.status_description,
|
|
78
|
+
},
|
|
79
|
+
fg="red",
|
|
80
|
+
err=use_stderr,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if detail or fields:
|
|
84
|
+
click.echo(err=use_stderr)
|
|
85
|
+
|
|
86
|
+
if detail:
|
|
87
|
+
click.secho(
|
|
88
|
+
"Detail: %(detail)s"
|
|
89
|
+
% {"detail": click.style(detail, fg="red", bold=False)},
|
|
90
|
+
bold=True,
|
|
91
|
+
err=use_stderr,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if fields:
|
|
95
|
+
for k, v in fields.items():
|
|
96
|
+
field = "%s Field" % k.capitalize()
|
|
97
|
+
|
|
98
|
+
# Flatten list/tuple error messages for text output
|
|
99
|
+
if isinstance(v, (list, tuple)):
|
|
100
|
+
v = " ".join(v)
|
|
101
|
+
|
|
102
|
+
click.secho(
|
|
103
|
+
"%(field)s: %(message)s"
|
|
104
|
+
% {
|
|
105
|
+
"field": click.style(field, bold=True),
|
|
106
|
+
"message": click.style(v, fg="red"),
|
|
107
|
+
},
|
|
108
|
+
err=use_stderr,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if hint:
|
|
112
|
+
click.echo(
|
|
113
|
+
f"Hint: {click.style(hint, fg='yellow')}",
|
|
114
|
+
err=use_stderr,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if opts.verbose and not opts.debug:
|
|
118
|
+
if exc.headers:
|
|
119
|
+
click.echo(err=use_stderr)
|
|
120
|
+
click.echo("Headers in Reply:", err=use_stderr)
|
|
121
|
+
for k, v in exc.headers.items():
|
|
122
|
+
click.echo(f"{k} = {v}", err=use_stderr)
|
|
123
|
+
|
|
124
|
+
if reraise_on_error:
|
|
125
|
+
raise
|
|
126
|
+
|
|
127
|
+
if exit_on_error:
|
|
128
|
+
ctx.exit(exc.status or 1)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_details(exc):
|
|
132
|
+
"""Get the details from the exception."""
|
|
133
|
+
detail = None
|
|
134
|
+
fields = collections.OrderedDict()
|
|
135
|
+
|
|
136
|
+
if exc.detail:
|
|
137
|
+
detail = exc.detail
|
|
138
|
+
|
|
139
|
+
if exc.fields:
|
|
140
|
+
for k, v in exc.fields.items():
|
|
141
|
+
try:
|
|
142
|
+
field_detail = v["detail"]
|
|
143
|
+
except (TypeError, KeyError):
|
|
144
|
+
field_detail = v
|
|
145
|
+
|
|
146
|
+
if k == "non_field_errors":
|
|
147
|
+
# Ensure we handle list/tuple for non_field_errors details joining
|
|
148
|
+
if isinstance(field_detail, (list, tuple)):
|
|
149
|
+
field_detail = " ".join(field_detail)
|
|
150
|
+
|
|
151
|
+
if detail:
|
|
152
|
+
detail += " " + field_detail
|
|
153
|
+
else:
|
|
154
|
+
detail = field_detail
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
fields[k] = field_detail
|
|
158
|
+
|
|
159
|
+
return detail, fields
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_error_hint(ctx, opts, exc):
|
|
163
|
+
"""Get a hint to show to the user (if any)."""
|
|
164
|
+
module = sys.modules[__name__]
|
|
165
|
+
get_specific_error_hint = getattr(module, "get_%s_error_hint" % exc.status, None)
|
|
166
|
+
if get_specific_error_hint:
|
|
167
|
+
return get_specific_error_hint(ctx, opts, exc)
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def get_401_error_hint(ctx, opts, exc):
|
|
172
|
+
"""Get the hint for a 401/Unauthorised error."""
|
|
173
|
+
# pylint: disable=unused-argument
|
|
174
|
+
if opts.api_key:
|
|
175
|
+
return (
|
|
176
|
+
"Since you have an API key set, this probably means "
|
|
177
|
+
"you don't have the permission to perform this action."
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
access_token = get_access_token(opts.api_host)
|
|
181
|
+
if access_token:
|
|
182
|
+
return "Since you have an SSO access token set, this probably means that it has expired. Try getting a new token with 'cloudsmith auth', then try again."
|
|
183
|
+
|
|
184
|
+
if ctx.info_name == "token":
|
|
185
|
+
# This is already the token command
|
|
186
|
+
return (
|
|
187
|
+
"The login failed - Either your email address and/or "
|
|
188
|
+
"your password was incorrect. Please check them and "
|
|
189
|
+
"try again!"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
"You don't have an API key or access token set, but it seems this action "
|
|
194
|
+
"requires authentication - Try getting your API key via "
|
|
195
|
+
"'cloudsmith token', or access token via 'cloudsmith auth', then try again."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_404_error_hint(ctx, opts, exc):
|
|
200
|
+
"""Get the hint for a 404/NotFound error."""
|
|
201
|
+
# pylint: disable=unused-argument
|
|
202
|
+
# pylint: disable=fixme
|
|
203
|
+
# TODO(ls): Expand this to be contextual (we could look at the
|
|
204
|
+
# arguments for the command).
|
|
205
|
+
return "This usually means the user/org is wrong or not visible."
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def get_500_error_hint(ctx, opts, exc):
|
|
209
|
+
"""Get the hint for a 500/InternalServerError error."""
|
|
210
|
+
# pylint: disable=unused-argument
|
|
211
|
+
return (
|
|
212
|
+
"This usually means the Cloudsmith service is encountering "
|
|
213
|
+
"issues, either with this specific command or as a whole. "
|
|
214
|
+
"Please accept our apologies and try again later."
|
|
215
|
+
)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Shared CLI helpers for package metadata content."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass, replace
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from ..core.version import get_version as get_cli_version
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class ResolvedMetadata:
|
|
15
|
+
"""Metadata content resolved from CLI options."""
|
|
16
|
+
|
|
17
|
+
provided: bool
|
|
18
|
+
content: dict[str, Any] | None
|
|
19
|
+
content_type: str | None = None
|
|
20
|
+
source_identity: str | None = None
|
|
21
|
+
content_file: str | None = None
|
|
22
|
+
source_label: str | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MetadataContentError(click.ClickException):
|
|
26
|
+
"""Raised when supplied metadata content is not a valid JSON object."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, message, *, source_label=None):
|
|
29
|
+
super().__init__(message)
|
|
30
|
+
self.source_label = source_label
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def default_metadata_source_identity() -> str:
|
|
34
|
+
"""Return the default value for metadata source identity options."""
|
|
35
|
+
return f"cloudsmith-cli@{get_cli_version()}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def source_label_for(content_file):
|
|
39
|
+
"""Return a human-readable label for a metadata content source."""
|
|
40
|
+
if content_file == "-":
|
|
41
|
+
return "stdin"
|
|
42
|
+
if content_file:
|
|
43
|
+
return os.path.basename(content_file)
|
|
44
|
+
return "inline"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _json_type_name(value):
|
|
48
|
+
if value is None:
|
|
49
|
+
return "null"
|
|
50
|
+
if isinstance(value, list):
|
|
51
|
+
return "array"
|
|
52
|
+
if isinstance(value, str):
|
|
53
|
+
return "string"
|
|
54
|
+
if isinstance(value, bool):
|
|
55
|
+
return "boolean"
|
|
56
|
+
if isinstance(value, (int, float)):
|
|
57
|
+
return "number"
|
|
58
|
+
return type(value).__name__
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_json_object(raw, source_label):
|
|
62
|
+
try:
|
|
63
|
+
content = json.loads(raw)
|
|
64
|
+
except ValueError as exc:
|
|
65
|
+
raise MetadataContentError(
|
|
66
|
+
f"Invalid JSON in {source_label}: {exc}",
|
|
67
|
+
source_label=source_label,
|
|
68
|
+
) from exc
|
|
69
|
+
|
|
70
|
+
if not isinstance(content, dict):
|
|
71
|
+
raise MetadataContentError(
|
|
72
|
+
"Metadata content must be a JSON object. Found "
|
|
73
|
+
f"{_json_type_name(content)}.",
|
|
74
|
+
source_label=source_label,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return content
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def resolve_metadata_content(
|
|
81
|
+
*,
|
|
82
|
+
content_file: str | None,
|
|
83
|
+
inline_content: str | None,
|
|
84
|
+
required: bool,
|
|
85
|
+
file_option_name: str,
|
|
86
|
+
content_option_name: str,
|
|
87
|
+
) -> ResolvedMetadata:
|
|
88
|
+
"""Resolve metadata content options into a parsed JSON object."""
|
|
89
|
+
if content_file is not None and inline_content is not None:
|
|
90
|
+
raise click.UsageError(
|
|
91
|
+
f"{file_option_name} and {content_option_name} are mutually exclusive."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if content_file is not None:
|
|
95
|
+
source_label = source_label_for(content_file)
|
|
96
|
+
if content_file == "-":
|
|
97
|
+
raw = click.get_text_stream("stdin").read()
|
|
98
|
+
else:
|
|
99
|
+
with open(content_file, encoding="utf-8") as fh:
|
|
100
|
+
raw = fh.read()
|
|
101
|
+
elif inline_content is not None:
|
|
102
|
+
source_label = source_label_for(None)
|
|
103
|
+
raw = inline_content
|
|
104
|
+
elif required:
|
|
105
|
+
raise click.UsageError(
|
|
106
|
+
f"One of {file_option_name} or {content_option_name} is required."
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
return ResolvedMetadata(provided=False, content=None)
|
|
110
|
+
|
|
111
|
+
return ResolvedMetadata(
|
|
112
|
+
provided=True,
|
|
113
|
+
content=_parse_json_object(raw, source_label),
|
|
114
|
+
content_file=content_file,
|
|
115
|
+
source_label=source_label,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def require_metadata_content_type(
|
|
120
|
+
*,
|
|
121
|
+
content_type: str | None,
|
|
122
|
+
content_provided: bool,
|
|
123
|
+
option_name: str,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Require content type when metadata content has been supplied."""
|
|
126
|
+
if content_provided and not content_type:
|
|
127
|
+
raise click.UsageError(
|
|
128
|
+
f"{option_name} is required when metadata content is supplied."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def attach_metadata_options(
|
|
133
|
+
metadata: ResolvedMetadata,
|
|
134
|
+
*,
|
|
135
|
+
content_type: str | None,
|
|
136
|
+
source_identity: str | None,
|
|
137
|
+
) -> ResolvedMetadata:
|
|
138
|
+
"""Return a resolved payload with content type and source identity attached."""
|
|
139
|
+
if not metadata.provided:
|
|
140
|
+
return metadata
|
|
141
|
+
|
|
142
|
+
return replace(
|
|
143
|
+
metadata,
|
|
144
|
+
content_type=content_type,
|
|
145
|
+
source_identity=source_identity or default_metadata_source_identity(),
|
|
146
|
+
)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from urllib.parse import urlencode
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from ..core.api.exceptions import ApiException
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_configured_session(opts):
|
|
9
|
+
"""
|
|
10
|
+
Create a requests session configured with the options from opts.
|
|
11
|
+
"""
|
|
12
|
+
session = requests.Session()
|
|
13
|
+
|
|
14
|
+
if hasattr(opts, "api_ssl_verify") and opts.api_ssl_verify is not None:
|
|
15
|
+
session.verify = opts.api_ssl_verify
|
|
16
|
+
|
|
17
|
+
if hasattr(opts, "api_proxy") and opts.api_proxy:
|
|
18
|
+
session.proxies = {"http": opts.api_proxy, "https": opts.api_proxy}
|
|
19
|
+
|
|
20
|
+
if hasattr(opts, "api_user_agent") and opts.api_user_agent:
|
|
21
|
+
session.headers.update({"User-Agent": opts.api_user_agent})
|
|
22
|
+
|
|
23
|
+
if hasattr(opts, "api_headers") and opts.api_headers:
|
|
24
|
+
session.headers.update(opts.api_headers)
|
|
25
|
+
|
|
26
|
+
return session
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_idp_url(api_host, owner, session):
|
|
30
|
+
org_saml_url = "{api_host}/orgs/{owner}/saml/?{params}".format(
|
|
31
|
+
api_host=api_host,
|
|
32
|
+
owner=owner,
|
|
33
|
+
params=urlencode({"redirect_url": "http://localhost:12400"}),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
org_saml_response = session.get(org_saml_url, timeout=30)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
org_saml_response.raise_for_status()
|
|
40
|
+
except requests.RequestException as exc:
|
|
41
|
+
raise ApiException(
|
|
42
|
+
org_saml_response.status_code,
|
|
43
|
+
headers=exc.response.headers,
|
|
44
|
+
body=exc.response.content,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return org_saml_response.json().get("redirect_url")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def exchange_2fa_token(api_host, two_factor_token, totp_token, session):
|
|
51
|
+
exchange_data = {"two_factor_token": two_factor_token, "totp_token": totp_token}
|
|
52
|
+
exchange_url = f"{api_host}/user/two-factor/"
|
|
53
|
+
|
|
54
|
+
headers = {
|
|
55
|
+
"Authorization": "Bearer {two_factor_token}".format(
|
|
56
|
+
two_factor_token=two_factor_token
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
exchange_response = session.post(
|
|
61
|
+
exchange_url,
|
|
62
|
+
data=exchange_data,
|
|
63
|
+
headers=headers,
|
|
64
|
+
timeout=30,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
exchange_response.raise_for_status()
|
|
69
|
+
except requests.RequestException as exc:
|
|
70
|
+
raise ApiException(
|
|
71
|
+
exchange_response.status_code,
|
|
72
|
+
headers=exc.response.headers,
|
|
73
|
+
body=exc.response.content,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
exchange_data = exchange_response.json()
|
|
77
|
+
access_token = exchange_data.get("access_token")
|
|
78
|
+
refresh_token = exchange_data.get("refresh_token")
|
|
79
|
+
|
|
80
|
+
return (access_token, refresh_token)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def refresh_access_token(api_host, access_token, refresh_token, session):
|
|
84
|
+
data = {"refresh_token": refresh_token}
|
|
85
|
+
url = f"{api_host}/user/refresh-token/"
|
|
86
|
+
|
|
87
|
+
headers = {"Authorization": f"Bearer {access_token}"}
|
|
88
|
+
|
|
89
|
+
response = session.post(
|
|
90
|
+
url,
|
|
91
|
+
data=data,
|
|
92
|
+
headers=headers,
|
|
93
|
+
timeout=30,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
response.raise_for_status()
|
|
98
|
+
except requests.RequestException as exc:
|
|
99
|
+
raise ApiException(
|
|
100
|
+
response.status_code,
|
|
101
|
+
headers=exc.response.headers,
|
|
102
|
+
body=exc.response.content,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
response_data = response.json()
|
|
106
|
+
access_token = response_data.get("access_token")
|
|
107
|
+
refresh_token = response_data.get("refresh_token")
|
|
108
|
+
|
|
109
|
+
return (access_token, refresh_token)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Core rate limit utilities."""
|
|
2
|
+
|
|
3
|
+
from collections import namedtuple
|
|
4
|
+
|
|
5
|
+
# pylint: disable=ungrouped-imports
|
|
6
|
+
import click
|
|
7
|
+
from click.utils import strip_ansi
|
|
8
|
+
|
|
9
|
+
Table = namedtuple(
|
|
10
|
+
"Table", ["headers", "plain_headers", "rows", "plain_rows", "column_widths"]
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def make_table(headers=None, rows=None):
|
|
15
|
+
"""Make a table from headers and rows."""
|
|
16
|
+
if callable(headers):
|
|
17
|
+
headers = headers()
|
|
18
|
+
if callable(rows):
|
|
19
|
+
rows = rows()
|
|
20
|
+
assert isinstance(headers, list)
|
|
21
|
+
assert isinstance(rows, list)
|
|
22
|
+
assert all(len(row) == len(headers) for row in rows)
|
|
23
|
+
|
|
24
|
+
plain_headers = [strip_ansi(str(v)) for v in headers]
|
|
25
|
+
plain_rows = [row for row in [strip_ansi(str(v)) for v in rows]]
|
|
26
|
+
|
|
27
|
+
plain_headers = []
|
|
28
|
+
column_widths = []
|
|
29
|
+
|
|
30
|
+
for k, v in enumerate(headers):
|
|
31
|
+
v = str(v)
|
|
32
|
+
plain = strip_ansi(v)
|
|
33
|
+
plain_headers.append(plain)
|
|
34
|
+
column_widths.append(len(plain))
|
|
35
|
+
|
|
36
|
+
if len(v) == len(plain):
|
|
37
|
+
# Value was unstyled, make it bold
|
|
38
|
+
v = click.style(v, bold=True)
|
|
39
|
+
|
|
40
|
+
headers[k] = v
|
|
41
|
+
|
|
42
|
+
plain_rows = []
|
|
43
|
+
for row in rows:
|
|
44
|
+
plain_row = []
|
|
45
|
+
for k, v in enumerate(row):
|
|
46
|
+
v = str(v)
|
|
47
|
+
plain = strip_ansi(v)
|
|
48
|
+
plain_row.append(plain)
|
|
49
|
+
column_widths[k] = max(column_widths[k], len(plain))
|
|
50
|
+
|
|
51
|
+
plain_rows.append(plain_row)
|
|
52
|
+
|
|
53
|
+
return Table(
|
|
54
|
+
headers=headers,
|
|
55
|
+
plain_headers=plain_headers,
|
|
56
|
+
rows=rows,
|
|
57
|
+
plain_rows=plain_rows,
|
|
58
|
+
column_widths=column_widths,
|
|
59
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""CLI/Commands - Custom types."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExpandPath(click.Path):
|
|
9
|
+
"""Extends Path to provide expanded user $HOME paths."""
|
|
10
|
+
|
|
11
|
+
def convert(self, value, *args, **kwargs): # pylint: disable=arguments-differ
|
|
12
|
+
"""Take a path with $HOME variables and resolve it to full path."""
|
|
13
|
+
value = os.path.expanduser(value)
|
|
14
|
+
return super().convert(value, *args, **kwargs)
|