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,263 @@
|
|
|
1
|
+
import html
|
|
2
|
+
import os
|
|
3
|
+
import socket
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
6
|
+
from urllib.parse import parse_qsl, unquote, urlparse
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from ..core.api.exceptions import ApiException
|
|
11
|
+
from ..core.api.init import initialise_api
|
|
12
|
+
from ..core.credentials.models import CredentialResult
|
|
13
|
+
from ..core.keyring import store_sso_tokens
|
|
14
|
+
from .saml import exchange_2fa_token
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_template_path(template_name):
|
|
18
|
+
"""Get the absolute path to a template file."""
|
|
19
|
+
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
20
|
+
return os.path.join(base_dir, "templates", template_name)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def render_template(template_name, **context):
|
|
24
|
+
"""
|
|
25
|
+
Render a template with the given context.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
template_name: Name of the template file
|
|
29
|
+
context: Dictionary of variables to replace in the template
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Rendered HTML content
|
|
33
|
+
"""
|
|
34
|
+
template_path = get_template_path(template_name)
|
|
35
|
+
|
|
36
|
+
with open(template_path, encoding="utf-8") as file:
|
|
37
|
+
content = file.read()
|
|
38
|
+
|
|
39
|
+
# Replace placeholders with context values
|
|
40
|
+
for key, value in context.items():
|
|
41
|
+
placeholder = f"<!-- {key.upper()}_PLACEHOLDER -->"
|
|
42
|
+
content = content.replace(placeholder, value if value else "")
|
|
43
|
+
|
|
44
|
+
return content
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AuthenticationWebServer(HTTPServer):
|
|
48
|
+
def __init__(
|
|
49
|
+
self, server_address, RequestHandlerClass, bind_and_activate=True, **kwargs
|
|
50
|
+
):
|
|
51
|
+
self.owner = kwargs.get("owner")
|
|
52
|
+
self.session = kwargs.get("session")
|
|
53
|
+
self.debug = kwargs.get("debug", False)
|
|
54
|
+
self.refresh_api_on_success = kwargs.get("refresh_api_on_success", False)
|
|
55
|
+
self.api_opts = kwargs.get("api_opts")
|
|
56
|
+
self.sso_access_token = None
|
|
57
|
+
self.exception = None
|
|
58
|
+
|
|
59
|
+
super().__init__(
|
|
60
|
+
server_address, RequestHandlerClass, bind_and_activate=bind_and_activate
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def api_host(self):
|
|
67
|
+
"""Get the API host from api_opts."""
|
|
68
|
+
return self.api_opts.host if self.api_opts else None
|
|
69
|
+
|
|
70
|
+
def refresh_api_config_after_auth(self):
|
|
71
|
+
"""Refresh the API configuration to pick up newly stored SSO tokens."""
|
|
72
|
+
if not self.api_opts:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
initialise_api(
|
|
76
|
+
debug=self.api_opts.debug,
|
|
77
|
+
host=self.api_opts.host,
|
|
78
|
+
proxy=getattr(self.api_opts, "proxy", None),
|
|
79
|
+
ssl_verify=getattr(self.api_opts, "ssl_verify", True),
|
|
80
|
+
user_agent=getattr(self.api_opts, "user_agent", None),
|
|
81
|
+
headers=getattr(self.api_opts, "headers", None),
|
|
82
|
+
rate_limit=getattr(self.api_opts, "rate_limit", True),
|
|
83
|
+
credential=(
|
|
84
|
+
CredentialResult(
|
|
85
|
+
api_key=self.sso_access_token,
|
|
86
|
+
source_name="sso",
|
|
87
|
+
auth_type="bearer",
|
|
88
|
+
)
|
|
89
|
+
if self.sso_access_token
|
|
90
|
+
else None
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def finish_request(self, request, client_address):
|
|
95
|
+
self.RequestHandlerClass(
|
|
96
|
+
request,
|
|
97
|
+
client_address,
|
|
98
|
+
self,
|
|
99
|
+
owner=self.owner,
|
|
100
|
+
debug=self.debug,
|
|
101
|
+
session=self.session,
|
|
102
|
+
refresh_api_on_success=self.refresh_api_on_success,
|
|
103
|
+
server_instance=self,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _handle_request_noblock(self):
|
|
107
|
+
# override to allow exceptions to bubble up to the CLI
|
|
108
|
+
try:
|
|
109
|
+
request, client_address = self.get_request()
|
|
110
|
+
except OSError:
|
|
111
|
+
return
|
|
112
|
+
if self.verify_request(request, client_address):
|
|
113
|
+
try:
|
|
114
|
+
self.process_request(request, client_address)
|
|
115
|
+
except ( # pylint: disable=broad-exception-caught
|
|
116
|
+
Exception,
|
|
117
|
+
ApiException,
|
|
118
|
+
) as exc:
|
|
119
|
+
self.handle_error(request, client_address)
|
|
120
|
+
self.exception = exc
|
|
121
|
+
self.shutdown_request(request)
|
|
122
|
+
except BaseException:
|
|
123
|
+
self.shutdown_request(request)
|
|
124
|
+
raise
|
|
125
|
+
else:
|
|
126
|
+
self.shutdown_request(request)
|
|
127
|
+
|
|
128
|
+
def handle_error(self, request, client_address):
|
|
129
|
+
if self.debug:
|
|
130
|
+
super().handle_error(request, client_address)
|
|
131
|
+
|
|
132
|
+
def shutdown_request(self, request):
|
|
133
|
+
super().shutdown_request(request)
|
|
134
|
+
if self.exception:
|
|
135
|
+
exc = self.exception
|
|
136
|
+
self.exception = None # Clear to prevent re-raising
|
|
137
|
+
raise exc
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class AuthenticationWebRequestHandler(BaseHTTPRequestHandler):
|
|
141
|
+
def __init__(self, request, client_address, server, **kwargs):
|
|
142
|
+
self.owner = kwargs.get("owner")
|
|
143
|
+
self.debug = kwargs.get("debug", False)
|
|
144
|
+
self.session = kwargs.get("session")
|
|
145
|
+
self.refresh_api_on_success = kwargs.get("refresh_api_on_success", False)
|
|
146
|
+
self.server_instance = kwargs.get("server_instance")
|
|
147
|
+
|
|
148
|
+
super().__init__(request, client_address, server)
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def api_host(self):
|
|
152
|
+
"""Get the API host from the server instance."""
|
|
153
|
+
return self.server_instance.api_host if self.server_instance else None
|
|
154
|
+
|
|
155
|
+
def _return_response(self, status=200, message=None):
|
|
156
|
+
self.send_response(status)
|
|
157
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
158
|
+
self.end_headers()
|
|
159
|
+
|
|
160
|
+
self.wfile.write(message.encode("utf-8"))
|
|
161
|
+
|
|
162
|
+
def _return_success_response(self):
|
|
163
|
+
html_content = render_template("auth_success.html")
|
|
164
|
+
self._return_response(message=html_content)
|
|
165
|
+
|
|
166
|
+
def _return_error_response(self, error_message=None):
|
|
167
|
+
error_details = ""
|
|
168
|
+
if error_message:
|
|
169
|
+
safe_error = html.escape(unquote(error_message))
|
|
170
|
+
error_details = f"<p class='error-details'>Error: {safe_error}</p>"
|
|
171
|
+
|
|
172
|
+
html_content = render_template("auth_error.html", error_details=error_details)
|
|
173
|
+
|
|
174
|
+
self._return_response(
|
|
175
|
+
status=500,
|
|
176
|
+
message=html_content,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def log_request(self, code="-", size="-"):
|
|
180
|
+
if self.debug:
|
|
181
|
+
return super().log_request(code=code, size=size)
|
|
182
|
+
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
def log_error(self, format, *args): # pylint: disable=redefined-builtin
|
|
186
|
+
if self.debug:
|
|
187
|
+
return super().log_error(format, *args)
|
|
188
|
+
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
@cached_property
|
|
192
|
+
def url(self):
|
|
193
|
+
return urlparse(self.path)
|
|
194
|
+
|
|
195
|
+
@cached_property
|
|
196
|
+
def query_data(self):
|
|
197
|
+
return dict(parse_qsl(self.url.query))
|
|
198
|
+
|
|
199
|
+
def do_GET(self):
|
|
200
|
+
access_token = self.query_data.get("access_token")
|
|
201
|
+
refresh_token = self.query_data.get("refresh_token")
|
|
202
|
+
two_factor_token = self.query_data.get("two_factor_token")
|
|
203
|
+
error = self.query_data.get("error")
|
|
204
|
+
|
|
205
|
+
if error:
|
|
206
|
+
click.secho(
|
|
207
|
+
f"\nAuthentication error received: {unquote(error)}", fg="red", err=True
|
|
208
|
+
)
|
|
209
|
+
self._return_error_response(error)
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
if access_token:
|
|
214
|
+
# Store the access token on the server instance so it can be
|
|
215
|
+
# passed directly to initialise_api(), avoiding a keyring
|
|
216
|
+
# roundtrip (critical when CLOUDSMITH_NO_KEYRING is set).
|
|
217
|
+
if self.server_instance:
|
|
218
|
+
self.server_instance.sso_access_token = access_token
|
|
219
|
+
|
|
220
|
+
if not store_sso_tokens(self.api_host, access_token, refresh_token):
|
|
221
|
+
click.echo(
|
|
222
|
+
"SSO tokens not stored (CLOUDSMITH_NO_KEYRING is set)",
|
|
223
|
+
err=True,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if self.refresh_api_on_success and self.server_instance:
|
|
227
|
+
self.server_instance.refresh_api_config_after_auth()
|
|
228
|
+
|
|
229
|
+
self._return_success_response()
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
if two_factor_token:
|
|
233
|
+
totp_token = click.prompt(
|
|
234
|
+
"Please enter your 2FA token", hide_input=True, type=str, err=True
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
access_token, refresh_token = exchange_2fa_token(
|
|
238
|
+
self.api_host, two_factor_token, totp_token, session=self.session
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Store the access token on the server instance (same as above)
|
|
242
|
+
if self.server_instance:
|
|
243
|
+
self.server_instance.sso_access_token = access_token
|
|
244
|
+
|
|
245
|
+
if not store_sso_tokens(self.api_host, access_token, refresh_token):
|
|
246
|
+
click.echo(
|
|
247
|
+
"SSO tokens not stored (CLOUDSMITH_NO_KEYRING is set)",
|
|
248
|
+
err=True,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if self.refresh_api_on_success and self.server_instance:
|
|
252
|
+
self.server_instance.refresh_api_config_after_auth()
|
|
253
|
+
|
|
254
|
+
click.secho("\nAuthentication complete", fg="green", err=True)
|
|
255
|
+
self._return_success_response()
|
|
256
|
+
return
|
|
257
|
+
except Exception as exc:
|
|
258
|
+
self._return_error_response()
|
|
259
|
+
raise exc
|
|
260
|
+
|
|
261
|
+
click.secho("\nNo valid authentication parameters received", fg="red", err=True)
|
|
262
|
+
self._return_error_response()
|
|
263
|
+
return
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Non-CLI functionality."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Cloudsmith API Wrappers."""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""API - Files endpoints."""
|
|
2
|
+
|
|
3
|
+
import cloudsmith_api
|
|
4
|
+
|
|
5
|
+
from .. import ratelimits
|
|
6
|
+
from .exceptions import catch_raise_api_exception
|
|
7
|
+
from .init import get_api_client
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_distros_api():
|
|
11
|
+
"""Get the distros API client."""
|
|
12
|
+
return get_api_client(cloudsmith_api.DistrosApi)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def list_distros(package_format=None):
|
|
16
|
+
"""List available distributions."""
|
|
17
|
+
client = get_distros_api()
|
|
18
|
+
|
|
19
|
+
# pylint: disable=fixme
|
|
20
|
+
# TODO(ls): Add package format param on the server-side to filter distros
|
|
21
|
+
# instead of doing it here.
|
|
22
|
+
with catch_raise_api_exception():
|
|
23
|
+
distros, _, headers = client.distros_list_with_http_info()
|
|
24
|
+
|
|
25
|
+
ratelimits.maybe_rate_limit(client, headers)
|
|
26
|
+
|
|
27
|
+
return [
|
|
28
|
+
distro.to_dict()
|
|
29
|
+
for distro in distros
|
|
30
|
+
if not package_format or distro.format == package_format
|
|
31
|
+
]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""API - entitlements endpoints."""
|
|
2
|
+
|
|
3
|
+
import cloudsmith_api
|
|
4
|
+
|
|
5
|
+
from .. import ratelimits
|
|
6
|
+
from ..pagination import PageInfo
|
|
7
|
+
from .exceptions import catch_raise_api_exception
|
|
8
|
+
from .init import get_api_client
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_entitlements_api():
|
|
12
|
+
"""Get the entitlements API client."""
|
|
13
|
+
return get_api_client(cloudsmith_api.EntitlementsApi)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def list_entitlements(owner, repo, page, page_size, show_tokens):
|
|
17
|
+
"""Get a list of entitlements on a repository."""
|
|
18
|
+
client = get_entitlements_api()
|
|
19
|
+
|
|
20
|
+
with catch_raise_api_exception():
|
|
21
|
+
data, _, headers = client.entitlements_list_with_http_info(
|
|
22
|
+
owner=owner,
|
|
23
|
+
repo=repo,
|
|
24
|
+
page=page,
|
|
25
|
+
page_size=page_size,
|
|
26
|
+
show_tokens=show_tokens,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
ratelimits.maybe_rate_limit(client, headers)
|
|
30
|
+
page_info = PageInfo.from_headers(headers)
|
|
31
|
+
entitlements = [ent.to_dict() for ent in data] # pylint: disable=no-member
|
|
32
|
+
return entitlements, page_info
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_entitlement(owner, repo, name, token, show_tokens):
|
|
36
|
+
"""Create an entitlement in a repository."""
|
|
37
|
+
client = get_entitlements_api()
|
|
38
|
+
|
|
39
|
+
data = {}
|
|
40
|
+
if name is not None:
|
|
41
|
+
data["name"] = name
|
|
42
|
+
|
|
43
|
+
if token is not None:
|
|
44
|
+
data["token"] = token
|
|
45
|
+
|
|
46
|
+
with catch_raise_api_exception():
|
|
47
|
+
data, _, headers = client.entitlements_create_with_http_info(
|
|
48
|
+
owner=owner, repo=repo, data=data, show_tokens=show_tokens
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
ratelimits.maybe_rate_limit(client, headers)
|
|
52
|
+
return data.to_dict() # pylint: disable=no-member
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def delete_entitlement(owner, repo, identifier):
|
|
56
|
+
"""Delete an entitlement from a repository."""
|
|
57
|
+
client = get_entitlements_api()
|
|
58
|
+
|
|
59
|
+
with catch_raise_api_exception():
|
|
60
|
+
_, _, headers = client.entitlements_delete_with_http_info(
|
|
61
|
+
owner=owner, repo=repo, identifier=identifier
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
ratelimits.maybe_rate_limit(client, headers)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def update_entitlement(owner, repo, identifier, name, token, show_tokens):
|
|
68
|
+
"""Update an entitlement in a repository."""
|
|
69
|
+
client = get_entitlements_api()
|
|
70
|
+
|
|
71
|
+
data = {}
|
|
72
|
+
if name is not None:
|
|
73
|
+
data["name"] = name
|
|
74
|
+
|
|
75
|
+
if token is not None:
|
|
76
|
+
data["token"] = token
|
|
77
|
+
|
|
78
|
+
with catch_raise_api_exception():
|
|
79
|
+
data, _, headers = client.entitlements_partial_update_with_http_info(
|
|
80
|
+
owner=owner,
|
|
81
|
+
repo=repo,
|
|
82
|
+
identifier=identifier,
|
|
83
|
+
data=data,
|
|
84
|
+
show_tokens=show_tokens,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
ratelimits.maybe_rate_limit(client, headers)
|
|
88
|
+
return data.to_dict() # pylint: disable=no-member
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def refresh_entitlement(owner, repo, identifier, show_tokens):
|
|
92
|
+
"""Refresh an entitlement in a repository."""
|
|
93
|
+
client = get_entitlements_api()
|
|
94
|
+
|
|
95
|
+
with catch_raise_api_exception():
|
|
96
|
+
data, _, headers = client.entitlements_refresh_with_http_info(
|
|
97
|
+
owner=owner, repo=repo, identifier=identifier, show_tokens=show_tokens
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
ratelimits.maybe_rate_limit(client, headers)
|
|
101
|
+
return data.to_dict()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def sync_entitlements(owner, repo, source, show_tokens):
|
|
105
|
+
"""Sync entitlements from another repository."""
|
|
106
|
+
client = get_entitlements_api()
|
|
107
|
+
|
|
108
|
+
with catch_raise_api_exception():
|
|
109
|
+
data, _, headers = client.entitlements_sync_with_http_info(
|
|
110
|
+
owner=owner, repo=repo, data={"source": source}, show_tokens=show_tokens
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
ratelimits.maybe_rate_limit(client, headers)
|
|
114
|
+
page_info = PageInfo.from_headers(headers)
|
|
115
|
+
entitlements = [ent.to_dict() for ent in data.tokens]
|
|
116
|
+
return entitlements, page_info
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def restrict_entitlement(owner, repo, identifier, data):
|
|
120
|
+
"""Restrict entitlement token using provided restrictions."""
|
|
121
|
+
|
|
122
|
+
client = get_entitlements_api()
|
|
123
|
+
|
|
124
|
+
with catch_raise_api_exception():
|
|
125
|
+
data, _, headers = client.entitlements_partial_update_with_http_info(
|
|
126
|
+
owner=owner, repo=repo, identifier=identifier, data=data
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
ratelimits.maybe_rate_limit(client, headers)
|
|
130
|
+
return data.to_dict() # pylint: disable=no-member
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""API - Exceptions."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import http.client
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from cloudsmith_api.rest import ApiException as _ApiException
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ApiException(Exception):
|
|
11
|
+
"""Exception raised by the Cloudsmith API."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, status, detail=None, headers=None, body=None, fields=None):
|
|
14
|
+
"""Create a new APIException."""
|
|
15
|
+
super().__init__()
|
|
16
|
+
self.status = status
|
|
17
|
+
if status == 422:
|
|
18
|
+
self.status_description = "Unprocessable Entity"
|
|
19
|
+
else:
|
|
20
|
+
self.status_description = http.client.responses.get(
|
|
21
|
+
status, "Unknown Status"
|
|
22
|
+
)
|
|
23
|
+
self.detail = detail
|
|
24
|
+
self.headers = headers or {}
|
|
25
|
+
self.body = body
|
|
26
|
+
self.fields = fields or {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@contextlib.contextmanager
|
|
30
|
+
def catch_raise_api_exception():
|
|
31
|
+
"""Context manager that translates upstream API exceptions."""
|
|
32
|
+
try:
|
|
33
|
+
yield
|
|
34
|
+
except _ApiException as exc:
|
|
35
|
+
detail = None
|
|
36
|
+
fields = None
|
|
37
|
+
|
|
38
|
+
if exc.body:
|
|
39
|
+
try:
|
|
40
|
+
# pylint: disable=no-member
|
|
41
|
+
data = json.loads(exc.body)
|
|
42
|
+
detail = data.get("detail", None)
|
|
43
|
+
fields = data.get("fields", None)
|
|
44
|
+
except ValueError:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
detail = detail or exc.reason
|
|
48
|
+
|
|
49
|
+
raise ApiException(
|
|
50
|
+
exc.status, detail=detail, headers=exc.headers, body=exc.body, fields=fields
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TwoFactorRequiredException(Exception):
|
|
55
|
+
def __init__(self, two_factor_token):
|
|
56
|
+
self.two_factor_token = two_factor_token
|
|
57
|
+
super().__init__("Two-factor authentication is required")
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""API - Files endpoints."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import cloudsmith_api
|
|
7
|
+
import requests
|
|
8
|
+
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
|
|
9
|
+
|
|
10
|
+
from .. import ratelimits
|
|
11
|
+
from ..rest import create_requests_session
|
|
12
|
+
from ..utils import calculate_file_md5
|
|
13
|
+
from .exceptions import ApiException, catch_raise_api_exception
|
|
14
|
+
from .init import get_api_client
|
|
15
|
+
|
|
16
|
+
CHUNK_SIZE = 1024 * 1024 * 100
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_files_api():
|
|
20
|
+
"""Get the files API client."""
|
|
21
|
+
return get_api_client(cloudsmith_api.FilesApi)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate_request_file_upload(owner, repo, filepath, md5_checksum=None):
|
|
25
|
+
"""Validate parameters for requesting a file upload."""
|
|
26
|
+
client = get_files_api()
|
|
27
|
+
md5_checksum = md5_checksum or calculate_file_md5(filepath)
|
|
28
|
+
|
|
29
|
+
with catch_raise_api_exception():
|
|
30
|
+
_, _, headers = client.files_validate_with_http_info(
|
|
31
|
+
owner=owner,
|
|
32
|
+
repo=repo,
|
|
33
|
+
data={"filename": os.path.basename(filepath), "md5_checksum": md5_checksum},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
ratelimits.maybe_rate_limit(client, headers)
|
|
37
|
+
return md5_checksum
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def request_file_upload(
|
|
41
|
+
owner, repo, filepath, md5_checksum=None, is_multi_part_upload=False
|
|
42
|
+
):
|
|
43
|
+
"""Request a new package file upload (for creating packages)."""
|
|
44
|
+
client = get_files_api()
|
|
45
|
+
md5_checksum = md5_checksum or calculate_file_md5(filepath)
|
|
46
|
+
|
|
47
|
+
method = "put_parts" if is_multi_part_upload else "post"
|
|
48
|
+
|
|
49
|
+
with catch_raise_api_exception():
|
|
50
|
+
data, _, headers = client.files_create_with_http_info(
|
|
51
|
+
owner=owner,
|
|
52
|
+
repo=repo,
|
|
53
|
+
data={
|
|
54
|
+
"filename": os.path.basename(filepath),
|
|
55
|
+
"md5_checksum": md5_checksum,
|
|
56
|
+
"method": method,
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# pylint: disable=no-member
|
|
61
|
+
# Pylint detects the returned value as a tuple
|
|
62
|
+
ratelimits.maybe_rate_limit(client, headers)
|
|
63
|
+
return data.identifier, data.upload_url, data.upload_fields
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def upload_file(upload_url, upload_fields, filepath, callback=None):
|
|
67
|
+
"""Upload a pre-signed file to Cloudsmith."""
|
|
68
|
+
upload_fields = list(upload_fields.items())
|
|
69
|
+
upload_fields.append(
|
|
70
|
+
("file", (os.path.basename(filepath), click.open_file(filepath, "rb")))
|
|
71
|
+
)
|
|
72
|
+
encoder = MultipartEncoder(upload_fields)
|
|
73
|
+
monitor = MultipartEncoderMonitor(encoder, callback=callback)
|
|
74
|
+
|
|
75
|
+
config = cloudsmith_api.Configuration()
|
|
76
|
+
if config.proxy:
|
|
77
|
+
proxies = {"http": config.proxy, "https": config.proxy}
|
|
78
|
+
else:
|
|
79
|
+
proxies = None
|
|
80
|
+
|
|
81
|
+
headers = {"content-type": monitor.content_type}
|
|
82
|
+
|
|
83
|
+
client = get_files_api()
|
|
84
|
+
headers["user-agent"] = client.api_client.user_agent
|
|
85
|
+
|
|
86
|
+
session = create_requests_session()
|
|
87
|
+
resp = session.post(upload_url, data=monitor, headers=headers, proxies=proxies)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
resp.raise_for_status()
|
|
91
|
+
except requests.RequestException as exc:
|
|
92
|
+
raise ApiException(
|
|
93
|
+
resp.status_code, headers=exc.response.headers, body=exc.response.content
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def multi_part_upload_file(
|
|
98
|
+
opts, upload_url, owner, repo, filepath, callback, upload_id
|
|
99
|
+
):
|
|
100
|
+
with open(filepath, "rb") as f:
|
|
101
|
+
chunk_number = 1
|
|
102
|
+
session = create_requests_session()
|
|
103
|
+
headers = {"X-Api-Key": opts.api_key}
|
|
104
|
+
while chunk := f.read(CHUNK_SIZE):
|
|
105
|
+
resp = session.put(
|
|
106
|
+
upload_url,
|
|
107
|
+
headers=headers,
|
|
108
|
+
data=chunk,
|
|
109
|
+
params={
|
|
110
|
+
"upload_id": upload_id,
|
|
111
|
+
"part_number": chunk_number,
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
try:
|
|
115
|
+
resp.raise_for_status()
|
|
116
|
+
except requests.RequestException as exc:
|
|
117
|
+
raise ApiException(
|
|
118
|
+
resp.status_code,
|
|
119
|
+
headers=exc.response.headers,
|
|
120
|
+
body=exc.response.content,
|
|
121
|
+
)
|
|
122
|
+
callback()
|
|
123
|
+
chunk_number += 1
|
|
124
|
+
|
|
125
|
+
api = get_files_api()
|
|
126
|
+
api.files_complete(
|
|
127
|
+
owner,
|
|
128
|
+
repo,
|
|
129
|
+
identifier=upload_id,
|
|
130
|
+
data={"upload_id": upload_id, "complete": True},
|
|
131
|
+
)
|