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