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