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,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)