embed-client 2.0.0.0__py3-none-any.whl → 3.1.0.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.
embed_client/config.py ADDED
@@ -0,0 +1,592 @@
1
+ """
2
+ Client configuration management module.
3
+
4
+ This module provides configuration management for the embed-client,
5
+ supporting all security modes and authentication methods.
6
+
7
+ Author: Vasiliy Zdanovskiy
8
+ email: vasilyvz@gmail.com
9
+ """
10
+
11
+ import json
12
+ import os
13
+ from typing import Any, Dict, Optional, List, Union
14
+ from pathlib import Path
15
+
16
+
17
+ class ClientConfig:
18
+ """
19
+ Configuration management class for the embed-client.
20
+ Allows loading settings from configuration file and environment variables.
21
+ Supports all security modes and authentication methods.
22
+ """
23
+
24
+ def __init__(self, config_path: Optional[str] = None):
25
+ """
26
+ Initialize client configuration.
27
+
28
+ Args:
29
+ config_path: Path to configuration file. If not specified,
30
+ "./config.json" is used.
31
+ """
32
+ self.config_path = config_path or "./config.json"
33
+ self.config_data: Dict[str, Any] = {}
34
+ self.load_config()
35
+
36
+ def load_config(self) -> None:
37
+ """
38
+ Load configuration from file and environment variables.
39
+ """
40
+ # Set default config values
41
+ self.config_data = {
42
+ "server": {
43
+ "host": "localhost",
44
+ "port": 8001,
45
+ "base_url": "http://localhost:8001"
46
+ },
47
+ "timeout": 30,
48
+ "retry_attempts": 3,
49
+ "retry_delay": 1,
50
+ "auth": {
51
+ "method": "none", # none, api_key, jwt, certificate, basic
52
+ "api_key": {
53
+ "key": None,
54
+ "header": "X-API-Key"
55
+ },
56
+ "jwt": {
57
+ "username": None,
58
+ "password": None,
59
+ "secret": None,
60
+ "expiry_hours": 24
61
+ },
62
+ "certificate": {
63
+ "enabled": False,
64
+ "cert_file": None,
65
+ "key_file": None,
66
+ "ca_cert_file": None
67
+ },
68
+ "basic": {
69
+ "username": None,
70
+ "password": None
71
+ }
72
+ },
73
+ "ssl": {
74
+ "enabled": False,
75
+ "verify": True,
76
+ "check_hostname": True,
77
+ "cert_file": None,
78
+ "key_file": None,
79
+ "ca_cert_file": None,
80
+ "client_cert_required": False
81
+ },
82
+ "security": {
83
+ "enabled": False,
84
+ "roles_enabled": False,
85
+ "roles_file": None
86
+ },
87
+ "logging": {
88
+ "enabled": False,
89
+ "level": "INFO",
90
+ "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
91
+ }
92
+ }
93
+
94
+ # Try to load configuration from file
95
+ if os.path.exists(self.config_path):
96
+ try:
97
+ with open(self.config_path, 'r', encoding='utf-8') as f:
98
+ file_config = json.load(f)
99
+ self._update_nested_dict(self.config_data, file_config)
100
+ except Exception as e:
101
+ print(f"Error loading config from {self.config_path}: {e}")
102
+
103
+ # Load configuration from environment variables
104
+ self._load_env_variables()
105
+
106
+ def load_from_file(self, config_path: str) -> None:
107
+ """
108
+ Load configuration from the specified file.
109
+
110
+ Args:
111
+ config_path: Path to configuration file.
112
+ """
113
+ self.config_path = config_path
114
+ self.load_config()
115
+
116
+ def _load_env_variables(self) -> None:
117
+ """
118
+ Load configuration from environment variables.
119
+ Environment variables should be in format EMBED_CLIENT_SECTION_KEY=value.
120
+ For example, EMBED_CLIENT_SERVER_PORT=8080.
121
+ """
122
+ prefix = "EMBED_CLIENT_"
123
+ for key, value in os.environ.items():
124
+ if key.startswith(prefix):
125
+ parts = key[len(prefix):].lower().split("_", 1)
126
+ if len(parts) == 2:
127
+ section, param = parts
128
+ if section not in self.config_data:
129
+ self.config_data[section] = {}
130
+ self.config_data[section][param] = self._convert_env_value(value)
131
+
132
+ def _convert_env_value(self, value: str) -> Any:
133
+ """
134
+ Convert environment variable value to appropriate type.
135
+
136
+ Args:
137
+ value: Value as string
138
+
139
+ Returns:
140
+ Converted value
141
+ """
142
+ # Try to convert to appropriate type
143
+ if value.lower() == "true":
144
+ return True
145
+ elif value.lower() == "false":
146
+ return False
147
+ elif value.isdigit():
148
+ return int(value)
149
+ else:
150
+ try:
151
+ return float(value)
152
+ except ValueError:
153
+ return value
154
+
155
+ def get(self, key: str, default: Any = None) -> Any:
156
+ """
157
+ Get configuration value for key.
158
+
159
+ Args:
160
+ key: Configuration key in format "section.param"
161
+ default: Default value if key not found
162
+
163
+ Returns:
164
+ Configuration value
165
+ """
166
+ parts = key.split(".")
167
+
168
+ # Get value from config
169
+ value = self.config_data
170
+ for part in parts:
171
+ if not isinstance(value, dict) or part not in value:
172
+ return default
173
+ value = value[part]
174
+
175
+ return value
176
+
177
+ def get_all(self) -> Dict[str, Any]:
178
+ """
179
+ Get all configuration values.
180
+
181
+ Returns:
182
+ Dictionary with all configuration values
183
+ """
184
+ return self.config_data.copy()
185
+
186
+ def set(self, key: str, value: Any) -> None:
187
+ """
188
+ Set configuration value for key.
189
+
190
+ Args:
191
+ key: Configuration key in format "section.param"
192
+ value: Configuration value
193
+ """
194
+ parts = key.split(".")
195
+ if len(parts) == 1:
196
+ self.config_data[key] = value
197
+ else:
198
+ section = parts[0]
199
+ param_key = ".".join(parts[1:])
200
+
201
+ if section not in self.config_data:
202
+ self.config_data[section] = {}
203
+
204
+ current = self.config_data[section]
205
+ for part in parts[1:-1]:
206
+ if part not in current:
207
+ current[part] = {}
208
+ current = current[part]
209
+
210
+ current[parts[-1]] = value
211
+
212
+ def save(self, path: Optional[str] = None) -> None:
213
+ """
214
+ Save configuration to file.
215
+
216
+ Args:
217
+ path: Path to configuration file. If not specified,
218
+ self.config_path is used.
219
+ """
220
+ save_path = path or self.config_path
221
+ with open(save_path, 'w', encoding='utf-8') as f:
222
+ json.dump(self.config_data, f, indent=2)
223
+
224
+ def _update_nested_dict(self, d: Dict, u: Dict) -> Dict:
225
+ """
226
+ Update nested dictionary recursively.
227
+
228
+ Args:
229
+ d: Dictionary to update
230
+ u: Dictionary with new values
231
+
232
+ Returns:
233
+ Updated dictionary
234
+ """
235
+ for k, v in u.items():
236
+ if isinstance(v, dict) and k in d and isinstance(d[k], dict):
237
+ self._update_nested_dict(d[k], v)
238
+ else:
239
+ d[k] = v
240
+ return d
241
+
242
+ def configure_auth_mode(self, mode: str, **kwargs) -> None:
243
+ """
244
+ Configure authentication mode.
245
+
246
+ Args:
247
+ mode: Authentication mode (none, api_key, jwt, certificate, basic)
248
+ **kwargs: Additional configuration parameters
249
+ """
250
+ self.set("auth.method", mode)
251
+
252
+ if mode == "api_key":
253
+ if "key" in kwargs:
254
+ self.set("auth.api_key.key", kwargs["key"])
255
+ if "header" in kwargs:
256
+ self.set("auth.api_key.header", kwargs["header"])
257
+ elif mode == "jwt":
258
+ if "username" in kwargs:
259
+ self.set("auth.jwt.username", kwargs["username"])
260
+ if "password" in kwargs:
261
+ self.set("auth.jwt.password", kwargs["password"])
262
+ if "secret" in kwargs:
263
+ self.set("auth.jwt.secret", kwargs["secret"])
264
+ if "expiry_hours" in kwargs:
265
+ self.set("auth.jwt.expiry_hours", kwargs["expiry_hours"])
266
+ elif mode == "certificate":
267
+ self.set("auth.certificate.enabled", True)
268
+ if "cert_file" in kwargs:
269
+ self.set("auth.certificate.cert_file", kwargs["cert_file"])
270
+ if "key_file" in kwargs:
271
+ self.set("auth.certificate.key_file", kwargs["key_file"])
272
+ if "ca_cert_file" in kwargs:
273
+ self.set("auth.certificate.ca_cert_file", kwargs["ca_cert_file"])
274
+ elif mode == "basic":
275
+ if "username" in kwargs:
276
+ self.set("auth.basic.username", kwargs["username"])
277
+ if "password" in kwargs:
278
+ self.set("auth.basic.password", kwargs["password"])
279
+
280
+ def configure_ssl(self, enabled: bool = True, **kwargs) -> None:
281
+ """
282
+ Configure SSL/TLS settings.
283
+
284
+ Args:
285
+ enabled: Enable SSL/TLS
286
+ **kwargs: Additional SSL configuration parameters
287
+ """
288
+ self.set("ssl.enabled", enabled)
289
+
290
+ if "verify" in kwargs:
291
+ self.set("ssl.verify", kwargs["verify"])
292
+ if "check_hostname" in kwargs:
293
+ self.set("ssl.check_hostname", kwargs["check_hostname"])
294
+ if "cert_file" in kwargs:
295
+ self.set("ssl.cert_file", kwargs["cert_file"])
296
+ if "key_file" in kwargs:
297
+ self.set("ssl.key_file", kwargs["key_file"])
298
+ if "ca_cert_file" in kwargs:
299
+ self.set("ssl.ca_cert_file", kwargs["ca_cert_file"])
300
+ if "client_cert_required" in kwargs:
301
+ self.set("ssl.client_cert_required", kwargs["client_cert_required"])
302
+
303
+ def configure_server(self, host: str, port: int, base_url: Optional[str] = None) -> None:
304
+ """
305
+ Configure server connection settings.
306
+
307
+ Args:
308
+ host: Server host
309
+ port: Server port
310
+ base_url: Full server URL (optional, will be constructed if not provided)
311
+ """
312
+ self.set("server.host", host)
313
+ self.set("server.port", port)
314
+
315
+ if base_url:
316
+ self.set("server.base_url", base_url)
317
+ else:
318
+ protocol = "https" if self.get("ssl.enabled", False) else "http"
319
+ self.set("server.base_url", f"{protocol}://{host}:{port}")
320
+
321
+ def get_server_url(self) -> str:
322
+ """
323
+ Get the complete server URL.
324
+
325
+ Returns:
326
+ Server URL string
327
+ """
328
+ return self.get("server.base_url", "http://localhost:8001")
329
+
330
+ def get_auth_method(self) -> str:
331
+ """
332
+ Get the authentication method.
333
+
334
+ Returns:
335
+ Authentication method string
336
+ """
337
+ return self.get("auth.method", "none")
338
+
339
+ def is_ssl_enabled(self) -> bool:
340
+ """
341
+ Check if SSL/TLS is enabled.
342
+
343
+ Returns:
344
+ True if SSL is enabled, False otherwise
345
+ """
346
+ return self.get("ssl.enabled", False)
347
+
348
+ def is_auth_enabled(self) -> bool:
349
+ """
350
+ Check if authentication is enabled.
351
+
352
+ Returns:
353
+ True if authentication is enabled, False otherwise
354
+ """
355
+ return self.get("auth.method", "none") != "none"
356
+
357
+ def is_security_enabled(self) -> bool:
358
+ """
359
+ Check if security features are enabled.
360
+
361
+ Returns:
362
+ True if security is enabled, False otherwise
363
+ """
364
+ return self.get("security.enabled", False)
365
+
366
+ def validate_config(self) -> List[str]:
367
+ """
368
+ Validate configuration and return list of errors.
369
+
370
+ Returns:
371
+ List of validation error messages
372
+ """
373
+ errors = []
374
+
375
+ # Validate server configuration
376
+ host = self.get("server.host")
377
+ port = self.get("server.port")
378
+ if not host:
379
+ errors.append("Server host is required")
380
+ if not port or not isinstance(port, int) or port <= 0:
381
+ errors.append("Server port must be a positive integer")
382
+
383
+ # Validate authentication configuration
384
+ auth_method = self.get("auth.method", "none")
385
+ if auth_method == "api_key":
386
+ if not self.get("auth.api_key.key"):
387
+ errors.append("API key is required for api_key authentication")
388
+ elif auth_method == "jwt":
389
+ if not all([self.get("auth.jwt.username"),
390
+ self.get("auth.jwt.password"),
391
+ self.get("auth.jwt.secret")]):
392
+ errors.append("Username, password, and secret are required for JWT authentication")
393
+ elif auth_method == "certificate":
394
+ if not all([self.get("auth.certificate.cert_file"),
395
+ self.get("auth.certificate.key_file")]):
396
+ errors.append("Certificate and key files are required for certificate authentication")
397
+ elif auth_method == "basic":
398
+ if not all([self.get("auth.basic.username"),
399
+ self.get("auth.basic.password")]):
400
+ errors.append("Username and password are required for basic authentication")
401
+
402
+ # Validate SSL configuration
403
+ if self.get("ssl.enabled", False):
404
+ if self.get("ssl.cert_file") and not os.path.exists(self.get("ssl.cert_file")):
405
+ errors.append(f"SSL certificate file not found: {self.get('ssl.cert_file')}")
406
+ if self.get("ssl.key_file") and not os.path.exists(self.get("ssl.key_file")):
407
+ errors.append(f"SSL key file not found: {self.get('ssl.key_file')}")
408
+ if self.get("ssl.ca_cert_file") and not os.path.exists(self.get("ssl.ca_cert_file")):
409
+ errors.append(f"SSL CA certificate file not found: {self.get('ssl.ca_cert_file')}")
410
+
411
+ return errors
412
+
413
+ def create_minimal_config(self) -> Dict[str, Any]:
414
+ """
415
+ Create minimal configuration with only essential features.
416
+
417
+ Returns:
418
+ Minimal configuration dictionary
419
+ """
420
+ minimal_config = self.config_data.copy()
421
+
422
+ # Disable all optional features
423
+ minimal_config["ssl"]["enabled"] = False
424
+ minimal_config["security"]["enabled"] = False
425
+ minimal_config["auth"]["method"] = "none"
426
+ minimal_config["logging"]["enabled"] = False
427
+
428
+ return minimal_config
429
+
430
+ def create_secure_config(self) -> Dict[str, Any]:
431
+ """
432
+ Create secure configuration with all security features enabled.
433
+
434
+ Returns:
435
+ Secure configuration dictionary
436
+ """
437
+ secure_config = self.config_data.copy()
438
+
439
+ # Enable all security features
440
+ secure_config["ssl"]["enabled"] = True
441
+ secure_config["security"]["enabled"] = True
442
+ secure_config["auth"]["method"] = "certificate"
443
+ secure_config["ssl"]["verify"] = True
444
+ secure_config["ssl"]["check_hostname"] = True
445
+ secure_config["ssl"]["client_cert_required"] = True
446
+
447
+ return secure_config
448
+
449
+ @classmethod
450
+ def from_dict(cls, config_dict: Dict[str, Any]) -> 'ClientConfig':
451
+ """
452
+ Create ClientConfig instance from dictionary.
453
+
454
+ Args:
455
+ config_dict: Configuration dictionary
456
+
457
+ Returns:
458
+ ClientConfig instance
459
+ """
460
+ config = cls()
461
+ config.config_data = config._update_nested_dict(config.config_data, config_dict)
462
+ return config
463
+
464
+ @classmethod
465
+ def from_file(cls, config_path: str) -> 'ClientConfig':
466
+ """
467
+ Create ClientConfig instance from file.
468
+
469
+ Args:
470
+ config_path: Path to configuration file
471
+
472
+ Returns:
473
+ ClientConfig instance
474
+ """
475
+ config = cls(config_path)
476
+ return config
477
+
478
+ @classmethod
479
+ def create_http_config(cls, host: str = "localhost", port: int = 8001) -> 'ClientConfig':
480
+ """
481
+ Create configuration for HTTP connection without authentication.
482
+
483
+ Args:
484
+ host: Server host
485
+ port: Server port
486
+
487
+ Returns:
488
+ ClientConfig instance
489
+ """
490
+ config = cls()
491
+ config.configure_server(host, port)
492
+ config.configure_auth_mode("none")
493
+ config.configure_ssl(False)
494
+ return config
495
+
496
+ @classmethod
497
+ def create_http_token_config(cls, host: str = "localhost", port: int = 8001,
498
+ api_key: str = None) -> 'ClientConfig':
499
+ """
500
+ Create configuration for HTTP connection with API key authentication.
501
+
502
+ Args:
503
+ host: Server host
504
+ port: Server port
505
+ api_key: API key for authentication
506
+
507
+ Returns:
508
+ ClientConfig instance
509
+ """
510
+ config = cls()
511
+ config.configure_server(host, port)
512
+ config.configure_auth_mode("api_key", key=api_key)
513
+ config.configure_ssl(False)
514
+ return config
515
+
516
+ @classmethod
517
+ def create_https_config(cls, host: str = "localhost", port: int = 8443,
518
+ cert_file: str = None, key_file: str = None,
519
+ ca_cert_file: str = None) -> 'ClientConfig':
520
+ """
521
+ Create configuration for HTTPS connection without authentication.
522
+
523
+ Args:
524
+ host: Server host
525
+ port: Server port
526
+ cert_file: Client certificate file
527
+ key_file: Client key file
528
+ ca_cert_file: CA certificate file
529
+
530
+ Returns:
531
+ ClientConfig instance
532
+ """
533
+ config = cls()
534
+ config.configure_ssl(True, cert_file=cert_file, key_file=key_file,
535
+ ca_cert_file=ca_cert_file)
536
+ config.configure_server(host, port)
537
+ config.configure_auth_mode("none")
538
+ return config
539
+
540
+ @classmethod
541
+ def create_https_token_config(cls, host: str = "localhost", port: int = 8443,
542
+ api_key: str = None, cert_file: str = None,
543
+ key_file: str = None, ca_cert_file: str = None) -> 'ClientConfig':
544
+ """
545
+ Create configuration for HTTPS connection with API key authentication.
546
+
547
+ Args:
548
+ host: Server host
549
+ port: Server port
550
+ api_key: API key for authentication
551
+ cert_file: Client certificate file
552
+ key_file: Client key file
553
+ ca_cert_file: CA certificate file
554
+
555
+ Returns:
556
+ ClientConfig instance
557
+ """
558
+ config = cls()
559
+ config.configure_ssl(True, cert_file=cert_file, key_file=key_file,
560
+ ca_cert_file=ca_cert_file)
561
+ config.configure_server(host, port)
562
+ config.configure_auth_mode("api_key", key=api_key)
563
+ return config
564
+
565
+ @classmethod
566
+ def create_mtls_config(cls, host: str = "localhost", port: int = 8443,
567
+ cert_file: str = None, key_file: str = None,
568
+ ca_cert_file: str = None) -> 'ClientConfig':
569
+ """
570
+ Create configuration for mTLS connection with client certificates.
571
+
572
+ Args:
573
+ host: Server host
574
+ port: Server port
575
+ cert_file: Client certificate file
576
+ key_file: Client key file
577
+ ca_cert_file: CA certificate file
578
+
579
+ Returns:
580
+ ClientConfig instance
581
+ """
582
+ config = cls()
583
+ config.configure_ssl(True, cert_file=cert_file, key_file=key_file,
584
+ ca_cert_file=ca_cert_file, client_cert_required=True)
585
+ config.configure_server(host, port)
586
+ config.configure_auth_mode("certificate", cert_file=cert_file,
587
+ key_file=key_file, ca_cert_file=ca_cert_file)
588
+ return config
589
+
590
+
591
+ # Singleton instance
592
+ config = ClientConfig()