embed-client-dev 4.0.78__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 (48) hide show
  1. embed_client/__init__.py +36 -0
  2. embed_client/__main__.py +15 -0
  3. embed_client/_version.py +3 -0
  4. embed_client/adapter_config_factory.py +213 -0
  5. embed_client/adapter_config_normalizer.py +135 -0
  6. embed_client/adapter_generator_bridge.py +166 -0
  7. embed_client/auth.py +189 -0
  8. embed_client/cli.py +483 -0
  9. embed_client/cli_core.py +427 -0
  10. embed_client/client_factory.py +515 -0
  11. embed_client/client_factory_examples.py +375 -0
  12. embed_client/client_factory_helpers.py +59 -0
  13. embed_client/config.py +408 -0
  14. embed_client/config_defaults.py +77 -0
  15. embed_client/config_examples.py +228 -0
  16. embed_client/config_generator.py +652 -0
  17. embed_client/config_generator_cli.py +151 -0
  18. embed_client/config_presets.py +245 -0
  19. embed_client/config_validator.py +246 -0
  20. embed_client/config_validator_cli.py +141 -0
  21. embed_client/constants.py +52 -0
  22. embed_client/demo_cli.py +78 -0
  23. embed_client/demo_security_cli.py +206 -0
  24. embed_client/embed_file_output_validate.py +348 -0
  25. embed_client/embed_file_status_store.py +80 -0
  26. embed_client/embedding_client.py +1929 -0
  27. embed_client/embedding_client_helpers.py +105 -0
  28. embed_client/example_async_usage.py +357 -0
  29. embed_client/example_async_usage_demo.py +282 -0
  30. embed_client/example_async_usage_ru.py +340 -0
  31. embed_client/example_async_usage_ru_demo.py +313 -0
  32. embed_client/exceptions.py +122 -0
  33. embed_client/identifier_types.py +56 -0
  34. embed_client/json_embed_file_preflight.py +128 -0
  35. embed_client/local_json_embed_file.py +73 -0
  36. embed_client/md_embed_file_prep.py +197 -0
  37. embed_client/response_normalizer.py +160 -0
  38. embed_client/response_parsers.py +219 -0
  39. embed_client/security_cli.py +200 -0
  40. embed_client/security_cli_core.py +227 -0
  41. embed_client/ssl_examples.py +278 -0
  42. embed_client/ssl_manager.py +141 -0
  43. embed_client_dev-4.0.78.dist-info/METADATA +155 -0
  44. embed_client_dev-4.0.78.dist-info/RECORD +48 -0
  45. embed_client_dev-4.0.78.dist-info/WHEEL +5 -0
  46. embed_client_dev-4.0.78.dist-info/entry_points.txt +4 -0
  47. embed_client_dev-4.0.78.dist-info/licenses/LICENSE +21 -0
  48. embed_client_dev-4.0.78.dist-info/top_level.txt +1 -0
@@ -0,0 +1,151 @@
1
+ """
2
+ CLI entry point for the embed-client configuration generator.
3
+
4
+ This module provides the command-line interface for generating configuration
5
+ files for all supported security modes using `ClientConfigGenerator`.
6
+
7
+ Author: Vasiliy Zdanovskiy
8
+ email: vasilyvz@gmail.com
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ from pathlib import Path
15
+
16
+ from embed_client.config_generator import ClientConfigGenerator
17
+
18
+
19
+ def main() -> None:
20
+ """CLI entry point for the configuration generator."""
21
+ parser = argparse.ArgumentParser(description="Generate embed-client configurations")
22
+ parser.add_argument(
23
+ "--mode",
24
+ choices=[
25
+ "http",
26
+ "http_token",
27
+ "http_token_roles",
28
+ "https",
29
+ "https_token",
30
+ "https_token_roles",
31
+ "mtls",
32
+ "mtls_roles",
33
+ "all",
34
+ ],
35
+ default="all",
36
+ help="Security mode to generate",
37
+ )
38
+ parser.add_argument("--host", default="localhost", help="Server host")
39
+ parser.add_argument("--port", type=int, default=8001, help="Server port")
40
+ parser.add_argument("--output", help="Output file path (for single mode)")
41
+ parser.add_argument(
42
+ "--output-dir", default="./configs", help="Output directory (for all modes)"
43
+ )
44
+ parser.add_argument("--cert-file", help="Client certificate file (for HTTPS/mTLS)")
45
+ parser.add_argument("--key-file", help="Client key file (for HTTPS/mTLS)")
46
+ parser.add_argument("--ca-cert-file", help="CA certificate file (for HTTPS/mTLS)")
47
+ parser.add_argument(
48
+ "--crl-file", help="CRL (Certificate Revocation List) file (optional)"
49
+ )
50
+ parser.add_argument("--api-key", help="API key for token modes")
51
+ parser.add_argument(
52
+ "--token", help="Authentication token (alternative to --api-key)"
53
+ )
54
+
55
+ args = parser.parse_args()
56
+
57
+ generator = ClientConfigGenerator()
58
+ output_dir = Path(args.output_dir) if args.output_dir else None
59
+
60
+ if args.mode == "all":
61
+ if output_dir:
62
+ output_dir.mkdir(parents=True, exist_ok=True)
63
+ configs = generator.generate_all_configs(
64
+ host=args.host,
65
+ port=args.port,
66
+ output_dir=output_dir,
67
+ cert_file=args.cert_file,
68
+ key_file=args.key_file,
69
+ ca_cert_file=args.ca_cert_file,
70
+ crl_file=getattr(args, "crl_file", None),
71
+ token=getattr(args, "token", None),
72
+ )
73
+ print(f"✅ Generated {len(configs)} configurations in {output_dir}")
74
+ else:
75
+ output_path = Path(args.output) if args.output else output_dir / f"{args.mode}.json" # type: ignore[operator]
76
+ if output_path:
77
+ output_path.parent.mkdir(parents=True, exist_ok=True)
78
+
79
+ # Common parameters for all modes
80
+ common_params = {
81
+ "host": args.host,
82
+ "port": args.port,
83
+ "token": getattr(args, "token", None),
84
+ "crl_file": getattr(args, "crl_file", None),
85
+ }
86
+
87
+ if args.mode == "http":
88
+ generator.generate_http_config(
89
+ output_path=output_path,
90
+ **common_params,
91
+ )
92
+ elif args.mode == "http_token":
93
+ generator.generate_http_token_config(
94
+ api_key=args.api_key,
95
+ output_path=output_path,
96
+ **common_params,
97
+ )
98
+ elif args.mode == "http_token_roles":
99
+ generator.generate_http_token_roles_config(
100
+ api_key=args.api_key,
101
+ output_path=output_path,
102
+ **common_params,
103
+ )
104
+ elif args.mode == "https":
105
+ generator.generate_https_config(
106
+ cert_file=args.cert_file,
107
+ key_file=args.key_file,
108
+ ca_cert_file=args.ca_cert_file,
109
+ output_path=output_path,
110
+ **common_params,
111
+ )
112
+ elif args.mode == "https_token":
113
+ generator.generate_https_token_config(
114
+ api_key=args.api_key,
115
+ cert_file=args.cert_file,
116
+ key_file=args.key_file,
117
+ ca_cert_file=args.ca_cert_file,
118
+ output_path=output_path,
119
+ **common_params,
120
+ )
121
+ elif args.mode == "https_token_roles":
122
+ generator.generate_https_token_roles_config(
123
+ api_key=args.api_key,
124
+ cert_file=args.cert_file,
125
+ key_file=args.key_file,
126
+ ca_cert_file=args.ca_cert_file,
127
+ output_path=output_path,
128
+ **common_params,
129
+ )
130
+ elif args.mode == "mtls":
131
+ generator.generate_mtls_config(
132
+ cert_file=args.cert_file,
133
+ key_file=args.key_file,
134
+ ca_cert_file=args.ca_cert_file,
135
+ output_path=output_path,
136
+ **common_params,
137
+ )
138
+ elif args.mode == "mtls_roles":
139
+ generator.generate_mtls_roles_config(
140
+ cert_file=args.cert_file,
141
+ key_file=args.key_file,
142
+ ca_cert_file=args.ca_cert_file,
143
+ output_path=output_path,
144
+ **common_params,
145
+ )
146
+
147
+ print(f"✅ Generated {args.mode} configuration: {output_path}")
148
+
149
+
150
+ if __name__ == "__main__":
151
+ main()
@@ -0,0 +1,245 @@
1
+ """
2
+ Helper functions and presets for ClientConfig.
3
+
4
+ Author: Vasiliy Zdanovskiy
5
+ email: vasilyvz@gmail.com
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type
12
+
13
+ if TYPE_CHECKING:
14
+ from embed_client.config import ClientConfig
15
+
16
+
17
+ def convert_env_value(value: str) -> Any:
18
+ """Convert environment variable value to appropriate Python type."""
19
+ lowered = value.lower()
20
+ if lowered == "true":
21
+ return True
22
+ if lowered == "false":
23
+ return False
24
+ if value.isdigit():
25
+ return int(value)
26
+ try:
27
+ return float(value)
28
+ except ValueError:
29
+ return value
30
+
31
+
32
+ def load_env_variables(
33
+ config_data: Dict[str, Any],
34
+ prefix: str = "EMBED_CLIENT_",
35
+ ) -> None:
36
+ """Load configuration overrides from environment variables into config_data."""
37
+ for key, value in os.environ.items():
38
+ if not key.startswith(prefix):
39
+ continue
40
+ parts = key[len(prefix) :].lower().split("_", 1)
41
+ if len(parts) != 2:
42
+ continue
43
+ section, param = parts
44
+ if section not in config_data or not isinstance(config_data[section], dict):
45
+ config_data[section] = {}
46
+ section_dict = config_data[section]
47
+ section_dict[param] = convert_env_value(value)
48
+
49
+
50
+ def validate_client_config(config: "ClientConfig") -> List[str]:
51
+ """Validate configuration and return list of human-readable error messages."""
52
+ errors: List[str] = []
53
+
54
+ # Validate server configuration
55
+ host = config.get("server.host")
56
+ port = config.get("server.port")
57
+ if not host:
58
+ errors.append("Server host is required")
59
+ if not port or not isinstance(port, int) or port <= 0:
60
+ errors.append("Server port must be a positive integer")
61
+
62
+ # Validate authentication configuration
63
+ auth_method = config.get("auth.method", "none")
64
+ if auth_method == "api_key":
65
+ if not config.get("auth.api_key.key"):
66
+ errors.append("API key is required for api_key authentication")
67
+ elif auth_method == "jwt":
68
+ if not all(
69
+ [
70
+ config.get("auth.jwt.username"),
71
+ config.get("auth.jwt.password"),
72
+ config.get("auth.jwt.secret"),
73
+ ]
74
+ ):
75
+ errors.append(
76
+ "Username, password, and secret are required for JWT authentication"
77
+ )
78
+ elif auth_method == "certificate":
79
+ if not all(
80
+ [
81
+ config.get("auth.certificate.cert_file"),
82
+ config.get("auth.certificate.key_file"),
83
+ ]
84
+ ):
85
+ errors.append(
86
+ "Certificate and key files are required for certificate authentication"
87
+ )
88
+ elif auth_method == "basic":
89
+ if not all(
90
+ [config.get("auth.basic.username"), config.get("auth.basic.password")]
91
+ ):
92
+ errors.append("Username and password are required for basic authentication")
93
+
94
+ # Validate SSL configuration
95
+ if config.get("ssl.enabled", False):
96
+ cert_file = config.get("ssl.cert_file")
97
+ key_file = config.get("ssl.key_file")
98
+ ca_cert_file = config.get("ssl.ca_cert_file")
99
+
100
+ if cert_file and not os.path.exists(cert_file):
101
+ errors.append(f"SSL certificate file not found: {cert_file}")
102
+ if key_file and not os.path.exists(key_file):
103
+ errors.append(f"SSL key file not found: {key_file}")
104
+ if ca_cert_file and not os.path.exists(ca_cert_file):
105
+ errors.append(f"SSL CA certificate file not found: {ca_cert_file}")
106
+
107
+ return errors
108
+
109
+
110
+ def create_minimal_config_dict(config: "ClientConfig") -> Dict[str, Any]:
111
+ """Create minimal configuration dictionary with only essential features."""
112
+ minimal_config = config.get_all()
113
+
114
+ # Disable all optional features
115
+ minimal_config["ssl"]["enabled"] = False
116
+ minimal_config["security"]["enabled"] = False
117
+ minimal_config["auth"]["method"] = "none"
118
+ minimal_config["logging"]["enabled"] = False
119
+
120
+ return minimal_config
121
+
122
+
123
+ def create_secure_config_dict(config: "ClientConfig") -> Dict[str, Any]:
124
+ """Create secure configuration dictionary with all security features enabled."""
125
+ secure_config = config.get_all()
126
+
127
+ # Enable all security features
128
+ secure_config["ssl"]["enabled"] = True
129
+ secure_config["security"]["enabled"] = True
130
+ secure_config["auth"]["method"] = "certificate"
131
+ secure_config["ssl"]["verify"] = True
132
+ secure_config["ssl"]["check_hostname"] = True
133
+ secure_config["ssl"]["client_cert_required"] = True
134
+
135
+ return secure_config
136
+
137
+
138
+ def create_http_config_for_class(
139
+ config_cls: Type["ClientConfig"],
140
+ host: str = "localhost",
141
+ port: int = 8001,
142
+ ) -> "ClientConfig":
143
+ """Factory helper for HTTP config without authentication."""
144
+ config = config_cls()
145
+ config.configure_server(host, port)
146
+ config.configure_auth_mode("none")
147
+ config.configure_ssl(False)
148
+ return config
149
+
150
+
151
+ def create_http_token_config_for_class(
152
+ config_cls: Type["ClientConfig"],
153
+ host: str = "localhost",
154
+ port: int = 8001,
155
+ api_key: Optional[str] = None,
156
+ ) -> "ClientConfig":
157
+ """Factory helper for HTTP config with API key authentication."""
158
+ config = config_cls()
159
+ config.configure_server(host, port)
160
+ config.configure_auth_mode("api_key", key=api_key)
161
+ config.configure_ssl(False)
162
+ return config
163
+
164
+
165
+ def create_https_config_for_class(
166
+ config_cls: Type["ClientConfig"],
167
+ host: str = "localhost",
168
+ port: int = 8443,
169
+ cert_file: Optional[str] = None,
170
+ key_file: Optional[str] = None,
171
+ ca_cert_file: Optional[str] = None,
172
+ ) -> "ClientConfig":
173
+ """Factory helper for HTTPS config without authentication."""
174
+ config = config_cls()
175
+ config.configure_ssl(
176
+ True,
177
+ cert_file=cert_file,
178
+ key_file=key_file,
179
+ ca_cert_file=ca_cert_file,
180
+ )
181
+ config.configure_server(host, port)
182
+ config.configure_auth_mode("none")
183
+ return config
184
+
185
+
186
+ def create_https_token_config_for_class(
187
+ config_cls: Type["ClientConfig"],
188
+ host: str = "localhost",
189
+ port: int = 8443,
190
+ api_key: Optional[str] = None,
191
+ cert_file: Optional[str] = None,
192
+ key_file: Optional[str] = None,
193
+ ca_cert_file: Optional[str] = None,
194
+ ) -> "ClientConfig":
195
+ """Factory helper for HTTPS config with API key authentication."""
196
+ config = config_cls()
197
+ config.configure_ssl(
198
+ True,
199
+ cert_file=cert_file,
200
+ key_file=key_file,
201
+ ca_cert_file=ca_cert_file,
202
+ )
203
+ config.configure_server(host, port)
204
+ config.configure_auth_mode("api_key", key=api_key)
205
+ return config
206
+
207
+
208
+ def create_mtls_config_for_class(
209
+ config_cls: Type["ClientConfig"],
210
+ host: str = "localhost",
211
+ port: int = 8443,
212
+ cert_file: Optional[str] = None,
213
+ key_file: Optional[str] = None,
214
+ ca_cert_file: Optional[str] = None,
215
+ ) -> "ClientConfig":
216
+ """Factory helper for mTLS config with client certificates."""
217
+ config = config_cls()
218
+ config.configure_ssl(
219
+ True,
220
+ cert_file=cert_file,
221
+ key_file=key_file,
222
+ ca_cert_file=ca_cert_file,
223
+ client_cert_required=True,
224
+ )
225
+ config.configure_server(host, port)
226
+ config.configure_auth_mode(
227
+ "certificate",
228
+ cert_file=cert_file,
229
+ key_file=key_file,
230
+ ca_cert_file=ca_cert_file,
231
+ )
232
+ return config
233
+
234
+
235
+ __all__ = [
236
+ "load_env_variables",
237
+ "validate_client_config",
238
+ "create_minimal_config_dict",
239
+ "create_secure_config_dict",
240
+ "create_http_config_for_class",
241
+ "create_http_token_config_for_class",
242
+ "create_https_config_for_class",
243
+ "create_https_token_config_for_class",
244
+ "create_mtls_config_for_class",
245
+ ]
@@ -0,0 +1,246 @@
1
+ """
2
+ Configuration Validator for embed-client.
3
+
4
+ Uses mcp_proxy_adapter.core.validation.config_validator.ConfigValidator as base.
5
+ Normalizes client config to adapter format, runs adapter validation, maps errors
6
+ back to embed-client section/key names. Keeps legacy ClientConfig checks for compatibility.
7
+
8
+ Author: Vasiliy Zdanovskiy
9
+ email: vasilyvz@gmail.com
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ from pathlib import Path
17
+ from typing import Any, Dict, List, Optional, Tuple
18
+
19
+ from embed_client.config import ClientConfig
20
+ from embed_client.config_presets import validate_client_config
21
+ from embed_client.adapter_config_normalizer import (
22
+ client_to_adapter_format,
23
+ map_adapter_errors_to_client,
24
+ )
25
+
26
+ try:
27
+ from mcp_proxy_adapter.core.validation.config_validator import ( # type: ignore[import-untyped]
28
+ ConfigValidator as AdapterConfigValidator,
29
+ )
30
+
31
+ _ADAPTER_AVAILABLE = True
32
+ except ImportError:
33
+ _ADAPTER_AVAILABLE = False
34
+ AdapterConfigValidator = None # type: ignore[misc, assignment]
35
+
36
+
37
+ class ConfigValidator:
38
+ """
39
+ Validator for embed-client configuration files.
40
+
41
+ Uses adapter ConfigValidator as base: normalizes client config to adapter
42
+ format, runs adapter validation, maps errors to embed-client names.
43
+ """
44
+
45
+ def __init__(self) -> None:
46
+ """Initialize configuration validator."""
47
+ pass
48
+
49
+ def _validate_via_adapter(
50
+ self, config_data: Dict[str, Any], config_path: Optional[Path] = None
51
+ ) -> List[str]:
52
+ """Run adapter ConfigValidator on normalized config; return list of error strings."""
53
+ if not _ADAPTER_AVAILABLE or AdapterConfigValidator is None:
54
+ return []
55
+ normalized = client_to_adapter_format(
56
+ config_data, config_path=str(config_path) if config_path else None
57
+ )
58
+ validator = AdapterConfigValidator(
59
+ config_path=str(config_path) if config_path else None
60
+ )
61
+ results = validator.validate_config(normalized)
62
+ errors = map_adapter_errors_to_client(
63
+ [r for r in results if getattr(r, "level", "") == "error"]
64
+ )
65
+ return errors
66
+
67
+ def validate_json_structure(
68
+ self,
69
+ config_data: Dict[str, Any],
70
+ config_path: Optional[Path] = None,
71
+ ) -> Tuple[bool, List[str]]:
72
+ """
73
+ Validate JSON structure and required fields.
74
+
75
+ Uses adapter ConfigValidator as base (normalize -> validate -> map errors).
76
+ Then runs legacy ClientConfig validation for embed-client specific checks.
77
+ config_path: optional path to config file (for resolving relative cert paths).
78
+ """
79
+ errors: List[str] = []
80
+
81
+ # Base validation via adapter
82
+ adapter_errors = self._validate_via_adapter(config_data, config_path)
83
+ errors.extend(adapter_errors)
84
+
85
+ # Legacy checks for sections adapter may not require
86
+ if "server" not in config_data:
87
+ errors.append("Missing required section: server")
88
+ else:
89
+ server = config_data["server"]
90
+ if "host" not in server:
91
+ errors.append("server.host is required")
92
+ if "port" not in server:
93
+ errors.append("server.port is required")
94
+ elif not isinstance(server["port"], int) or server["port"] <= 0:
95
+ errors.append("server.port must be a positive integer")
96
+
97
+ protocol = config_data.get(
98
+ "protocol", config_data.get("server", {}).get("protocol", "http")
99
+ )
100
+ if protocol not in ("http", "https", "mtls"):
101
+ errors.append(
102
+ f"Invalid protocol: {protocol}. Must be 'http', 'https', or 'mtls'"
103
+ )
104
+
105
+ if "auth" in config_data:
106
+ auth = config_data["auth"]
107
+ auth_method = auth.get("method", "none")
108
+ if auth_method == "api_key":
109
+ if "api_key" not in auth or "key" not in auth["api_key"]:
110
+ errors.append(
111
+ "auth.api_key.key is required for api_key authentication"
112
+ )
113
+ elif auth_method == "jwt":
114
+ jwt = auth.get("jwt", {})
115
+ if not all(key in jwt for key in ["username", "password", "secret"]):
116
+ errors.append(
117
+ "auth.jwt.username, auth.jwt.password, and auth.jwt.secret "
118
+ "are required for JWT authentication"
119
+ )
120
+ elif auth_method == "certificate":
121
+ cert = auth.get("certificate", {})
122
+ if not all(key in cert for key in ["cert_file", "key_file"]):
123
+ errors.append(
124
+ "auth.certificate.cert_file and auth.certificate.key_file "
125
+ "are required for certificate authentication"
126
+ )
127
+ elif auth_method == "basic":
128
+ basic = auth.get("basic", {})
129
+ if not all(key in basic for key in ["username", "password"]):
130
+ errors.append(
131
+ "auth.basic.username and auth.basic.password "
132
+ "are required for basic authentication"
133
+ )
134
+
135
+ return len(errors) == 0, errors
136
+
137
+ def validate_config_file(self, config_path: Path) -> Tuple[bool, str, List[str]]:
138
+ """
139
+ Validate a single configuration file.
140
+
141
+ Args:
142
+ config_path: Path to configuration file
143
+
144
+ Returns:
145
+ Tuple of (is_valid, status_message, list_of_errors)
146
+ """
147
+ try:
148
+ if not config_path.exists():
149
+ return False, "File not found", []
150
+
151
+ with open(config_path, "r", encoding="utf-8") as f:
152
+ config_data = json.load(f)
153
+
154
+ # Validate JSON structure (pass path for relative cert resolution)
155
+ is_valid, errors = self.validate_json_structure(config_data, config_path)
156
+
157
+ if not is_valid:
158
+ return False, f"{len(errors)} error(s) in structure", errors
159
+
160
+ # Validate using ClientConfig validator
161
+ try:
162
+ client_config = ClientConfig(str(config_path))
163
+ config_errors = validate_client_config(client_config)
164
+
165
+ if config_errors:
166
+ errors.extend(config_errors)
167
+ return False, f"{len(errors)} error(s) found", errors
168
+
169
+ return True, "OK", []
170
+ except Exception as e:
171
+ errors.append(f"Error loading config: {str(e)}")
172
+ return False, f"Config loading failed: {str(e)}", errors
173
+
174
+ except json.JSONDecodeError as e:
175
+ return False, f"JSON decode error: {str(e)}", []
176
+ except Exception as e:
177
+ return False, f"Unexpected error: {str(e)}", []
178
+
179
+ def validate_config_dict(
180
+ self, config_data: Dict[str, Any]
181
+ ) -> Tuple[bool, List[str]]:
182
+ """
183
+ Validate configuration dictionary.
184
+
185
+ Args:
186
+ config_data: Configuration dictionary
187
+
188
+ Returns:
189
+ Tuple of (is_valid, list_of_errors)
190
+ """
191
+ # Validate JSON structure (no path for dict)
192
+ is_valid, errors = self.validate_json_structure(config_data, None)
193
+
194
+ if not is_valid:
195
+ return False, errors
196
+
197
+ # Create temporary ClientConfig to validate
198
+ try:
199
+ # Create a temporary config file
200
+ import tempfile
201
+
202
+ with tempfile.NamedTemporaryFile(
203
+ mode="w", suffix=".json", delete=False
204
+ ) as f:
205
+ json.dump(config_data, f, indent=2)
206
+ temp_path = f.name
207
+
208
+ try:
209
+ client_config = ClientConfig(temp_path)
210
+ config_errors = validate_client_config(client_config)
211
+ if config_errors:
212
+ errors.extend(config_errors)
213
+ return False, errors
214
+ return True, []
215
+ finally:
216
+ os.unlink(temp_path)
217
+ except Exception as e:
218
+ errors.append(f"Error validating config: {str(e)}")
219
+ return False, errors
220
+
221
+ def validate_config_directory(
222
+ self, config_dir: Path
223
+ ) -> Dict[str, Tuple[bool, str, List[str]]]:
224
+ """
225
+ Validate all configuration files in a directory.
226
+
227
+ Args:
228
+ config_dir: Directory containing configuration files
229
+
230
+ Returns:
231
+ Dictionary mapping file names to (is_valid, status_message, list_of_errors)
232
+ """
233
+ results: Dict[str, Tuple[bool, str, List[str]]] = {}
234
+
235
+ if not config_dir.exists():
236
+ return results
237
+
238
+ config_files = list(config_dir.glob("*.json"))
239
+
240
+ for config_file in sorted(config_files):
241
+ results[config_file.name] = self.validate_config_file(config_file)
242
+
243
+ return results
244
+
245
+
246
+ __all__ = ["ConfigValidator"]