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.
- embed_client/__init__.py +36 -0
- embed_client/__main__.py +15 -0
- embed_client/_version.py +3 -0
- embed_client/adapter_config_factory.py +213 -0
- embed_client/adapter_config_normalizer.py +135 -0
- embed_client/adapter_generator_bridge.py +166 -0
- embed_client/auth.py +189 -0
- embed_client/cli.py +483 -0
- embed_client/cli_core.py +427 -0
- embed_client/client_factory.py +515 -0
- embed_client/client_factory_examples.py +375 -0
- embed_client/client_factory_helpers.py +59 -0
- embed_client/config.py +408 -0
- embed_client/config_defaults.py +77 -0
- embed_client/config_examples.py +228 -0
- embed_client/config_generator.py +652 -0
- embed_client/config_generator_cli.py +151 -0
- embed_client/config_presets.py +245 -0
- embed_client/config_validator.py +246 -0
- embed_client/config_validator_cli.py +141 -0
- embed_client/constants.py +52 -0
- embed_client/demo_cli.py +78 -0
- embed_client/demo_security_cli.py +206 -0
- embed_client/embed_file_output_validate.py +348 -0
- embed_client/embed_file_status_store.py +80 -0
- embed_client/embedding_client.py +1929 -0
- embed_client/embedding_client_helpers.py +105 -0
- embed_client/example_async_usage.py +357 -0
- embed_client/example_async_usage_demo.py +282 -0
- embed_client/example_async_usage_ru.py +340 -0
- embed_client/example_async_usage_ru_demo.py +313 -0
- embed_client/exceptions.py +122 -0
- embed_client/identifier_types.py +56 -0
- embed_client/json_embed_file_preflight.py +128 -0
- embed_client/local_json_embed_file.py +73 -0
- embed_client/md_embed_file_prep.py +197 -0
- embed_client/response_normalizer.py +160 -0
- embed_client/response_parsers.py +219 -0
- embed_client/security_cli.py +200 -0
- embed_client/security_cli_core.py +227 -0
- embed_client/ssl_examples.py +278 -0
- embed_client/ssl_manager.py +141 -0
- embed_client_dev-4.0.78.dist-info/METADATA +155 -0
- embed_client_dev-4.0.78.dist-info/RECORD +48 -0
- embed_client_dev-4.0.78.dist-info/WHEEL +5 -0
- embed_client_dev-4.0.78.dist-info/entry_points.txt +4 -0
- embed_client_dev-4.0.78.dist-info/licenses/LICENSE +21 -0
- 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"]
|