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,131 @@
1
+ """Core pagination utilities."""
2
+
3
+ from collections.abc import Callable, Sequence
4
+ from typing import Any
5
+
6
+ MAX_PAGE_SIZE = 1000
7
+
8
+
9
+ class PageInfo:
10
+ """Data for pagination results."""
11
+
12
+ count = None
13
+ page = None
14
+ page_size = None
15
+ page_total = None
16
+
17
+ def __str__(self):
18
+ """Get page information as text."""
19
+ data = self.as_dict()
20
+ data["valid"] = self.is_valid
21
+ return (
22
+ "Valid: %(valid)s, Count: %(count)s, Page: %(page)s, "
23
+ "Size: %(page_size)s, Total: %(results_total)s" % data
24
+ )
25
+
26
+ def calculate_range(self, num_results):
27
+ """Calculate beginning and end of page range for results."""
28
+ if self.is_valid and num_results:
29
+ from_range = (self.page - 1) * self.page_size
30
+ to_range = from_range + num_results
31
+ from_range += 1
32
+ else:
33
+ from_range = 0
34
+ to_range = 0
35
+
36
+ return from_range, to_range
37
+
38
+ def as_dict(self, num_results=None):
39
+ """Create PageInfo from a dictionary."""
40
+ if not self.is_valid:
41
+ return {}
42
+
43
+ data = {
44
+ "results_total": self.count,
45
+ "page": self.page,
46
+ "page_size": self.page_size,
47
+ "page_max": self.page_total,
48
+ }
49
+
50
+ if num_results is not None:
51
+ from_range, to_range = self.calculate_range(num_results)
52
+ data["page_results_len"] = to_range - from_range
53
+ data["page_results_from"] = from_range
54
+ data["page_results_to"] = to_range
55
+
56
+ return data
57
+
58
+ @property
59
+ def is_valid(self):
60
+ """Check if the page information is valid."""
61
+ return all(
62
+ x is not None
63
+ for x in (self.count, self.page, self.page_size, self.page_total)
64
+ )
65
+
66
+ @classmethod
67
+ def from_headers(cls, headers):
68
+ """Create PageInfo from HTTP headers."""
69
+ info = PageInfo()
70
+
71
+ if "X-Pagination-Count" in headers:
72
+ info.count = int(headers["X-Pagination-Count"])
73
+ if "X-Pagination-Page" in headers:
74
+ info.page = int(headers["X-Pagination-Page"])
75
+ if "X-Pagination-PageSize" in headers:
76
+ info.page_size = int(headers["X-Pagination-PageSize"])
77
+ if "X-Pagination-PageTotal" in headers:
78
+ info.page_total = int(headers["X-Pagination-PageTotal"])
79
+
80
+ return info
81
+
82
+
83
+ def paginate_results(
84
+ api_function: Callable[..., tuple[Sequence[Any], PageInfo]],
85
+ page_all: bool,
86
+ page: int,
87
+ page_size: int = MAX_PAGE_SIZE,
88
+ **kwargs: Any,
89
+ ) -> tuple[list[Any], PageInfo]:
90
+ """Retrieve paginated results.
91
+
92
+ Behaviour:
93
+ - If ``page_all`` is False: perform a single paged request and return the
94
+ results plus the (possibly invalid) ``PageInfo``. Single-resource API
95
+ endpoints frequently omit pagination headers; we tolerate that here.
96
+ - If ``page_all`` is True: iterate all pages requesting ``MAX_PAGE_SIZE``.
97
+ Missing pagination headers during aggregation are treated as a user
98
+ misuse (e.g. attempting ``--page-all`` against a single-resource
99
+ endpoint) and raise a ``click.ClickException`` for consistent UX.
100
+
101
+ Raises:
102
+ click.ClickException: If pagination headers are absent while trying to
103
+ aggregate multiple pages with ``page_all``.
104
+ """
105
+ if not page_all:
106
+ results, page_info = api_function(page=page, page_size=page_size, **kwargs)
107
+ # For single resource endpoints (e.g. repos_read) pagination headers may be absent.
108
+ # In that case we return the results with potentially invalid page_info (empty when serialized)
109
+ # rather than raising. Downstream pretty printers handle an invalid page_info gracefully.
110
+ return list(results), page_info
111
+
112
+ all_results: list[Any] = []
113
+ current_page = 1
114
+ last_page_info: PageInfo | None = None
115
+ while True:
116
+ page_results, last_page_info = api_function(
117
+ page=current_page, page_size=MAX_PAGE_SIZE, **kwargs
118
+ )
119
+ if not last_page_info.is_valid:
120
+ # No pagination headers (single-resource endpoint). Treat as single page.
121
+ # Return accumulated results without raising; command-level validators
122
+ # handle misuse of --page-all with single-resource endpoints.
123
+ all_results.extend(page_results)
124
+ return all_results, last_page_info
125
+ all_results.extend(page_results)
126
+
127
+ if current_page >= last_page_info.page_total:
128
+ break
129
+ current_page += 1
130
+
131
+ return all_results, last_page_info
@@ -0,0 +1,88 @@
1
+ """Core rate limit utilities."""
2
+
3
+ import datetime
4
+ import time
5
+
6
+
7
+ class RateLimitsInfo:
8
+ """Data for rate limits."""
9
+
10
+ interval = None
11
+ limit = None
12
+ remaining = None
13
+ reset = None
14
+ throttled = None
15
+
16
+ def __str__(self):
17
+ """Get rate limit information as text."""
18
+ return (
19
+ "Throttled: %(throttled)s, Remaining: %(remaining)d/%(limit)d, "
20
+ "Interval: %(interval)f, Reset: %(reset)s"
21
+ % {
22
+ "throttled": "Yes" if self.throttled else "No",
23
+ "remaining": self.remaining,
24
+ "limit": self.limit,
25
+ "interval": self.interval,
26
+ "reset": self.reset,
27
+ }
28
+ )
29
+
30
+ @classmethod
31
+ def from_dict(cls, data):
32
+ """Create RateLimitsInfo from a dictionary."""
33
+ info = RateLimitsInfo()
34
+
35
+ if "interval" in data:
36
+ info.interval = float(data["interval"])
37
+ if "limit" in data:
38
+ info.limit = int(data["limit"])
39
+ if "remaining" in data:
40
+ info.remaining = int(data["remaining"])
41
+ if "reset" in data:
42
+ info.reset = datetime.datetime.utcfromtimestamp(int(data["reset"]))
43
+ if "throttled" in data:
44
+ info.throttled = bool(data["throttled"])
45
+ else:
46
+ info.throttled = info.remaining == 0
47
+
48
+ return info
49
+
50
+ @classmethod
51
+ def from_headers(cls, headers):
52
+ """Create RateLimitsInfo from HTTP headers."""
53
+ try:
54
+ data = {
55
+ "interval": headers["X-RateLimit-Interval"],
56
+ "limit": headers["X-RateLimit-Limit"],
57
+ "remaining": headers["X-RateLimit-Remaining"],
58
+ "reset": headers["X-RateLimit-Reset"],
59
+ }
60
+ except KeyError:
61
+ data = {}
62
+
63
+ return cls.from_dict(data)
64
+
65
+
66
+ def maybe_rate_limit(client, headers):
67
+ """Optionally pause the process based on suggested rate interval."""
68
+ rate_limit(client, headers)
69
+
70
+
71
+ def rate_limit(client, headers):
72
+ """Pause the process based on suggested rate interval."""
73
+ if not client or not headers:
74
+ return False
75
+
76
+ if not getattr(client.config, "rate_limit", False):
77
+ return False
78
+
79
+ rate_info = RateLimitsInfo.from_headers(headers)
80
+ if not rate_info or not rate_info.interval:
81
+ return False
82
+
83
+ if rate_info.interval:
84
+ cb = getattr(client.config, "rate_limit_callback", None)
85
+ if cb and callable(cb):
86
+ cb(rate_info)
87
+ time.sleep(rate_info.interval)
88
+ return True
@@ -0,0 +1,255 @@
1
+ """A RESTful API client with retry builtin."""
2
+
3
+ import io
4
+ import json
5
+ import logging
6
+ import re
7
+ import time
8
+ from urllib.parse import urlencode
9
+
10
+ import requests
11
+ import requests.exceptions
12
+ from cloudsmith_api.configuration import Configuration
13
+ from cloudsmith_api.rest import ApiException, RESTClientObject
14
+ from requests.adapters import HTTPAdapter
15
+ from urllib3.util.retry import Retry
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class RetryWithCallback(Retry):
21
+ """A urllib3 Retry with a callback on retries."""
22
+
23
+ def __init__(self, *args, **kwargs):
24
+ self.error_retry_cb = kwargs.pop("error_retry_cb", None)
25
+ super().__init__(*args, **kwargs)
26
+
27
+ def new(self, **kw):
28
+ kw["error_retry_cb"] = self.error_retry_cb
29
+ return super().new(**kw)
30
+
31
+ def sleep_for_retry(self, response=None):
32
+ retry_after = self.get_retry_after(response)
33
+ if retry_after:
34
+ self._sleep_with_callback(retry_after, context="retry-after")
35
+ return True
36
+
37
+ return False
38
+
39
+ def _sleep_backoff(self):
40
+ backoff = self.get_backoff_time()
41
+ if backoff <= 0:
42
+ return
43
+ self._sleep_with_callback(backoff, context="backoff")
44
+
45
+ def _sleep_with_callback(self, seconds, context=None):
46
+ """Sleep, but generate a callback before it."""
47
+ if self.error_retry_cb and callable(self.error_retry_cb):
48
+ self.error_retry_cb(seconds, context=context)
49
+ return time.sleep(seconds)
50
+
51
+
52
+ def create_requests_session(
53
+ retries=None,
54
+ backoff_factor=None,
55
+ status_forcelist=None,
56
+ pools_size=4,
57
+ maxsize=4,
58
+ ssl_verify=None,
59
+ ssl_cert=None,
60
+ proxy=None,
61
+ session=None,
62
+ error_retry_cb=None,
63
+ respect_retry_after_header=True,
64
+ user_agent=None,
65
+ headers=None,
66
+ ):
67
+ """Create a requests session that retries some errors."""
68
+ # pylint: disable=too-many-branches
69
+ config = Configuration()
70
+
71
+ if retries is None:
72
+ retry_max = getattr(config, "error_retry_max", None)
73
+ retries = retry_max if retry_max is not None else 5
74
+
75
+ if backoff_factor is None:
76
+ retry_backoff = getattr(config, "error_retry_backoff", None)
77
+ backoff_factor = retry_backoff if retry_backoff is not None else 0.23
78
+
79
+ if status_forcelist is None:
80
+ retry_codes = getattr(config, "error_retry_codes", None)
81
+ status_forcelist = (
82
+ retry_codes if retry_codes is not None else [500, 502, 503, 504]
83
+ )
84
+
85
+ if ssl_verify is None:
86
+ ssl_verify = config.verify_ssl
87
+
88
+ if ssl_cert is None:
89
+ if config.cert_file and config.key_file:
90
+ ssl_cert = (config.cert_file, config.key_file)
91
+ elif config.cert_file:
92
+ ssl_cert = config.cert_file
93
+
94
+ if proxy is None:
95
+ proxy = Configuration().proxy
96
+
97
+ session = session or requests.Session()
98
+ session.verify = ssl_verify
99
+ session.cert = ssl_cert
100
+
101
+ if proxy:
102
+ session.proxies = {"http": proxy, "https": proxy}
103
+
104
+ retry = RetryWithCallback(
105
+ backoff_factor=backoff_factor,
106
+ connect=retries,
107
+ allowed_methods=False,
108
+ read=retries,
109
+ status_forcelist=tuple(status_forcelist),
110
+ status=retries,
111
+ total=retries,
112
+ error_retry_cb=error_retry_cb,
113
+ respect_retry_after_header=respect_retry_after_header,
114
+ )
115
+
116
+ adapter = HTTPAdapter(
117
+ max_retries=retry,
118
+ pool_connections=pools_size,
119
+ pool_maxsize=maxsize,
120
+ pool_block=True,
121
+ )
122
+
123
+ session.mount("http://", adapter)
124
+ session.mount("https://", adapter)
125
+
126
+ if user_agent:
127
+ session.headers["User-Agent"] = user_agent
128
+
129
+ if headers:
130
+ session.headers.update(headers)
131
+
132
+ return session
133
+
134
+
135
+ class RestResponse(io.IOBase):
136
+ """A urllib3 adapter for a requests response."""
137
+
138
+ def __init__(self, response):
139
+ super().__init__()
140
+ self.response = response
141
+ self.status = response.status_code
142
+ self.reason = response.reason
143
+ self._data = None
144
+
145
+ @property
146
+ def data(self):
147
+ """
148
+ Get the content for the response (lazily decoded).
149
+ """
150
+ if self._data is None:
151
+ self._data = self.response.content.decode("utf-8")
152
+ return self._data
153
+
154
+ def getheaders(self):
155
+ """
156
+ Return a dictionary of the response headers.
157
+ """
158
+ return self.response.headers
159
+
160
+ def getheader(self, name, default=None):
161
+ """
162
+ Return a given response header.
163
+ """
164
+ return self.response.headers.get(name, default)
165
+
166
+
167
+ class RestClient(RESTClientObject):
168
+ """A rest client interface based on requests, with retry."""
169
+
170
+ def __init__(self, *args, **kwargs):
171
+ # pylint: disable=super-init-not-called
172
+ self.session = create_requests_session(*args, **kwargs)
173
+
174
+ def request(
175
+ self,
176
+ method,
177
+ url,
178
+ query_params=None,
179
+ headers=None,
180
+ body=None,
181
+ post_params=None,
182
+ _preload_content=True,
183
+ _request_timeout=None,
184
+ ):
185
+ """
186
+ :param method: http request method
187
+ :param url: http request url
188
+ :param query_params: query parameters in the url
189
+ :param headers: http request headers
190
+ :param body: request json body, for `application/json`
191
+ :param post_params: request post parameters,
192
+ `application/x-www-form-urlencoded`
193
+ and `multipart/form-data`
194
+ :param _preload_content: if False, the response object will be returned without
195
+ reading/decoding response data. Default is True.
196
+ :param _request_timeout: timeout setting for this request. If one number provided, it will be total request
197
+ timeout. It can also be a pair (tuple) of (connection, read) timeouts.
198
+ """
199
+ # Based on the RESTClientObject class generated by Swagger
200
+ method = method.upper()
201
+ assert method in ["GET", "HEAD", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"]
202
+
203
+ post_params = post_params or {}
204
+ headers = headers or {}
205
+
206
+ if "Content-Type" not in headers:
207
+ headers["Content-Type"] = "application/json"
208
+
209
+ request_kwargs = {}
210
+
211
+ if query_params:
212
+ url += "?" + urlencode(query_params)
213
+
214
+ if method in ["POST", "PUT", "PATCH", "OPTIONS", "DELETE"]:
215
+ if re.search("json", headers["Content-Type"], re.IGNORECASE):
216
+ request_body = None
217
+ if body:
218
+ request_body = json.dumps(body)
219
+ request_kwargs["data"] = request_body
220
+ elif headers["Content-Type"] == "application/x-www-form-urlencoded":
221
+ request_kwargs["data"] = post_params
222
+ elif headers["Content-Type"] == "multipart/form-data":
223
+ del headers["Content-Type"]
224
+ request_kwargs["data"] = post_params
225
+ elif isinstance(body, str):
226
+ request_kwargs["data"] = body
227
+ else:
228
+ # Cannot generate the request from given parameters
229
+ msg = """Cannot prepare a request message for provided arguments.
230
+ Please check that your arguments match declared content type."""
231
+ raise ApiException(status=0, reason=msg)
232
+
233
+ try:
234
+ resp = self.session.request(
235
+ method,
236
+ url,
237
+ timeout=_request_timeout,
238
+ stream=not _preload_content,
239
+ headers=headers,
240
+ **request_kwargs,
241
+ )
242
+ except requests.exceptions.RequestException as exc:
243
+ msg = f"{type(exc).__name__}\n{str(exc)}"
244
+ raise ApiException(status=0, reason=msg)
245
+
246
+ resp.encoding = resp.apparent_encoding or "utf-8"
247
+ rest_resp = RestResponse(resp)
248
+
249
+ if _preload_content:
250
+ logger.debug("response body: %s", rest_resp.data)
251
+
252
+ if not 200 <= rest_resp.status <= 299:
253
+ raise ApiException(http_resp=rest_resp)
254
+
255
+ return rest_resp
@@ -0,0 +1,95 @@
1
+ """Core utilities."""
2
+
3
+ import hashlib
4
+ import os
5
+
6
+ import click
7
+
8
+
9
+ def get_help_website():
10
+ """Get the URL for the help website."""
11
+ return "https://docs.cloudsmith.com/developer-tools/cli"
12
+
13
+
14
+ def get_github_website():
15
+ """Get the URL for the GitHub project."""
16
+ return "https://github.com/cloudsmith-io/cloudsmith-cli"
17
+
18
+
19
+ def get_root_path():
20
+ """Get the root directory for the application."""
21
+ return os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir))
22
+
23
+
24
+ def get_data_path():
25
+ """Get the data directory for the application."""
26
+ return os.path.join(get_root_path(), "data")
27
+
28
+
29
+ def read_file(*path):
30
+ """Read the specific file into a string in its entirety."""
31
+ real_path = os.path.realpath(os.path.join(*path))
32
+ with click.open_file(real_path, "r") as fp:
33
+ return fp.read()
34
+
35
+
36
+ def calculate_file_md5(filepath, blocksize=2**20):
37
+ """Calculate an MD5 hash for a file."""
38
+ checksum = hashlib.md5(usedforsecurity=False)
39
+
40
+ with click.open_file(filepath, "rb") as f:
41
+
42
+ def update_chunk():
43
+ """Add chunk to checksum."""
44
+ buf = f.read(blocksize)
45
+ if buf:
46
+ checksum.update(buf)
47
+ return bool(buf)
48
+
49
+ while update_chunk():
50
+ pass
51
+
52
+ return checksum.hexdigest()
53
+
54
+
55
+ def get_file_size(filepath):
56
+ """Get the size of a file in bytes."""
57
+ statinfo = os.stat(filepath)
58
+ return statinfo.st_size
59
+
60
+
61
+ def get_page_kwargs(**kwargs):
62
+ """Construct page and page size kwargs (if present)."""
63
+ page_kwargs = {}
64
+
65
+ page = kwargs.get("page")
66
+ if page is not None and page > 0:
67
+ page_kwargs["page"] = page
68
+
69
+ page_size = kwargs.get("page_size")
70
+ if page_size is not None and page_size > 0:
71
+ page_kwargs["page_size"] = page_size
72
+
73
+ return page_kwargs
74
+
75
+
76
+ def get_query_kwargs(**kwargs):
77
+ """Construct page and page size kwargs (if present)."""
78
+ query_kwargs = {}
79
+
80
+ query = kwargs.pop("query")
81
+ if query:
82
+ query_kwargs["query"] = query
83
+
84
+ return query_kwargs
85
+
86
+
87
+ def get_sort_kwargs(**kwargs):
88
+ """Construct sort kwargs (if present)."""
89
+ sort_kwargs = {}
90
+
91
+ sort = kwargs.get("sort")
92
+ if sort:
93
+ sort_kwargs["sort"] = sort
94
+
95
+ return sort_kwargs
@@ -0,0 +1,20 @@
1
+ """Core version utilities."""
2
+
3
+ import semver
4
+
5
+ from . import utils
6
+
7
+
8
+ def get_version():
9
+ """Get the raw/unparsed version of the application as a string."""
10
+ return utils.read_file(utils.get_data_path(), "VERSION").strip()
11
+
12
+
13
+ def get_version_info():
14
+ """Get the application version as a VersionInfo object."""
15
+ return parse_version(get_version())
16
+
17
+
18
+ def parse_version(version):
19
+ """Get a version string as a VersionInfo object."""
20
+ return semver.parse_version_info(version)
@@ -0,0 +1,7 @@
1
+ # Copyright 2026 Cloudsmith Ltd
2
+ """
3
+ Credential helpers for various package managers.
4
+
5
+ This package provides credential helper implementations for Docker, pip, npm, etc.
6
+ Each helper follows its respective package manager's credential helper protocol.
7
+ """
@@ -0,0 +1,41 @@
1
+ # Copyright 2026 Cloudsmith Ltd
2
+ """Backend-kind enumeration for credential helpers."""
3
+
4
+ from enum import IntEnum
5
+
6
+
7
+ class BackendKind(IntEnum):
8
+ """Mirror of the server-side BackendKind enum (cloudsmith/package/enums.py)."""
9
+
10
+ DEB = 0
11
+ RPM = 1
12
+ RUBY = 2
13
+ PYTHON = 3
14
+ MAVEN = 4
15
+ BOWER = 5
16
+ DOCKER = 6
17
+ RAW = 7
18
+ CHOCOLATEY = 8
19
+ NPM = 9
20
+ NUGET = 10
21
+ VAGRANT = 11
22
+ COMPOSER = 12
23
+ ALPINE = 13
24
+ HELM = 14
25
+ CONAN = 15
26
+ CARGO = 16
27
+ LUAROCKS = 17
28
+ CRAN = 18
29
+ GO = 19
30
+ DART = 20
31
+ COCOAPODS = 21
32
+ TERRAFORM = 22
33
+ P2 = 23
34
+ CONDA = 24
35
+ HEX = 25
36
+ SWIFT = 26
37
+ HUGGINGFACE = 27
38
+ GENERIC = 28
39
+ VSX = 29
40
+ MCP = 30
41
+ DEFAULT = 99