solidworks-mcp-python 1.5.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 (115) hide show
  1. solidworks_mcp/__init__.py +57 -0
  2. solidworks_mcp/adapters/__init__.py +40 -0
  3. solidworks_mcp/adapters/base.py +1882 -0
  4. solidworks_mcp/adapters/circuit_breaker.py +2052 -0
  5. solidworks_mcp/adapters/com_executor.py +252 -0
  6. solidworks_mcp/adapters/complexity_analyzer.py +272 -0
  7. solidworks_mcp/adapters/connection_pool.py +1884 -0
  8. solidworks_mcp/adapters/factory.py +366 -0
  9. solidworks_mcp/adapters/intelligent_router.py +218 -0
  10. solidworks_mcp/adapters/mock_adapter.py +4016 -0
  11. solidworks_mcp/adapters/pywin32_adapter.py +2842 -0
  12. solidworks_mcp/adapters/solidworks/__init__.py +17 -0
  13. solidworks_mcp/adapters/solidworks/bitmap.py +180 -0
  14. solidworks_mcp/adapters/solidworks/com_utils.py +56 -0
  15. solidworks_mcp/adapters/solidworks/dimension_layout.py +753 -0
  16. solidworks_mcp/adapters/solidworks/documentation.py +1476 -0
  17. solidworks_mcp/adapters/solidworks/drawing_standard.py +1993 -0
  18. solidworks_mcp/adapters/solidworks/face_selection.py +392 -0
  19. solidworks_mcp/adapters/solidworks/features.py +3362 -0
  20. solidworks_mcp/adapters/solidworks/io.py +3649 -0
  21. solidworks_mcp/adapters/solidworks/rebuild_diagnostics.py +269 -0
  22. solidworks_mcp/adapters/solidworks/rebuild_inspector.py +146 -0
  23. solidworks_mcp/adapters/solidworks/selection.py +308 -0
  24. solidworks_mcp/adapters/solidworks/sketch.py +2545 -0
  25. solidworks_mcp/adapters/sw_feature_errors.py +66 -0
  26. solidworks_mcp/adapters/sw_type_info.py +317 -0
  27. solidworks_mcp/adapters/vba_adapter.py +318 -0
  28. solidworks_mcp/adapters/vba_macro_executor.py +215 -0
  29. solidworks_mcp/agents/__init__.py +68 -0
  30. solidworks_mcp/agents/design_knowledge/assembly_patterns.md +127 -0
  31. solidworks_mcp/agents/design_knowledge/hinges.md +118 -0
  32. solidworks_mcp/agents/design_knowledge/snap_fits.md +93 -0
  33. solidworks_mcp/agents/design_knowledge/solidworks_basics.md +3 -0
  34. solidworks_mcp/agents/design_knowledge/tolerancing.md +128 -0
  35. solidworks_mcp/agents/harness.py +216 -0
  36. solidworks_mcp/agents/history_db.py +1508 -0
  37. solidworks_mcp/agents/retrieval_index.py +164 -0
  38. solidworks_mcp/agents/schemas.py +165 -0
  39. solidworks_mcp/agents/smoke_test.py +285 -0
  40. solidworks_mcp/agents/soc_exporter.py +650 -0
  41. solidworks_mcp/agents/soc_pickup.py +332 -0
  42. solidworks_mcp/agents/soc_rewind.py +221 -0
  43. solidworks_mcp/agents/vector_rag.py +665 -0
  44. solidworks_mcp/cache/__init__.py +5 -0
  45. solidworks_mcp/cache/response_cache.py +164 -0
  46. solidworks_mcp/config.py +729 -0
  47. solidworks_mcp/exceptions.py +37 -0
  48. solidworks_mcp/security/__init__.py +68 -0
  49. solidworks_mcp/security/auth.py +128 -0
  50. solidworks_mcp/security/cors.py +27 -0
  51. solidworks_mcp/security/rate_limiting.py +90 -0
  52. solidworks_mcp/security/runtime.py +155 -0
  53. solidworks_mcp/server.py +777 -0
  54. solidworks_mcp/server_cli_fixed.py +120 -0
  55. solidworks_mcp/setup/__init__.py +1 -0
  56. solidworks_mcp/setup/cli.py +399 -0
  57. solidworks_mcp/setup/clients.py +157 -0
  58. solidworks_mcp/setup/config_file.py +230 -0
  59. solidworks_mcp/setup/handshake.py +202 -0
  60. solidworks_mcp/setup/launcher.py +102 -0
  61. solidworks_mcp/setup/legacy.py +100 -0
  62. solidworks_mcp/setup/registration.py +193 -0
  63. solidworks_mcp/setup/solidworks.py +49 -0
  64. solidworks_mcp/tools/__init__.py +64 -0
  65. solidworks_mcp/tools/analysis.py +723 -0
  66. solidworks_mcp/tools/automation.py +578 -0
  67. solidworks_mcp/tools/docs_discovery.py +1476 -0
  68. solidworks_mcp/tools/drawing.py +1015 -0
  69. solidworks_mcp/tools/drawing_analysis.py +456 -0
  70. solidworks_mcp/tools/drawing_automation.py +627 -0
  71. solidworks_mcp/tools/engineering_data.py +466 -0
  72. solidworks_mcp/tools/error_classifier.py +232 -0
  73. solidworks_mcp/tools/export.py +833 -0
  74. solidworks_mcp/tools/file_facts.py +40 -0
  75. solidworks_mcp/tools/file_management.py +1473 -0
  76. solidworks_mcp/tools/input_compat.py +17 -0
  77. solidworks_mcp/tools/macro_recording.py +464 -0
  78. solidworks_mcp/tools/modeling.py +2360 -0
  79. solidworks_mcp/tools/sketching.py +1422 -0
  80. solidworks_mcp/tools/template_management.py +367 -0
  81. solidworks_mcp/tools/tool_boundary.py +280 -0
  82. solidworks_mcp/tools/vba_generation.py +1145 -0
  83. solidworks_mcp/ui/__init__.py +9 -0
  84. solidworks_mcp/ui/local_llm.py +725 -0
  85. solidworks_mcp/ui/routers/__init__.py +10 -0
  86. solidworks_mcp/ui/routers/checkpoint.py +24 -0
  87. solidworks_mcp/ui/routers/docs.py +47 -0
  88. solidworks_mcp/ui/routers/llm.py +83 -0
  89. solidworks_mcp/ui/routers/local_model.py +69 -0
  90. solidworks_mcp/ui/routers/model.py +75 -0
  91. solidworks_mcp/ui/routers/preview.py +38 -0
  92. solidworks_mcp/ui/routers/session.py +161 -0
  93. solidworks_mcp/ui/routers/viewer.py +154 -0
  94. solidworks_mcp/ui/schemas.py +502 -0
  95. solidworks_mcp/ui/server.py +298 -0
  96. solidworks_mcp/ui/service.py +481 -0
  97. solidworks_mcp/ui/services/__init__.py +136 -0
  98. solidworks_mcp/ui/services/_utils.py +958 -0
  99. solidworks_mcp/ui/services/checkpoint_service.py +1110 -0
  100. solidworks_mcp/ui/services/docs_service.py +282 -0
  101. solidworks_mcp/ui/services/llm_service.py +956 -0
  102. solidworks_mcp/ui/services/model_service.py +459 -0
  103. solidworks_mcp/ui/services/parameter_repair_service.py +314 -0
  104. solidworks_mcp/ui/services/preview_service.py +489 -0
  105. solidworks_mcp/ui/services/session_service.py +1389 -0
  106. solidworks_mcp/utils/__init__.py +9 -0
  107. solidworks_mcp/utils/feature_tree_classifier.py +342 -0
  108. solidworks_mcp/utils/logging.py +76 -0
  109. solidworks_mcp/utils/validation.py +77 -0
  110. solidworks_mcp/version.py +8 -0
  111. solidworks_mcp_python-1.5.0.dist-info/METADATA +410 -0
  112. solidworks_mcp_python-1.5.0.dist-info/RECORD +115 -0
  113. solidworks_mcp_python-1.5.0.dist-info/WHEEL +4 -0
  114. solidworks_mcp_python-1.5.0.dist-info/entry_points.txt +3 -0
  115. solidworks_mcp_python-1.5.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,37 @@
1
+ """Custom exceptions for SolidWorks MCP Server."""
2
+
3
+
4
+ class SolidWorksMCPError(Exception):
5
+ """Base exception for SolidWorks MCP Server errors."""
6
+
7
+ pass
8
+
9
+
10
+ class SolidWorksConnectionError(SolidWorksMCPError):
11
+ """Exception raised when SolidWorks connection fails."""
12
+
13
+ pass
14
+
15
+
16
+ class SolidWorksOperationError(SolidWorksMCPError):
17
+ """Exception raised when SolidWorks operation fails."""
18
+
19
+ pass
20
+
21
+
22
+ class ConfigurationError(SolidWorksMCPError):
23
+ """Exception raised for configuration issues."""
24
+
25
+ pass
26
+
27
+
28
+ class AuthenticationError(SolidWorksMCPError):
29
+ """Exception raised for authentication failures."""
30
+
31
+ pass
32
+
33
+
34
+ class RateLimitError(SolidWorksMCPError):
35
+ """Exception raised when rate limit is exceeded."""
36
+
37
+ pass
@@ -0,0 +1,68 @@
1
+ """Security module for SolidWorks MCP Server.
2
+
3
+ Provides authentication, authorization, and security features based on the configured
4
+ security level.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from ..config import SolidWorksMCPConfig
10
+ from .auth import setup_authentication, validate_api_key
11
+ from .cors import setup_cors
12
+ from .rate_limiting import setup_rate_limiting
13
+ from .runtime import SecurityEnforcer
14
+
15
+ _security_enforcer: SecurityEnforcer | None = None
16
+
17
+
18
+ def get_security_enforcer() -> SecurityEnforcer | None:
19
+ """Return the active runtime security enforcer, if configured.
20
+
21
+ Returns:
22
+ SecurityEnforcer | None: The result produced by the operation.
23
+ """
24
+ return _security_enforcer
25
+
26
+
27
+ async def setup_security(mcp: Any, config: SolidWorksMCPConfig) -> None:
28
+ """Configure security middleware based on selected security level.
29
+
30
+ Args:
31
+ mcp (Any): The mcp value.
32
+ config (SolidWorksMCPConfig): Configuration values for the operation.
33
+
34
+ Returns:
35
+ None: None.
36
+ """
37
+ from ..config import SecurityLevel
38
+
39
+ if config.security_level == SecurityLevel.MINIMAL:
40
+ # Local only, minimal security
41
+ global _security_enforcer
42
+ _security_enforcer = SecurityEnforcer(config)
43
+ return
44
+
45
+ if config.security_level in [SecurityLevel.STANDARD, SecurityLevel.STRICT]:
46
+ # Setup API key authentication
47
+ if config.api_key:
48
+ setup_authentication(mcp, config)
49
+
50
+ # Setup CORS
51
+ if config.enable_cors:
52
+ setup_cors(mcp, config)
53
+
54
+ # Setup rate limiting
55
+ if config.enable_rate_limiting:
56
+ setup_rate_limiting(mcp, config)
57
+
58
+ _security_enforcer = SecurityEnforcer(config)
59
+
60
+
61
+ __all__ = [
62
+ "setup_security",
63
+ "setup_authentication",
64
+ "validate_api_key",
65
+ "setup_cors",
66
+ "setup_rate_limiting",
67
+ "get_security_enforcer",
68
+ ]
@@ -0,0 +1,128 @@
1
+ """Authentication and authorization for SolidWorks MCP Server."""
2
+
3
+ import secrets
4
+ from collections.abc import Awaitable, Callable
5
+ from functools import wraps
6
+ from typing import Any, TypeVar, cast
7
+
8
+ from ..config import SolidWorksMCPConfig
9
+
10
+ F = TypeVar("F", bound=Callable[..., Awaitable[Any]])
11
+
12
+
13
+ def setup_authentication(mcp: Any, config: SolidWorksMCPConfig) -> None:
14
+ """Configure authentication middleware hooks.
15
+
16
+ Args:
17
+ mcp (Any): The mcp value.
18
+ config (SolidWorksMCPConfig): Configuration values for the operation.
19
+
20
+ Returns:
21
+ None: None.
22
+ """
23
+ api_key = getattr(config, "api_key", None)
24
+ api_keys = getattr(config, "api_keys", [])
25
+ api_key_required = bool(getattr(config, "api_key_required", False))
26
+ auth_mode = "api_key" if (api_key or api_keys or api_key_required) else "none"
27
+ try:
28
+ mcp._security_auth_enabled = True
29
+ mcp._security_auth_mode = auth_mode
30
+ except (AttributeError, TypeError):
31
+ # Some tests intentionally pass plain object() instances without __dict__.
32
+ return
33
+
34
+
35
+ def validate_api_key(provided_key: str, expected_key: str) -> bool:
36
+ """Validate API key using constant-time comparison.
37
+
38
+ Args:
39
+ provided_key (str): The provided key value.
40
+ expected_key (str): The expected key value.
41
+
42
+ Returns:
43
+ bool: True if validate api key, otherwise False.
44
+ """
45
+ if not provided_key or not expected_key:
46
+ return False
47
+
48
+ return secrets.compare_digest(provided_key, expected_key)
49
+
50
+
51
+ def require_auth(config: SolidWorksMCPConfig) -> Callable[[F], F]:
52
+ """Decorate a coroutine with authentication checks.
53
+
54
+ Args:
55
+ config (SolidWorksMCPConfig): Configuration values for the operation.
56
+
57
+ Returns:
58
+ Callable[[F], F]: The result produced by the operation.
59
+ """
60
+
61
+ def decorator(func: F) -> F:
62
+ """Wrap a coroutine with request-level authentication checks.
63
+
64
+ Args:
65
+ func (F): The func value.
66
+
67
+ Returns:
68
+ F: The result produced by the operation.
69
+
70
+ Raises:
71
+ RuntimeError: Authentication failed: invalid api_key.
72
+ """
73
+
74
+ @wraps(func)
75
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
76
+ """Run the wrapped coroutine after API key validation.
77
+
78
+ Args:
79
+ *args (Any): Additional positional arguments forwarded to the call.
80
+ **kwargs (Any): Additional keyword arguments forwarded to the call.
81
+
82
+ Returns:
83
+ Any: The result produced by the operation.
84
+
85
+ Raises:
86
+ RuntimeError: Authentication failed: invalid api_key.
87
+ """
88
+ security_level = str(getattr(config, "security_level", "minimal"))
89
+ if security_level == "minimal":
90
+ return await func(*args, **kwargs)
91
+
92
+ api_key_required = bool(getattr(config, "api_key_required", False))
93
+ api_key = getattr(config, "api_key", None)
94
+ api_keys = getattr(config, "api_keys", [])
95
+ if not (api_key_required or api_key or api_keys):
96
+ return await func(*args, **kwargs)
97
+
98
+ payload = kwargs.get("input_data")
99
+ if payload is None and args:
100
+ payload = args[0]
101
+
102
+ payload_dict: dict[str, Any] = {}
103
+ if payload is not None and hasattr(payload, "model_dump"):
104
+ payload_dict = cast(dict[str, Any], payload.model_dump())
105
+ elif isinstance(payload, dict):
106
+ payload_dict = payload
107
+
108
+ provided_key_value = payload_dict.get("api_key")
109
+ provided_key = str(provided_key_value) if provided_key_value else ""
110
+
111
+ expected_key = ""
112
+ if api_key is not None and hasattr(api_key, "get_secret_value"):
113
+ expected_key = api_key.get_secret_value()
114
+ elif isinstance(api_key, str):
115
+ expected_key = api_key
116
+ elif api_keys:
117
+ expected_key = api_keys[0]
118
+
119
+ if not validate_api_key(
120
+ provided_key=provided_key, expected_key=expected_key
121
+ ):
122
+ raise RuntimeError("authentication failed: invalid api_key")
123
+
124
+ return await func(*args, **kwargs)
125
+
126
+ return wrapper # type: ignore[return-value]
127
+
128
+ return decorator
@@ -0,0 +1,27 @@
1
+ """CORS (Cross-Origin Resource Sharing) configuration for remote deployments."""
2
+
3
+ from typing import Any
4
+
5
+ from ..config import SolidWorksMCPConfig
6
+
7
+
8
+ def setup_cors(mcp: Any, config: SolidWorksMCPConfig) -> None:
9
+ """Configure CORS middleware for remote deployments.
10
+
11
+ Args:
12
+ mcp (Any): The mcp value.
13
+ config (SolidWorksMCPConfig): Configuration values for the operation.
14
+
15
+ Returns:
16
+ None: None.
17
+ """
18
+ cors_origins = getattr(config, "cors_origins", [])
19
+ allowed_origins = getattr(config, "allowed_origins", [])
20
+ enable_cors = bool(getattr(config, "enable_cors", False))
21
+ origins = cors_origins or allowed_origins
22
+ try:
23
+ mcp._security_cors_enabled = enable_cors
24
+ mcp._security_cors_origins = list(origins)
25
+ except (AttributeError, TypeError):
26
+ # Some tests intentionally pass plain object() instances without __dict__.
27
+ return
@@ -0,0 +1,90 @@
1
+ """Rate limiting for API endpoints."""
2
+
3
+ import time
4
+ from collections import defaultdict
5
+ from typing import Any
6
+
7
+ from ..config import SolidWorksMCPConfig
8
+
9
+
10
+ class RateLimiter:
11
+ """Simple in-memory rate limiter.
12
+
13
+ Args:
14
+ max_requests (int): The max requests value.
15
+ time_window (int): The time window value. Defaults to 60.
16
+
17
+ Attributes:
18
+ max_requests (Any): The max requests value.
19
+ time_window (Any): The time window value.
20
+ """
21
+
22
+ def __init__(self, max_requests: int, time_window: int = 60):
23
+ """Initialize the rate limiter.
24
+
25
+ Args:
26
+ max_requests (int): The max requests value.
27
+ time_window (int): The time window value. Defaults to 60.
28
+ """
29
+ self.max_requests = max_requests
30
+ self.time_window = time_window
31
+ self.requests: dict[str, list[float]] = defaultdict(list)
32
+
33
+ def is_allowed(self, client_id: str) -> bool:
34
+ """Check if client is within rate limits.
35
+
36
+ Args:
37
+ client_id (str): The client id value.
38
+
39
+ Returns:
40
+ bool: True if allowed, otherwise False.
41
+ """
42
+ now = time.time()
43
+ window_start = now - self.time_window
44
+
45
+ # Remove old requests
46
+ self.requests[client_id] = [
47
+ req_time for req_time in self.requests[client_id] if req_time > window_start
48
+ ]
49
+
50
+ # Check if under limit
51
+ if len(self.requests[client_id]) < self.max_requests:
52
+ self.requests[client_id].append(now)
53
+ return True
54
+
55
+ return False
56
+
57
+
58
+ # Global rate limiter instance
59
+ _rate_limiter: RateLimiter | None = None
60
+
61
+
62
+ def setup_rate_limiting(mcp: Any, config: SolidWorksMCPConfig) -> None:
63
+ """Initialize in-memory rate limiting.
64
+
65
+ Args:
66
+ mcp (Any): The mcp value.
67
+ config (SolidWorksMCPConfig): Configuration values for the operation.
68
+
69
+ Returns:
70
+ None: None.
71
+ """
72
+ global _rate_limiter
73
+ _rate_limiter = RateLimiter(
74
+ max_requests=config.rate_limit_per_minute,
75
+ time_window=60, # 1 minute
76
+ )
77
+
78
+
79
+ def check_rate_limit(client_id: str) -> bool:
80
+ """Check if client is within rate limits.
81
+
82
+ Args:
83
+ client_id (str): The client id value.
84
+
85
+ Returns:
86
+ bool: True if check rate limit, otherwise False.
87
+ """
88
+ if _rate_limiter is None:
89
+ return True
90
+ return _rate_limiter.is_allowed(client_id)
@@ -0,0 +1,155 @@
1
+ """Runtime security enforcement helpers for tool invocations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import secrets
6
+ from dataclasses import dataclass
7
+ from typing import cast
8
+
9
+ from ..config import SecurityLevel, SolidWorksMCPConfig
10
+ from .auth import validate_api_key
11
+ from .rate_limiting import check_rate_limit
12
+
13
+
14
+ class SecurityError(RuntimeError):
15
+ """Raised when a request violates configured security policies."""
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class SecurityContext:
20
+ """Extracted invocation security context.
21
+
22
+ Attributes:
23
+ api_key (str | None): The api key value.
24
+ client_id (str): The client id value.
25
+ """
26
+
27
+ client_id: str
28
+ api_key: str | None
29
+
30
+
31
+ class SecurityEnforcer:
32
+ """Enforce authentication and rate-limit policies at runtime.
33
+
34
+ Args:
35
+ config (SolidWorksMCPConfig): Configuration values for the operation.
36
+
37
+ Attributes:
38
+ _config (Any): The config value.
39
+ """
40
+
41
+ def __init__(self, config: SolidWorksMCPConfig) -> None:
42
+ """Initialize the security enforcer.
43
+
44
+ Args:
45
+ config (SolidWorksMCPConfig): Configuration values for the operation.
46
+
47
+ Returns:
48
+ None: None.
49
+ """
50
+ self._config = config
51
+
52
+ def enforce(self, tool_name: str, payload: object) -> None:
53
+ """Validate invocation against runtime security policy.
54
+
55
+ Args:
56
+ tool_name (str): The tool name value.
57
+ payload (object): The payload value.
58
+
59
+ Returns:
60
+ None: None.
61
+
62
+ Raises:
63
+ SecurityError: Authentication failed: invalid api_key.
64
+ """
65
+ context = self._extract_context(payload)
66
+
67
+ if self._config.enable_rate_limiting and not check_rate_limit(
68
+ context.client_id
69
+ ):
70
+ raise SecurityError(
71
+ f"rate limit exceeded for client '{context.client_id}' while calling '{tool_name}'"
72
+ )
73
+
74
+ if not self._is_auth_required():
75
+ return
76
+
77
+ expected_key = self._expected_api_key()
78
+ if not expected_key:
79
+ raise SecurityError("authentication required but no API key configured")
80
+
81
+ provided_key = context.api_key
82
+ if provided_key is None:
83
+ raise SecurityError("authentication required but api_key was not provided")
84
+
85
+ if not validate_api_key(provided_key=provided_key, expected_key=expected_key):
86
+ raise SecurityError("authentication failed: invalid api_key")
87
+
88
+ def _extract_context(self, payload: object) -> SecurityContext:
89
+ """Extract client and auth information from payload object.
90
+
91
+ Args:
92
+ payload (object): The payload value.
93
+
94
+ Returns:
95
+ SecurityContext: The result produced by the operation.
96
+ """
97
+ if hasattr(payload, "model_dump"):
98
+ payload_dict = payload.model_dump()
99
+ elif isinstance(payload, dict):
100
+ payload_dict = payload
101
+ else:
102
+ payload_dict = {}
103
+
104
+ client_id_raw = payload_dict.get("client_id", "anonymous")
105
+ client_id = str(client_id_raw).strip() if client_id_raw else "anonymous"
106
+
107
+ api_key_raw = payload_dict.get("api_key")
108
+ api_key = str(api_key_raw) if api_key_raw is not None else None
109
+
110
+ return SecurityContext(client_id=client_id, api_key=api_key)
111
+
112
+ def _is_auth_required(self) -> bool:
113
+ """Return whether API key validation should be enforced.
114
+
115
+ Returns:
116
+ bool: True if auth required, otherwise False.
117
+ """
118
+ if bool(getattr(self._config, "api_key_required", False)):
119
+ return True
120
+ if getattr(self._config, "security_level", None) == SecurityLevel.STRICT:
121
+ return True
122
+ return getattr(self._config, "api_key", None) is not None or bool(
123
+ getattr(self._config, "api_keys", [])
124
+ )
125
+
126
+ def _expected_api_key(self) -> str | None:
127
+ """Return configured expected API key for validation.
128
+
129
+ Returns:
130
+ str | None: The result produced by the operation.
131
+ """
132
+ api_key = getattr(self._config, "api_key", None)
133
+ if api_key is not None:
134
+ get_secret_value = getattr(api_key, "get_secret_value", None)
135
+ if callable(get_secret_value):
136
+ return cast("str | None", get_secret_value())
137
+ return cast(str, api_key)
138
+
139
+ api_keys = getattr(self._config, "api_keys", [])
140
+ if api_keys:
141
+ return cast("str | None", api_keys[0])
142
+ return None
143
+
144
+
145
+ def constant_time_equals(left: str, right: str) -> bool:
146
+ """Compare two strings in constant time.
147
+
148
+ Args:
149
+ left (str): The left value.
150
+ right (str): The right value.
151
+
152
+ Returns:
153
+ bool: True if constant time equals, otherwise False.
154
+ """
155
+ return secrets.compare_digest(left, right)