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,111 @@
1
+ # Copyright 2026 Cloudsmith Ltd
2
+ """
3
+ Shared utilities for credential helpers.
4
+
5
+ Provides domain checking used by all credential helpers.
6
+ """
7
+
8
+ import logging
9
+ import os
10
+
11
+ from .custom_domains import get_custom_domains, get_format_domains
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def extract_hostname(url):
17
+ """
18
+ Extract bare hostname from any URL format.
19
+
20
+ Handles protocols, sparse+ prefix, ports, paths, and trailing slashes.
21
+
22
+ Args:
23
+ url: URL in any format (e.g., "sparse+https://cargo.cloudsmith.io/org/repo/")
24
+
25
+ Returns:
26
+ str: Lowercase hostname (e.g., "cargo.cloudsmith.io")
27
+ """
28
+ if not url:
29
+ return ""
30
+
31
+ normalized = url.lower().strip()
32
+
33
+ # Remove sparse+ prefix (Cargo)
34
+ if normalized.startswith("sparse+"):
35
+ normalized = normalized[7:]
36
+
37
+ # Remove protocol
38
+ if "://" in normalized:
39
+ normalized = normalized.split("://", 1)[1]
40
+
41
+ # Remove userinfo (user@host)
42
+ if "@" in normalized.split("/")[0]:
43
+ normalized = normalized.split("@", 1)[1]
44
+
45
+ # Extract hostname (before first / or :)
46
+ hostname = normalized.split("/")[0].split(":")[0]
47
+
48
+ return hostname
49
+
50
+
51
+ def is_cloudsmith_domain(
52
+ url, api_key=None, auth_type="api_key", api_host=None, backend_kind=None
53
+ ):
54
+ """
55
+ Check if a URL points to a Cloudsmith service.
56
+
57
+ Checks standard *.cloudsmith.io domains first (no auth needed).
58
+ If not a standard domain, queries the Cloudsmith API for custom domains.
59
+
60
+ Args:
61
+ url: URL or hostname to check
62
+ api_key: API key/token for authenticating custom domain lookups
63
+ auth_type: "api_key" (X-Api-Key header) or "bearer" (Authorization: Bearer)
64
+ api_host: Cloudsmith API host URL
65
+ backend_kind: If given, custom domains only match when their backend_kind
66
+ equals it (standard *.cloudsmith.io domains always match regardless).
67
+ When None (default), any enabled+validated custom domain matches.
68
+
69
+ Returns:
70
+ bool: True if this is a Cloudsmith domain
71
+ """
72
+ hostname = extract_hostname(url)
73
+ if not hostname:
74
+ return False
75
+
76
+ # Standard Cloudsmith domains — no auth needed, always match regardless of backend_kind
77
+ if (
78
+ hostname in ("cloudsmith.io", "cloudsmith.com")
79
+ or hostname.endswith(".cloudsmith.io")
80
+ or hostname.endswith(".cloudsmith.com")
81
+ ):
82
+ return True
83
+
84
+ # Custom domains require org + auth
85
+ org = os.environ.get("CLOUDSMITH_ORG", "").strip()
86
+ if not org:
87
+ return False
88
+
89
+ if not api_key:
90
+ return False
91
+
92
+ if backend_kind is not None:
93
+ hosts = {
94
+ host.lower()
95
+ for host in get_format_domains(
96
+ org,
97
+ backend_kind,
98
+ api_key=api_key,
99
+ auth_type=auth_type,
100
+ api_host=api_host,
101
+ )
102
+ }
103
+ else:
104
+ hosts = {
105
+ d.host.lower()
106
+ for d in get_custom_domains(
107
+ org, api_key=api_key, auth_type=auth_type, api_host=api_host
108
+ )
109
+ if d.enabled and d.validated
110
+ }
111
+ return hostname in hosts
@@ -0,0 +1,281 @@
1
+ # Copyright 2026 Cloudsmith Ltd
2
+ """
3
+ Helper for discovering Cloudsmith custom domains.
4
+
5
+ This module provides functions to fetch custom domains from the Cloudsmith API
6
+ for use in credential helpers. Results are cached on the filesystem.
7
+ """
8
+
9
+ import json
10
+ import logging
11
+ import time
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Literal
15
+
16
+ from ..cli.config import get_default_config_path
17
+ from ..core.api.exceptions import ApiException
18
+ from ..core.api.init import initialise_api
19
+ from ..core.api.orgs import list_custom_domains
20
+ from ..core.cache_utils import atomic_write_json
21
+ from ..core.credentials.models import CredentialResult
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Cache custom domains for 1 hour
26
+ CACHE_TTL_SECONDS = 3600
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class CustomDomain:
31
+ """A structured Cloudsmith custom domain record."""
32
+
33
+ host: str
34
+ backend_kind: int | None
35
+ enabled: bool
36
+ validated: bool
37
+
38
+
39
+ def get_cache_dir() -> Path:
40
+ """
41
+ Get the cache directory for custom domains.
42
+ """
43
+ cache_dir = Path(get_default_config_path()) / "custom_domains_cache"
44
+ cache_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
45
+ return cache_dir
46
+
47
+
48
+ def get_cache_path(org: str) -> Path:
49
+ """
50
+ Get the cache file path for an organization's custom domains.
51
+
52
+ Args:
53
+ org: Organization slug
54
+
55
+ Returns:
56
+ Path to cache file
57
+ """
58
+ cache_dir = get_cache_dir()
59
+ safe_org = "".join(c if c.isalnum() or c in "-_" else "_" for c in org)
60
+ return cache_dir / f"{safe_org}.json"
61
+
62
+
63
+ def is_cache_valid(cache_path: Path) -> bool:
64
+ """
65
+ Check if a cache file exists and is still valid.
66
+
67
+ Args:
68
+ cache_path: Path to cache file
69
+
70
+ Returns:
71
+ bool: True if cache exists and hasn't expired
72
+ """
73
+ if not cache_path.exists():
74
+ return False
75
+
76
+ try:
77
+ mtime = cache_path.stat().st_mtime
78
+ age = time.time() - mtime
79
+ return age < CACHE_TTL_SECONDS
80
+ except OSError:
81
+ return False
82
+
83
+
84
+ def read_cache(cache_path: Path) -> list[CustomDomain] | None:
85
+ """
86
+ Read custom domains from cache file.
87
+
88
+ Args:
89
+ cache_path: Path to cache file
90
+
91
+ Returns:
92
+ List of CustomDomain records or None if cache invalid/missing
93
+ """
94
+ if not is_cache_valid(cache_path):
95
+ return None
96
+
97
+ try:
98
+ with open(cache_path, encoding="utf-8") as f:
99
+ data = json.load(f)
100
+ if isinstance(data, dict) and "domains" in data:
101
+ domains = data["domains"]
102
+ if isinstance(domains, list):
103
+ # Detect legacy format: non-empty list of strings (old build stored
104
+ # domains as plain strings, not dicts). Treat as a cache miss so the
105
+ # caller re-fetches and rewrites in the current dict format.
106
+ if domains and not any(isinstance(d, dict) for d in domains):
107
+ logger.debug(
108
+ "Stale string-format cache detected at %s, treating as miss",
109
+ cache_path,
110
+ )
111
+ return None
112
+
113
+ records = [
114
+ CustomDomain(
115
+ host=d["host"],
116
+ backend_kind=d.get("backend_kind"),
117
+ enabled=bool(d.get("enabled", False)),
118
+ validated=bool(d.get("validated", False)),
119
+ )
120
+ for d in domains
121
+ if isinstance(d, dict) and d.get("host")
122
+ ]
123
+ logger.debug(
124
+ "Read %d domains from cache: %s", len(records), cache_path
125
+ )
126
+ return records
127
+ except (OSError, json.JSONDecodeError) as exc:
128
+ logger.debug("Failed to read cache %s: %s", cache_path, exc)
129
+
130
+ return None
131
+
132
+
133
+ def write_cache(cache_path: Path, domains: list[CustomDomain]) -> None:
134
+ """Write custom domains to cache file."""
135
+ data = {
136
+ "domains": [
137
+ {
138
+ "host": d.host,
139
+ "backend_kind": d.backend_kind,
140
+ "enabled": d.enabled,
141
+ "validated": d.validated,
142
+ }
143
+ for d in domains
144
+ ],
145
+ "cached_at": time.time(),
146
+ }
147
+ try:
148
+ atomic_write_json(cache_path, data)
149
+ logger.debug("Wrote %d domains to cache: %s", len(domains), cache_path)
150
+ except OSError as exc:
151
+ logger.debug("Failed to write cache %s: %s", cache_path, exc)
152
+
153
+
154
+ def get_custom_domains( # pylint: disable=too-many-return-statements
155
+ org: str,
156
+ *,
157
+ api_key: str | None = None,
158
+ auth_type: str = "api_key",
159
+ api_host: str | None = None,
160
+ refresh: bool = False,
161
+ ) -> list[CustomDomain]:
162
+ """
163
+ Fetch custom domains for a Cloudsmith organization.
164
+
165
+ Results are cached on the filesystem for 1 hour to avoid excessive API calls.
166
+
167
+ Args:
168
+ org: Organization slug
169
+ api_key: Optional API key/token for authentication
170
+ auth_type: "api_key" (uses X-Api-Key header) or "bearer" (uses Authorization: Bearer)
171
+ api_host: Cloudsmith API host URL (including version). Taken from the SDK
172
+ configuration default when not provided.
173
+ refresh: When ``True``, skip the cache read and always fetch from the API.
174
+ The fresh result is still written to the cache.
175
+
176
+ Returns:
177
+ List of CustomDomain records.
178
+ Empty list if API call fails or org has no custom domains.
179
+
180
+ Note:
181
+ Only ``ApiException`` is handled here (per-status). Network-layer errors
182
+ (DNS/timeout/SSL/urllib3) are intentionally NOT caught — they propagate to
183
+ the caller. The credential-helper protocol boundary (the click command) and
184
+ the installer are responsible for catching broadly and refusing gracefully,
185
+ so the library stays free of bare ``except Exception`` (reviewer feedback).
186
+ """
187
+ cache_path = get_cache_path(org)
188
+ cached = None if refresh else read_cache(cache_path)
189
+ if cached is not None:
190
+ logger.debug("Using cached custom domains for %s", org)
191
+ return cached
192
+
193
+ logger.debug("Fetching custom domains from API for %s", org)
194
+
195
+ normalized_auth_type: Literal["api_key", "bearer"] = (
196
+ "bearer" if auth_type == "bearer" else "api_key"
197
+ )
198
+ credential = (
199
+ CredentialResult(
200
+ api_key=api_key,
201
+ source_name="credential-helper",
202
+ auth_type=normalized_auth_type,
203
+ )
204
+ if api_key
205
+ else None
206
+ )
207
+ initialise_api(host=api_host, credential=credential)
208
+
209
+ try:
210
+ raw_domains = list_custom_domains(org)
211
+ except ApiException as exc:
212
+ if exc.status in (401, 403):
213
+ # Don't cache auth failures - might work later once authenticated.
214
+ logger.debug(
215
+ "Custom domains API requires auth - assuming no custom domains for %s",
216
+ org,
217
+ )
218
+ return []
219
+
220
+ if exc.status == 404:
221
+ # Cache empty result to avoid repeated lookups for a missing org.
222
+ logger.debug("Organization %s not found or has no custom domains", org)
223
+ write_cache(cache_path, [])
224
+ return []
225
+
226
+ if exc.status == 402:
227
+ # Custom domains product feature not enabled - treat as none.
228
+ logger.debug("Custom domains not enabled for %s", org)
229
+ write_cache(cache_path, [])
230
+ return []
231
+
232
+ logger.debug("Failed to fetch custom domains for %s: HTTP %s", org, exc.status)
233
+ return []
234
+
235
+ records = [
236
+ CustomDomain(
237
+ host=d["host"],
238
+ backend_kind=d.get("backend_kind"),
239
+ enabled=bool(d.get("enabled", False)),
240
+ validated=bool(d.get("validated", False)),
241
+ )
242
+ for d in raw_domains
243
+ if d.get("host")
244
+ ]
245
+
246
+ logger.debug("Fetched %d custom domains for %s", len(records), org)
247
+ write_cache(cache_path, records)
248
+ return records
249
+
250
+
251
+ def get_format_domains(
252
+ org: str,
253
+ backend_kind: int,
254
+ *,
255
+ api_key: str | None = None,
256
+ auth_type: str = "api_key",
257
+ api_host: str | None = None,
258
+ refresh: bool = False,
259
+ ) -> list[str]:
260
+ """
261
+ Return enabled and validated custom domain hostnames for a specific backend format.
262
+
263
+ Args:
264
+ org: Organization slug
265
+ backend_kind: BackendKind int value (e.g. BackendKind.DOCKER == 6)
266
+ api_key: Optional API key/token for authentication
267
+ auth_type: "api_key" or "bearer"
268
+ api_host: Cloudsmith API host URL
269
+ refresh: When ``True``, bypass the cache and fetch fresh data from the API.
270
+
271
+ Returns:
272
+ List of hostnames that are enabled, validated, and match the given backend_kind.
273
+ """
274
+ domains = get_custom_domains(
275
+ org, api_key=api_key, auth_type=auth_type, api_host=api_host, refresh=refresh
276
+ )
277
+ return [
278
+ d.host
279
+ for d in domains
280
+ if d.backend_kind == int(backend_kind) and d.enabled and d.validated
281
+ ]
@@ -0,0 +1,4 @@
1
+ # Copyright 2026 Cloudsmith Ltd
2
+ from .runtime import execute, get_credentials
3
+
4
+ __all__ = ["execute", "get_credentials"]