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,635 @@
1
+ """CLI - Configuration."""
2
+
3
+ import os
4
+ import re
5
+ import threading
6
+
7
+ import click
8
+ from click_configfile import ConfigFileReader, Param, SectionSchema, matches_section
9
+
10
+ from ..core.utils import get_data_path, read_file
11
+ from . import utils, validators
12
+
13
+ OPTIONS = threading.local()
14
+
15
+
16
+ class ConfigParam(Param):
17
+ # For compatibility with click>=7.0
18
+ def __init__(self, *args, **kwargs):
19
+ super().__init__(*args, **kwargs)
20
+ self.ctx = None
21
+
22
+ def parse(self, text):
23
+ if text:
24
+ text = text.strip()
25
+ if self.type.name == "boolean":
26
+ if not text:
27
+ return None
28
+ return super().parse(text)
29
+
30
+ def get_error_hint(self, ctx):
31
+ if self.ctx:
32
+ files = []
33
+ for path in self.ctx.config_searchpath:
34
+ for filename in self.ctx.config_files:
35
+ files.append(os.path.join(path, filename))
36
+ files = " or ".join(files)
37
+ msg = f"{self.name} in {files}"
38
+ else:
39
+ msg = f"{self.name} in a config file"
40
+ return msg
41
+
42
+
43
+ def get_default_config_path():
44
+ """Get the default path to cloudsmith config files."""
45
+ return click.get_app_dir("cloudsmith") # only returns a single path
46
+
47
+
48
+ _CFG_SEARCH_PATHS = (
49
+ ".",
50
+ get_default_config_path(),
51
+ os.path.expanduser("~/.cloudsmith"),
52
+ )
53
+
54
+
55
+ class ConfigSchema:
56
+ """Schema for standard configuration."""
57
+
58
+ @matches_section("default")
59
+ class Default(SectionSchema):
60
+ """Default configuration schema."""
61
+
62
+ api_headers = ConfigParam(name="api_headers", type=str)
63
+ api_host = ConfigParam(name="api_host", type=str)
64
+ api_proxy = ConfigParam(name="api_proxy", type=str)
65
+ api_ssl_verify = ConfigParam(name="api_ssl_verify", type=bool, default=True)
66
+ api_user_agent = ConfigParam(name="api_user_agent", type=str)
67
+ mcp_allowed_tools = ConfigParam(name="mcp_allowed_tools", type=str)
68
+ mcp_allowed_tool_groups = ConfigParam(name="mcp_allowed_tool_groups", type=str)
69
+ oidc_audience = ConfigParam(name="oidc_audience", type=str)
70
+ oidc_org = ConfigParam(name="oidc_org", type=str)
71
+ oidc_service_slug = ConfigParam(name="oidc_service_slug", type=str)
72
+ oidc_detector_order = ConfigParam(name="oidc_detector_order", type=str)
73
+ oidc_disabled_detectors = ConfigParam(name="oidc_disabled_detectors", type=str)
74
+ metadata_failure_mode = ConfigParam(name="metadata_failure_mode", type=str)
75
+
76
+ @matches_section("profile:*")
77
+ class Profile(Default):
78
+ """Profile-specific configuration schema."""
79
+
80
+
81
+ class ConfigReader(ConfigFileReader):
82
+ """Reader for standard configuration."""
83
+
84
+ config_files = ["config.ini"]
85
+ config_name = "standard"
86
+ config_searchpath = list(_CFG_SEARCH_PATHS)
87
+ config_section_schemas = [ConfigSchema.Default, ConfigSchema.Profile]
88
+
89
+ @classmethod
90
+ def select_config_schema_for(cls, section_name):
91
+ section_schema = super().select_config_schema_for(section_name)
92
+ for v in section_schema.__dict__.values():
93
+ if isinstance(v, ConfigParam):
94
+ v.ctx = cls
95
+ return section_schema
96
+
97
+ @classmethod
98
+ def get_storage_name_for(cls, section_name):
99
+ """Get storage name for a configuration section."""
100
+ if not section_name or section_name == "default":
101
+ return "default"
102
+ return section_name
103
+
104
+ @classmethod
105
+ def get_default_filepath(cls):
106
+ """Get the default filepath for the configuration file."""
107
+ if not cls.config_files:
108
+ return None
109
+ if not cls.config_searchpath:
110
+ return None
111
+ filename = cls.config_files[0]
112
+ filepath = cls.config_searchpath[0]
113
+ return os.path.join(filepath, filename)
114
+
115
+ @classmethod
116
+ def create_default_file(cls, data=None, mode=None):
117
+ """Create a config file and override data if specified."""
118
+ filepath = cls.get_default_filepath()
119
+ if not filepath:
120
+ return False
121
+
122
+ filename = os.path.basename(filepath)
123
+ config = read_file(get_data_path(), filename)
124
+
125
+ # Find and replace data in default config
126
+ data = data or {}
127
+ for k, v in data.items():
128
+ v = v or ""
129
+ config = re.sub(
130
+ rf"^({k})\s*=\s*$",
131
+ f"{k} = {v}",
132
+ config,
133
+ flags=re.MULTILINE,
134
+ )
135
+
136
+ dirpath = os.path.dirname(filepath)
137
+ if not os.path.exists(dirpath):
138
+ os.makedirs(dirpath)
139
+ with click.open_file(filepath, "w+") as f:
140
+ f.write(config)
141
+
142
+ if mode is not None:
143
+ os.chmod(filepath, mode)
144
+
145
+ return True
146
+
147
+ @classmethod
148
+ def has_default_file(cls):
149
+ """Check if a configuration file exists."""
150
+ for filename in cls.config_files:
151
+ for searchpath in cls.config_searchpath:
152
+ path = os.path.join(searchpath, filename)
153
+ if os.path.exists(path):
154
+ return True
155
+
156
+ return False
157
+
158
+ @classmethod
159
+ def read_relative_config_value(cls, key, profile=None):
160
+ """Read `key` as loaded from directory-relative (untrusted) configs only.
161
+
162
+ Loads via the normal machinery but through a reader narrowed to the
163
+ relative search paths/files (e.g. the current working directory);
164
+ absolute/user-level paths and any explicit ``--config-file`` are
165
+ excluded. The value is parsed and quote-stripped exactly as a real run
166
+ would. Returns None when the key is absent or empty.
167
+ """
168
+ relative_searchpath = [p for p in cls.config_searchpath if not os.path.isabs(p)]
169
+ relative_files = [f for f in cls.config_files if not os.path.isabs(f)]
170
+ if not relative_searchpath or not relative_files:
171
+ return None
172
+
173
+ relative_reader = type(
174
+ "RelativeConfigReader",
175
+ (cls,),
176
+ {"config_searchpath": relative_searchpath, "config_files": relative_files},
177
+ )
178
+ probe = Options()
179
+ relative_reader.load_config(probe, profile=profile)
180
+ return probe.opts.get(key)
181
+
182
+ @classmethod
183
+ def load_config(cls, opts, path=None, profile=None):
184
+ """Load a configuration file into an options object."""
185
+ if path and os.path.exists(path):
186
+ if os.path.isdir(path):
187
+ cls.config_searchpath.insert(0, path)
188
+ else:
189
+ cls.config_files.insert(0, path)
190
+
191
+ config = cls.read_config()
192
+ values = config.get("default", {})
193
+ cls._load_values_into_opts(opts, values)
194
+
195
+ if profile and profile != "default":
196
+ values = config.get("profile:%s" % profile, {})
197
+ cls._load_values_into_opts(opts, values)
198
+
199
+ return values
200
+
201
+ @staticmethod
202
+ def _load_values_into_opts(opts, values):
203
+ for k, v in values.items():
204
+ if v is None:
205
+ continue
206
+ if isinstance(v, str):
207
+ if v.startswith('"') or v.startswith("'"):
208
+ v = v[1:]
209
+ if v.endswith('"') or v.endswith("'"):
210
+ v = v[:-1]
211
+ if not v:
212
+ continue
213
+ else:
214
+ if v is None:
215
+ continue
216
+ setattr(opts, k, v)
217
+
218
+
219
+ class CredentialsSchema:
220
+ """Schema for credentials configuration."""
221
+
222
+ @matches_section("default")
223
+ class Default(SectionSchema):
224
+ """Default configuration schema."""
225
+
226
+ api_key = ConfigParam(name="api_key", type=str)
227
+
228
+ @matches_section("profile:*")
229
+ class Profile(Default):
230
+ """Profile-specific configuration schema."""
231
+
232
+
233
+ class CredentialsReader(ConfigReader):
234
+ """Reader for credentials configuration."""
235
+
236
+ config_files = ["credentials.ini"]
237
+ config_name = "credentials"
238
+ config_searchpath = list(_CFG_SEARCH_PATHS)
239
+ config_section_schemas = [CredentialsSchema.Default, CredentialsSchema.Profile]
240
+
241
+ @classmethod
242
+ def find_existing_files(cls):
243
+ """Return a list of existing credentials file paths."""
244
+ paths = []
245
+ seen = set()
246
+ for filename in cls.config_files:
247
+ for searchpath in cls.config_searchpath:
248
+ path = os.path.join(searchpath, filename)
249
+ if os.path.exists(path) and path not in seen:
250
+ paths.append(path)
251
+ seen.add(path)
252
+ return paths
253
+
254
+ @classmethod
255
+ def _set_api_key(cls, path, api_key=""):
256
+ """Write api_key value in a credentials file, preserving structure."""
257
+ with open(path) as f:
258
+ content = f.read()
259
+ replacement = rf"\1 = {api_key}" if api_key else r"\1 ="
260
+ content = re.sub(
261
+ r"^(api_key)\s*=\s*.*$",
262
+ replacement,
263
+ content,
264
+ flags=re.MULTILINE,
265
+ )
266
+ with open(path, "w") as f:
267
+ f.write(content)
268
+
269
+ @classmethod
270
+ def clear_api_key(cls, path):
271
+ """Clear api_key values in a credentials file, preserving structure."""
272
+ cls._set_api_key(path)
273
+
274
+ @classmethod
275
+ def update_api_key(cls, path, api_key):
276
+ """Update api_key value in an existing credentials file, preserving structure."""
277
+ cls._set_api_key(path, api_key)
278
+
279
+
280
+ class Options: # pylint: disable=too-many-public-methods
281
+ """Options object that holds config for the application."""
282
+
283
+ def __init__(self, *args, **kwargs):
284
+ """Initialise a new Options object."""
285
+ super().__init__(*args, **kwargs)
286
+ self.opts = {}
287
+ for k, v in kwargs.items():
288
+ setattr(self, k, v)
289
+
290
+ @staticmethod
291
+ def get_config_reader():
292
+ """Get the non-credentials config reader class."""
293
+ return ConfigReader
294
+
295
+ @staticmethod
296
+ def get_creds_reader():
297
+ """Get the credentials config reader class."""
298
+ return CredentialsReader
299
+
300
+ def load_config_file(self, path, profile=None):
301
+ """Load the standard config file."""
302
+ config_cls = self.get_config_reader()
303
+ return config_cls.load_config(self, path, profile=profile)
304
+
305
+ def load_creds_file(self, path, profile=None):
306
+ """Load the credentials config file."""
307
+ config_cls = self.get_creds_reader()
308
+ return config_cls.load_config(self, path, profile=profile)
309
+
310
+ @property
311
+ def api_config(self):
312
+ """Get value for API config dictionary."""
313
+ return self._get_option("api_config")
314
+
315
+ @api_config.setter
316
+ def api_config(self, value):
317
+ """Set value for API config dictionary."""
318
+ self._set_option("api_config", value)
319
+
320
+ @property
321
+ def api_headers(self):
322
+ """Get value for API headers."""
323
+ return self._get_option("api_headers")
324
+
325
+ @api_headers.setter
326
+ def api_headers(self, value):
327
+ """Set value for API headers."""
328
+ value = validators.validate_api_headers("api_headers", value)
329
+ self._set_option("api_headers", value)
330
+
331
+ @property
332
+ def api_host(self):
333
+ """Get value for API host."""
334
+ return self._get_option("api_host")
335
+
336
+ @api_host.setter
337
+ def api_host(self, value):
338
+ """Set value for API host."""
339
+ self._set_option("api_host", value)
340
+
341
+ @property
342
+ def api_key(self):
343
+ """Get value for API key."""
344
+ return self._get_option("api_key")
345
+
346
+ @api_key.setter
347
+ def api_key(self, value):
348
+ """Set value for API key."""
349
+ self._set_option("api_key", value)
350
+
351
+ @property
352
+ def api_key_from_flag(self):
353
+ """Get API key set explicitly via --api-key CLI flag."""
354
+ return self._get_option("api_key_from_flag")
355
+
356
+ @api_key_from_flag.setter
357
+ def api_key_from_flag(self, value):
358
+ self._set_option("api_key_from_flag", value, allow_clear=True)
359
+
360
+ @property
361
+ def api_key_from_env(self):
362
+ """Get API key from CLOUDSMITH_API_KEY environment variable."""
363
+ return self._get_option("api_key_from_env")
364
+
365
+ @api_key_from_env.setter
366
+ def api_key_from_env(self, value):
367
+ self._set_option("api_key_from_env", value, allow_clear=True)
368
+
369
+ @property
370
+ def api_key_from_file(self):
371
+ """Get API key loaded from credentials.ini."""
372
+ return self._get_option("api_key_from_file")
373
+
374
+ @api_key_from_file.setter
375
+ def api_key_from_file(self, value):
376
+ self._set_option("api_key_from_file", value, allow_clear=True)
377
+
378
+ @property
379
+ def api_proxy(self):
380
+ """Get value for API proxy."""
381
+ return self._get_option("api_proxy")
382
+
383
+ @api_proxy.setter
384
+ def api_proxy(self, value):
385
+ """Set value for API proxy."""
386
+ self._set_option("api_proxy", value)
387
+
388
+ @property
389
+ def api_ssl_verify(self):
390
+ """Get value for API SSL verify."""
391
+ return self._get_option("api_ssl_verify", default=True)
392
+
393
+ @api_ssl_verify.setter
394
+ def api_ssl_verify(self, value):
395
+ """Set value for API SSL verify."""
396
+ self._set_option("api_ssl_verify", value, allow_clear=False)
397
+
398
+ @property
399
+ def api_user_agent(self):
400
+ """Get value for API user agent."""
401
+ return utils.make_user_agent(prefix=self._get_option("api_user_agent"))
402
+
403
+ @api_user_agent.setter
404
+ def api_user_agent(self, value):
405
+ """Set value for API user agent."""
406
+ self._set_option("api_user_agent", value)
407
+
408
+ @property
409
+ def rate_limit(self):
410
+ """Get value for rate limiting."""
411
+ return self._get_option("rate_limit", default=True)
412
+
413
+ @rate_limit.setter
414
+ def rate_limit(self, value):
415
+ """Set value for rate limiting."""
416
+ self._set_option("rate_limit", value)
417
+
418
+ @property
419
+ def rate_limit_warning(self):
420
+ """Get value for rate limiting warning (in seconds)."""
421
+ return self._get_option("rate_limit_warning", default=10)
422
+
423
+ @rate_limit_warning.setter
424
+ def rate_limit_warning(self, value):
425
+ """Set value for rate limiting warning (in seconds)."""
426
+ self._set_option("rate_limit_warning", value)
427
+
428
+ @property
429
+ def always_show_rate_limit(self):
430
+ """Get value for rate limiting warning (in seconds)."""
431
+ return self._get_option("always_show_rate_limit", default=False)
432
+
433
+ @always_show_rate_limit.setter
434
+ def always_show_rate_limit(self, value):
435
+ """Set value for rate limiting warning (in seconds)."""
436
+ self._set_option("always_show_rate_limit", value)
437
+
438
+ @property
439
+ def debug(self):
440
+ """Get value for debug flag."""
441
+ return self._get_option("debug", default=False)
442
+
443
+ @debug.setter
444
+ def debug(self, value):
445
+ """Set value for debug flag."""
446
+ self._set_option("debug", bool(value))
447
+
448
+ @property
449
+ def mcp_allowed_tools(self):
450
+ """Get value for Allowed MCP Tools."""
451
+ return self._get_option("mcp_allowed_tools")
452
+
453
+ @mcp_allowed_tools.setter
454
+ def mcp_allowed_tools(self, value):
455
+ """Set value for Allowed MCP Tools."""
456
+ if not value:
457
+ return
458
+ tools = [tool.strip() for tool in value.split(",")]
459
+
460
+ self._set_option("mcp_allowed_tools", tools)
461
+
462
+ @property
463
+ def mcp_allowed_tool_groups(self):
464
+ """Get value for Allowed MCP Tool Groups."""
465
+ return self._get_option("mcp_allowed_tool_groups")
466
+
467
+ @mcp_allowed_tool_groups.setter
468
+ def mcp_allowed_tool_groups(self, value):
469
+ """Set value for Allowed MCP Tool Groups."""
470
+ if not value:
471
+ return
472
+ tool_groups = [group.strip() for group in value.split(",")]
473
+
474
+ self._set_option("mcp_allowed_tool_groups", tool_groups)
475
+
476
+ @property
477
+ def oidc_audience(self):
478
+ """Get value for OIDC audience."""
479
+ return self._get_option("oidc_audience")
480
+
481
+ @oidc_audience.setter
482
+ def oidc_audience(self, value):
483
+ """Set value for OIDC audience."""
484
+ self._set_option("oidc_audience", value)
485
+
486
+ @property
487
+ def oidc_org(self):
488
+ """Get value for OIDC organisation slug."""
489
+ return self._get_option("oidc_org")
490
+
491
+ @oidc_org.setter
492
+ def oidc_org(self, value):
493
+ """Set value for OIDC organisation slug."""
494
+ self._set_option("oidc_org", value)
495
+
496
+ @property
497
+ def oidc_service_slug(self):
498
+ """Get value for OIDC service slug."""
499
+ return self._get_option("oidc_service_slug")
500
+
501
+ @oidc_service_slug.setter
502
+ def oidc_service_slug(self, value):
503
+ """Set value for OIDC service slug."""
504
+ self._set_option("oidc_service_slug", value)
505
+
506
+ @property
507
+ def oidc_discovery_disabled(self):
508
+ """Get value for OIDC discovery disabled flag."""
509
+ return self._get_option("oidc_discovery_disabled", default=False)
510
+
511
+ @oidc_discovery_disabled.setter
512
+ def oidc_discovery_disabled(self, value):
513
+ """Set value for OIDC discovery disabled flag."""
514
+ self._set_option(
515
+ "oidc_discovery_disabled", bool(value) if value is not None else False
516
+ )
517
+
518
+ @property
519
+ def oidc_detector_order(self):
520
+ """Get value for the OIDC detector evaluation order."""
521
+ return self._get_option("oidc_detector_order")
522
+
523
+ @oidc_detector_order.setter
524
+ def oidc_detector_order(self, value):
525
+ """Set value for the OIDC detector evaluation order."""
526
+ self._set_option("oidc_detector_order", value)
527
+
528
+ @property
529
+ def oidc_disabled_detectors(self):
530
+ """Get the comma-separated OIDC detector ids disabled via config."""
531
+ return self._get_option("oidc_disabled_detectors")
532
+
533
+ @oidc_disabled_detectors.setter
534
+ def oidc_disabled_detectors(self, value):
535
+ """Set the comma-separated OIDC detector ids disabled via config."""
536
+ self._set_option("oidc_disabled_detectors", value)
537
+
538
+ @property
539
+ def metadata_failure_mode(self):
540
+ """Get value for push-time metadata failure mode."""
541
+ return self._get_option("metadata_failure_mode")
542
+
543
+ @metadata_failure_mode.setter
544
+ def metadata_failure_mode(self, value):
545
+ """Set value for push-time metadata failure mode."""
546
+ if value is None:
547
+ return
548
+ normalised = str(value).strip().lower()
549
+ if normalised not in {"error", "warn", "0"}:
550
+ raise click.UsageError(
551
+ f"Invalid metadata_failure_mode {value!r}. "
552
+ "Expected one of: 'error', 'warn', '0'."
553
+ )
554
+ self._set_option("metadata_failure_mode", normalised)
555
+
556
+ @property
557
+ def output(self):
558
+ """Get value for output format."""
559
+ return self._get_option("output")
560
+
561
+ @output.setter
562
+ def output(self, value):
563
+ """Set value for output format."""
564
+ self._set_option("output", value)
565
+
566
+ @property
567
+ def verbose(self):
568
+ """Get value for verbose flag."""
569
+ return self._get_option("verbose", default=False)
570
+
571
+ @verbose.setter
572
+ def verbose(self, value):
573
+ """Set value for verbose flag."""
574
+ self._set_option("verbose", bool(value))
575
+
576
+ @property
577
+ def error_retry_max(self):
578
+ """Get value for error_retry_max."""
579
+ return self._get_option("error_retry_max", default=5)
580
+
581
+ @error_retry_max.setter
582
+ def error_retry_max(self, value):
583
+ """Set value for error_retry_max."""
584
+ self._set_option("error_retry_max", int(value))
585
+
586
+ @property
587
+ def error_retry_backoff(self):
588
+ """Get value for error_retry_backoff."""
589
+ return self._get_option("error_retry_backoff", default=0.23)
590
+
591
+ @error_retry_backoff.setter
592
+ def error_retry_backoff(self, value):
593
+ """Set value for error_retry_backoff."""
594
+ self._set_option("error_retry_backoff", float(value))
595
+
596
+ @property
597
+ def error_retry_codes(self):
598
+ """Get value for error_retry_codes."""
599
+ return self._get_option("error_retry_codes", default=[500, 502, 503, 504])
600
+
601
+ @error_retry_codes.setter
602
+ def error_retry_codes(self, value):
603
+ """Set value for error_retry_codes."""
604
+ if isinstance(value, str):
605
+ value = [int(x) for x in value.split(",")]
606
+ self._set_option("error_retry_codes", value)
607
+
608
+ def _get_option(self, name, default=None):
609
+ """Get value for an option."""
610
+ value = self.opts.get(name)
611
+ if value is None:
612
+ return default
613
+ return value
614
+
615
+ def _set_option(self, name, value, allow_clear=False):
616
+ """Set value for an option."""
617
+ if not allow_clear:
618
+ # Prevent clears if value was set
619
+ try:
620
+ current_value = self._get_option(name)
621
+ if value is None and current_value is not None:
622
+ return
623
+ except AttributeError:
624
+ pass
625
+
626
+ self.opts[name] = value
627
+
628
+
629
+ def get_or_create_options(ctx):
630
+ """Get or create the options object."""
631
+ try:
632
+ return OPTIONS.value
633
+ except AttributeError:
634
+ OPTIONS.value = ctx.ensure_object(Options)
635
+ return OPTIONS.value