claude-mpm 4.3.22__py3-none-any.whl → 4.4.3__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/WORKFLOW.md +2 -14
- claude_mpm/cli/commands/configure.py +2 -29
- claude_mpm/cli/commands/doctor.py +2 -2
- claude_mpm/cli/commands/mpm_init.py +3 -3
- claude_mpm/cli/parsers/configure_parser.py +4 -15
- claude_mpm/core/framework/__init__.py +38 -0
- claude_mpm/core/framework/formatters/__init__.py +11 -0
- claude_mpm/core/framework/formatters/capability_generator.py +356 -0
- claude_mpm/core/framework/formatters/content_formatter.py +283 -0
- claude_mpm/core/framework/formatters/context_generator.py +180 -0
- claude_mpm/core/framework/loaders/__init__.py +13 -0
- claude_mpm/core/framework/loaders/agent_loader.py +202 -0
- claude_mpm/core/framework/loaders/file_loader.py +213 -0
- claude_mpm/core/framework/loaders/instruction_loader.py +151 -0
- claude_mpm/core/framework/loaders/packaged_loader.py +208 -0
- claude_mpm/core/framework/processors/__init__.py +11 -0
- claude_mpm/core/framework/processors/memory_processor.py +222 -0
- claude_mpm/core/framework/processors/metadata_processor.py +146 -0
- claude_mpm/core/framework/processors/template_processor.py +238 -0
- claude_mpm/core/framework_loader.py +277 -1798
- claude_mpm/hooks/__init__.py +9 -1
- claude_mpm/hooks/kuzu_memory_hook.py +352 -0
- claude_mpm/hooks/memory_integration_hook.py +1 -1
- claude_mpm/services/agents/memory/content_manager.py +5 -2
- claude_mpm/services/agents/memory/memory_file_service.py +1 -0
- claude_mpm/services/agents/memory/memory_limits_service.py +1 -0
- claude_mpm/services/core/path_resolver.py +1 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +1 -0
- claude_mpm/services/mcp_config_manager.py +67 -4
- claude_mpm/services/mcp_gateway/core/process_pool.py +281 -0
- claude_mpm/services/mcp_gateway/core/startup_verification.py +2 -2
- claude_mpm/services/mcp_gateway/main.py +3 -13
- claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -10
- claude_mpm/services/mcp_gateway/tools/__init__.py +13 -2
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +36 -6
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +542 -0
- claude_mpm/services/shared/__init__.py +2 -1
- claude_mpm/services/shared/service_factory.py +8 -5
- claude_mpm/services/unified/__init__.py +65 -0
- claude_mpm/services/unified/analyzer_strategies/__init__.py +44 -0
- claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +473 -0
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +643 -0
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +804 -0
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +661 -0
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +696 -0
- claude_mpm/services/unified/config_strategies/__init__.py +190 -0
- claude_mpm/services/unified/config_strategies/config_schema.py +689 -0
- claude_mpm/services/unified/config_strategies/context_strategy.py +748 -0
- claude_mpm/services/unified/config_strategies/error_handling_strategy.py +999 -0
- claude_mpm/services/unified/config_strategies/file_loader_strategy.py +871 -0
- claude_mpm/services/unified/config_strategies/unified_config_service.py +802 -0
- claude_mpm/services/unified/config_strategies/validation_strategy.py +1105 -0
- claude_mpm/services/unified/deployment_strategies/__init__.py +97 -0
- claude_mpm/services/unified/deployment_strategies/base.py +557 -0
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +486 -0
- claude_mpm/services/unified/deployment_strategies/local.py +594 -0
- claude_mpm/services/unified/deployment_strategies/utils.py +672 -0
- claude_mpm/services/unified/deployment_strategies/vercel.py +471 -0
- claude_mpm/services/unified/interfaces.py +499 -0
- claude_mpm/services/unified/migration.py +532 -0
- claude_mpm/services/unified/strategies.py +551 -0
- claude_mpm/services/unified/unified_analyzer.py +534 -0
- claude_mpm/services/unified/unified_config.py +688 -0
- claude_mpm/services/unified/unified_deployment.py +470 -0
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/METADATA +15 -15
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/RECORD +71 -32
- claude_mpm/cli/commands/configure_tui.py +0 -1927
- claude_mpm/services/mcp_gateway/tools/ticket_tools.py +0 -645
- claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +0 -602
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/WHEEL +0 -0
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,689 @@
|
|
1
|
+
"""
|
2
|
+
Configuration Schema - Declarative configuration with automatic validation
|
3
|
+
Part of Phase 3 Configuration Consolidation
|
4
|
+
"""
|
5
|
+
|
6
|
+
from typing import Any, Dict, List, Optional, Union, Type, Callable, Generic, TypeVar
|
7
|
+
from dataclasses import dataclass, field, asdict
|
8
|
+
from enum import Enum
|
9
|
+
from datetime import datetime
|
10
|
+
import json
|
11
|
+
from pathlib import Path
|
12
|
+
|
13
|
+
from claude_mpm.core.logging_utils import get_logger
|
14
|
+
|
15
|
+
T = TypeVar('T')
|
16
|
+
|
17
|
+
|
18
|
+
class SchemaType(Enum):
|
19
|
+
"""Supported schema types"""
|
20
|
+
STRING = "string"
|
21
|
+
INTEGER = "integer"
|
22
|
+
NUMBER = "number"
|
23
|
+
BOOLEAN = "boolean"
|
24
|
+
ARRAY = "array"
|
25
|
+
OBJECT = "object"
|
26
|
+
NULL = "null"
|
27
|
+
ANY = "any"
|
28
|
+
|
29
|
+
|
30
|
+
class SchemaFormat(Enum):
|
31
|
+
"""Supported format constraints"""
|
32
|
+
DATE = "date"
|
33
|
+
DATETIME = "datetime"
|
34
|
+
TIME = "time"
|
35
|
+
EMAIL = "email"
|
36
|
+
URI = "uri"
|
37
|
+
UUID = "uuid"
|
38
|
+
IPV4 = "ipv4"
|
39
|
+
IPV6 = "ipv6"
|
40
|
+
HOSTNAME = "hostname"
|
41
|
+
PATH = "path"
|
42
|
+
REGEX = "regex"
|
43
|
+
JSON = "json"
|
44
|
+
BASE64 = "base64"
|
45
|
+
SEMVER = "semver"
|
46
|
+
|
47
|
+
|
48
|
+
@dataclass
|
49
|
+
class SchemaProperty:
|
50
|
+
"""Schema property definition"""
|
51
|
+
type: Union[SchemaType, List[SchemaType]]
|
52
|
+
description: Optional[str] = None
|
53
|
+
default: Any = None
|
54
|
+
required: bool = False
|
55
|
+
nullable: bool = False
|
56
|
+
|
57
|
+
# Constraints
|
58
|
+
minimum: Optional[Union[int, float]] = None
|
59
|
+
maximum: Optional[Union[int, float]] = None
|
60
|
+
exclusive_minimum: Optional[Union[int, float]] = None
|
61
|
+
exclusive_maximum: Optional[Union[int, float]] = None
|
62
|
+
|
63
|
+
min_length: Optional[int] = None
|
64
|
+
max_length: Optional[int] = None
|
65
|
+
pattern: Optional[str] = None
|
66
|
+
format: Optional[SchemaFormat] = None
|
67
|
+
|
68
|
+
enum: Optional[List[Any]] = None
|
69
|
+
const: Optional[Any] = None
|
70
|
+
|
71
|
+
# Array constraints
|
72
|
+
min_items: Optional[int] = None
|
73
|
+
max_items: Optional[int] = None
|
74
|
+
unique_items: bool = False
|
75
|
+
items: Optional['SchemaProperty'] = None
|
76
|
+
|
77
|
+
# Object constraints
|
78
|
+
properties: Optional[Dict[str, 'SchemaProperty']] = None
|
79
|
+
additional_properties: Union[bool, 'SchemaProperty'] = True
|
80
|
+
required_properties: Optional[List[str]] = None
|
81
|
+
|
82
|
+
# Advanced
|
83
|
+
dependencies: Optional[Dict[str, Union[List[str], 'SchemaProperty']]] = None
|
84
|
+
one_of: Optional[List['SchemaProperty']] = None
|
85
|
+
any_of: Optional[List['SchemaProperty']] = None
|
86
|
+
all_of: Optional[List['SchemaProperty']] = None
|
87
|
+
not_schema: Optional['SchemaProperty'] = None
|
88
|
+
|
89
|
+
# Custom validation
|
90
|
+
validator: Optional[Callable[[Any], bool]] = None
|
91
|
+
transformer: Optional[Callable[[Any], Any]] = None
|
92
|
+
|
93
|
+
# Metadata
|
94
|
+
deprecated: bool = False
|
95
|
+
examples: Optional[List[Any]] = None
|
96
|
+
read_only: bool = False
|
97
|
+
write_only: bool = False
|
98
|
+
|
99
|
+
|
100
|
+
@dataclass
|
101
|
+
class ConfigSchema:
|
102
|
+
"""Complete configuration schema"""
|
103
|
+
title: str
|
104
|
+
description: Optional[str] = None
|
105
|
+
version: str = "1.0.0"
|
106
|
+
properties: Dict[str, SchemaProperty] = field(default_factory=dict)
|
107
|
+
required: List[str] = field(default_factory=list)
|
108
|
+
additional_properties: Union[bool, SchemaProperty] = True
|
109
|
+
|
110
|
+
# Schema metadata
|
111
|
+
schema_id: Optional[str] = None
|
112
|
+
schema_uri: Optional[str] = None
|
113
|
+
|
114
|
+
# Defaults
|
115
|
+
defaults: Dict[str, Any] = field(default_factory=dict)
|
116
|
+
|
117
|
+
# Validation rules
|
118
|
+
dependencies: Optional[Dict[str, Union[List[str], SchemaProperty]]] = None
|
119
|
+
pattern_properties: Optional[Dict[str, SchemaProperty]] = None
|
120
|
+
|
121
|
+
# Conditional schemas
|
122
|
+
if_schema: Optional['ConfigSchema'] = None
|
123
|
+
then_schema: Optional['ConfigSchema'] = None
|
124
|
+
else_schema: Optional['ConfigSchema'] = None
|
125
|
+
|
126
|
+
# Composition
|
127
|
+
all_of: Optional[List['ConfigSchema']] = None
|
128
|
+
any_of: Optional[List['ConfigSchema']] = None
|
129
|
+
one_of: Optional[List['ConfigSchema']] = None
|
130
|
+
not_schema: Optional['ConfigSchema'] = None
|
131
|
+
|
132
|
+
# Custom handlers
|
133
|
+
pre_validators: List[Callable] = field(default_factory=list)
|
134
|
+
post_validators: List[Callable] = field(default_factory=list)
|
135
|
+
transformers: List[Callable] = field(default_factory=list)
|
136
|
+
|
137
|
+
|
138
|
+
class SchemaBuilder:
|
139
|
+
"""Builder for creating configuration schemas fluently"""
|
140
|
+
|
141
|
+
def __init__(self, title: str):
|
142
|
+
self.schema = ConfigSchema(title=title)
|
143
|
+
self.logger = get_logger(self.__class__.__name__)
|
144
|
+
|
145
|
+
def description(self, desc: str) -> 'SchemaBuilder':
|
146
|
+
"""Set schema description"""
|
147
|
+
self.schema.description = desc
|
148
|
+
return self
|
149
|
+
|
150
|
+
def version(self, ver: str) -> 'SchemaBuilder':
|
151
|
+
"""Set schema version"""
|
152
|
+
self.schema.version = ver
|
153
|
+
return self
|
154
|
+
|
155
|
+
def property(
|
156
|
+
self,
|
157
|
+
name: str,
|
158
|
+
type: Union[SchemaType, str],
|
159
|
+
**kwargs
|
160
|
+
) -> 'SchemaBuilder':
|
161
|
+
"""Add a property to the schema"""
|
162
|
+
if isinstance(type, str):
|
163
|
+
type = SchemaType(type)
|
164
|
+
|
165
|
+
prop = SchemaProperty(type=type, **kwargs)
|
166
|
+
self.schema.properties[name] = prop
|
167
|
+
|
168
|
+
if kwargs.get('required', False):
|
169
|
+
if name not in self.schema.required:
|
170
|
+
self.schema.required.append(name)
|
171
|
+
|
172
|
+
return self
|
173
|
+
|
174
|
+
def string(self, name: str, **kwargs) -> 'SchemaBuilder':
|
175
|
+
"""Add string property"""
|
176
|
+
return self.property(name, SchemaType.STRING, **kwargs)
|
177
|
+
|
178
|
+
def integer(self, name: str, **kwargs) -> 'SchemaBuilder':
|
179
|
+
"""Add integer property"""
|
180
|
+
return self.property(name, SchemaType.INTEGER, **kwargs)
|
181
|
+
|
182
|
+
def number(self, name: str, **kwargs) -> 'SchemaBuilder':
|
183
|
+
"""Add number property"""
|
184
|
+
return self.property(name, SchemaType.NUMBER, **kwargs)
|
185
|
+
|
186
|
+
def boolean(self, name: str, **kwargs) -> 'SchemaBuilder':
|
187
|
+
"""Add boolean property"""
|
188
|
+
return self.property(name, SchemaType.BOOLEAN, **kwargs)
|
189
|
+
|
190
|
+
def array(self, name: str, items: Optional[SchemaProperty] = None, **kwargs) -> 'SchemaBuilder':
|
191
|
+
"""Add array property"""
|
192
|
+
return self.property(name, SchemaType.ARRAY, items=items, **kwargs)
|
193
|
+
|
194
|
+
def object(self, name: str, properties: Optional[Dict[str, SchemaProperty]] = None, **kwargs) -> 'SchemaBuilder':
|
195
|
+
"""Add object property"""
|
196
|
+
return self.property(name, SchemaType.OBJECT, properties=properties, **kwargs)
|
197
|
+
|
198
|
+
def enum(self, name: str, values: List[Any], **kwargs) -> 'SchemaBuilder':
|
199
|
+
"""Add enum property"""
|
200
|
+
return self.property(name, SchemaType.STRING, enum=values, **kwargs)
|
201
|
+
|
202
|
+
def required_fields(self, *fields: str) -> 'SchemaBuilder':
|
203
|
+
"""Mark fields as required"""
|
204
|
+
for field in fields:
|
205
|
+
if field not in self.schema.required:
|
206
|
+
self.schema.required.append(field)
|
207
|
+
return self
|
208
|
+
|
209
|
+
def default(self, name: str, value: Any) -> 'SchemaBuilder':
|
210
|
+
"""Set default value for property"""
|
211
|
+
if name in self.schema.properties:
|
212
|
+
self.schema.properties[name].default = value
|
213
|
+
self.schema.defaults[name] = value
|
214
|
+
return self
|
215
|
+
|
216
|
+
def dependency(self, field: str, depends_on: Union[str, List[str]]) -> 'SchemaBuilder':
|
217
|
+
"""Add field dependency"""
|
218
|
+
if self.schema.dependencies is None:
|
219
|
+
self.schema.dependencies = {}
|
220
|
+
|
221
|
+
if isinstance(depends_on, str):
|
222
|
+
depends_on = [depends_on]
|
223
|
+
|
224
|
+
self.schema.dependencies[field] = depends_on
|
225
|
+
return self
|
226
|
+
|
227
|
+
def validator(self, func: Callable) -> 'SchemaBuilder':
|
228
|
+
"""Add custom validator"""
|
229
|
+
self.schema.post_validators.append(func)
|
230
|
+
return self
|
231
|
+
|
232
|
+
def transformer(self, func: Callable) -> 'SchemaBuilder':
|
233
|
+
"""Add transformer function"""
|
234
|
+
self.schema.transformers.append(func)
|
235
|
+
return self
|
236
|
+
|
237
|
+
def build(self) -> ConfigSchema:
|
238
|
+
"""Build and return the schema"""
|
239
|
+
return self.schema
|
240
|
+
|
241
|
+
|
242
|
+
class SchemaValidator:
|
243
|
+
"""Validates configurations against schemas"""
|
244
|
+
|
245
|
+
def __init__(self):
|
246
|
+
self.logger = get_logger(self.__class__.__name__)
|
247
|
+
self.errors: List[str] = []
|
248
|
+
self.warnings: List[str] = []
|
249
|
+
|
250
|
+
def validate(self, config: Dict[str, Any], schema: ConfigSchema) -> bool:
|
251
|
+
"""Validate configuration against schema"""
|
252
|
+
self.errors = []
|
253
|
+
self.warnings = []
|
254
|
+
|
255
|
+
# Run pre-validators
|
256
|
+
for validator in schema.pre_validators:
|
257
|
+
if not validator(config):
|
258
|
+
self.errors.append(f"Pre-validation failed: {validator.__name__}")
|
259
|
+
return False
|
260
|
+
|
261
|
+
# Validate required fields
|
262
|
+
for field in schema.required:
|
263
|
+
if field not in config:
|
264
|
+
self.errors.append(f"Required field missing: {field}")
|
265
|
+
|
266
|
+
# Validate properties
|
267
|
+
for name, prop in schema.properties.items():
|
268
|
+
if name in config:
|
269
|
+
self._validate_property(config[name], prop, name)
|
270
|
+
|
271
|
+
# Check additional properties
|
272
|
+
if not schema.additional_properties:
|
273
|
+
extra = set(config.keys()) - set(schema.properties.keys())
|
274
|
+
if extra:
|
275
|
+
self.errors.append(f"Additional properties not allowed: {extra}")
|
276
|
+
|
277
|
+
# Validate dependencies
|
278
|
+
if schema.dependencies:
|
279
|
+
self._validate_dependencies(config, schema.dependencies)
|
280
|
+
|
281
|
+
# Run post-validators
|
282
|
+
for validator in schema.post_validators:
|
283
|
+
if not validator(config):
|
284
|
+
self.errors.append(f"Post-validation failed: {validator.__name__}")
|
285
|
+
|
286
|
+
return len(self.errors) == 0
|
287
|
+
|
288
|
+
def _validate_property(self, value: Any, prop: SchemaProperty, path: str):
|
289
|
+
"""Validate a single property"""
|
290
|
+
# Check nullable
|
291
|
+
if value is None:
|
292
|
+
if not prop.nullable:
|
293
|
+
self.errors.append(f"{path}: null value not allowed")
|
294
|
+
return
|
295
|
+
|
296
|
+
# Check type
|
297
|
+
if not self._check_type(value, prop.type):
|
298
|
+
self.errors.append(f"{path}: type mismatch, expected {prop.type}")
|
299
|
+
|
300
|
+
# Check constraints based on type
|
301
|
+
if isinstance(value, (int, float)):
|
302
|
+
self._validate_numeric(value, prop, path)
|
303
|
+
elif isinstance(value, str):
|
304
|
+
self._validate_string(value, prop, path)
|
305
|
+
elif isinstance(value, list):
|
306
|
+
self._validate_array(value, prop, path)
|
307
|
+
elif isinstance(value, dict):
|
308
|
+
self._validate_object(value, prop, path)
|
309
|
+
|
310
|
+
# Check enum
|
311
|
+
if prop.enum and value not in prop.enum:
|
312
|
+
self.errors.append(f"{path}: value must be one of {prop.enum}")
|
313
|
+
|
314
|
+
# Check const
|
315
|
+
if prop.const is not None and value != prop.const:
|
316
|
+
self.errors.append(f"{path}: value must be {prop.const}")
|
317
|
+
|
318
|
+
# Run custom validator
|
319
|
+
if prop.validator and not prop.validator(value):
|
320
|
+
self.errors.append(f"{path}: custom validation failed")
|
321
|
+
|
322
|
+
def _check_type(self, value: Any, expected: Union[SchemaType, List[SchemaType]]) -> bool:
|
323
|
+
"""Check if value matches expected type"""
|
324
|
+
if isinstance(expected, list):
|
325
|
+
return any(self._check_type(value, t) for t in expected)
|
326
|
+
|
327
|
+
type_map = {
|
328
|
+
SchemaType.STRING: str,
|
329
|
+
SchemaType.INTEGER: int,
|
330
|
+
SchemaType.NUMBER: (int, float),
|
331
|
+
SchemaType.BOOLEAN: bool,
|
332
|
+
SchemaType.ARRAY: list,
|
333
|
+
SchemaType.OBJECT: dict,
|
334
|
+
SchemaType.NULL: type(None),
|
335
|
+
SchemaType.ANY: object
|
336
|
+
}
|
337
|
+
|
338
|
+
expected_type = type_map.get(expected, object)
|
339
|
+
return isinstance(value, expected_type)
|
340
|
+
|
341
|
+
def _validate_numeric(self, value: Union[int, float], prop: SchemaProperty, path: str):
|
342
|
+
"""Validate numeric constraints"""
|
343
|
+
if prop.minimum is not None and value < prop.minimum:
|
344
|
+
self.errors.append(f"{path}: value {value} is below minimum {prop.minimum}")
|
345
|
+
|
346
|
+
if prop.maximum is not None and value > prop.maximum:
|
347
|
+
self.errors.append(f"{path}: value {value} exceeds maximum {prop.maximum}")
|
348
|
+
|
349
|
+
if prop.exclusive_minimum is not None and value <= prop.exclusive_minimum:
|
350
|
+
self.errors.append(f"{path}: value {value} must be greater than {prop.exclusive_minimum}")
|
351
|
+
|
352
|
+
if prop.exclusive_maximum is not None and value >= prop.exclusive_maximum:
|
353
|
+
self.errors.append(f"{path}: value {value} must be less than {prop.exclusive_maximum}")
|
354
|
+
|
355
|
+
def _validate_string(self, value: str, prop: SchemaProperty, path: str):
|
356
|
+
"""Validate string constraints"""
|
357
|
+
if prop.min_length is not None and len(value) < prop.min_length:
|
358
|
+
self.errors.append(f"{path}: length {len(value)} is below minimum {prop.min_length}")
|
359
|
+
|
360
|
+
if prop.max_length is not None and len(value) > prop.max_length:
|
361
|
+
self.errors.append(f"{path}: length {len(value)} exceeds maximum {prop.max_length}")
|
362
|
+
|
363
|
+
if prop.pattern:
|
364
|
+
import re
|
365
|
+
if not re.match(prop.pattern, value):
|
366
|
+
self.errors.append(f"{path}: does not match pattern {prop.pattern}")
|
367
|
+
|
368
|
+
if prop.format:
|
369
|
+
if not self._validate_format(value, prop.format):
|
370
|
+
self.errors.append(f"{path}: invalid format {prop.format.value}")
|
371
|
+
|
372
|
+
def _validate_array(self, value: List, prop: SchemaProperty, path: str):
|
373
|
+
"""Validate array constraints"""
|
374
|
+
if prop.min_items is not None and len(value) < prop.min_items:
|
375
|
+
self.errors.append(f"{path}: array length {len(value)} is below minimum {prop.min_items}")
|
376
|
+
|
377
|
+
if prop.max_items is not None and len(value) > prop.max_items:
|
378
|
+
self.errors.append(f"{path}: array length {len(value)} exceeds maximum {prop.max_items}")
|
379
|
+
|
380
|
+
if prop.unique_items:
|
381
|
+
seen = set()
|
382
|
+
for item in value:
|
383
|
+
item_key = str(item) if not isinstance(item, (dict, list)) else json.dumps(item, sort_keys=True)
|
384
|
+
if item_key in seen:
|
385
|
+
self.errors.append(f"{path}: duplicate items not allowed")
|
386
|
+
break
|
387
|
+
seen.add(item_key)
|
388
|
+
|
389
|
+
if prop.items:
|
390
|
+
for i, item in enumerate(value):
|
391
|
+
self._validate_property(item, prop.items, f"{path}[{i}]")
|
392
|
+
|
393
|
+
def _validate_object(self, value: Dict, prop: SchemaProperty, path: str):
|
394
|
+
"""Validate object constraints"""
|
395
|
+
if prop.properties:
|
396
|
+
for name, sub_prop in prop.properties.items():
|
397
|
+
if name in value:
|
398
|
+
self._validate_property(value[name], sub_prop, f"{path}.{name}")
|
399
|
+
|
400
|
+
if prop.required_properties:
|
401
|
+
for req in prop.required_properties:
|
402
|
+
if req not in value:
|
403
|
+
self.errors.append(f"{path}: required property '{req}' missing")
|
404
|
+
|
405
|
+
if not prop.additional_properties:
|
406
|
+
if prop.properties:
|
407
|
+
extra = set(value.keys()) - set(prop.properties.keys())
|
408
|
+
if extra:
|
409
|
+
self.errors.append(f"{path}: additional properties not allowed: {extra}")
|
410
|
+
|
411
|
+
def _validate_dependencies(self, config: Dict, dependencies: Dict):
|
412
|
+
"""Validate field dependencies"""
|
413
|
+
for field, deps in dependencies.items():
|
414
|
+
if field in config:
|
415
|
+
if isinstance(deps, list):
|
416
|
+
for dep in deps:
|
417
|
+
if dep not in config:
|
418
|
+
self.errors.append(f"Field '{field}' requires '{dep}' to be present")
|
419
|
+
|
420
|
+
def _validate_format(self, value: str, format: SchemaFormat) -> bool:
|
421
|
+
"""Validate string format"""
|
422
|
+
validators = {
|
423
|
+
SchemaFormat.EMAIL: lambda v: '@' in v and '.' in v.split('@')[1],
|
424
|
+
SchemaFormat.DATE: lambda v: self._try_parse_date(v, '%Y-%m-%d'),
|
425
|
+
SchemaFormat.DATETIME: lambda v: self._try_parse_date(v, '%Y-%m-%dT%H:%M:%S'),
|
426
|
+
SchemaFormat.UUID: lambda v: self._validate_uuid(v),
|
427
|
+
SchemaFormat.IPV4: lambda v: self._validate_ipv4(v),
|
428
|
+
SchemaFormat.IPV6: lambda v: self._validate_ipv6(v),
|
429
|
+
SchemaFormat.URI: lambda v: '://' in v,
|
430
|
+
SchemaFormat.PATH: lambda v: True, # Any string is valid path
|
431
|
+
SchemaFormat.SEMVER: lambda v: self._validate_semver(v)
|
432
|
+
}
|
433
|
+
|
434
|
+
validator = validators.get(format)
|
435
|
+
return validator(value) if validator else True
|
436
|
+
|
437
|
+
def _try_parse_date(self, value: str, format: str) -> bool:
|
438
|
+
"""Try to parse date string"""
|
439
|
+
try:
|
440
|
+
datetime.strptime(value, format)
|
441
|
+
return True
|
442
|
+
except:
|
443
|
+
return False
|
444
|
+
|
445
|
+
def _validate_uuid(self, value: str) -> bool:
|
446
|
+
"""Validate UUID format"""
|
447
|
+
import uuid
|
448
|
+
try:
|
449
|
+
uuid.UUID(value)
|
450
|
+
return True
|
451
|
+
except:
|
452
|
+
return False
|
453
|
+
|
454
|
+
def _validate_ipv4(self, value: str) -> bool:
|
455
|
+
"""Validate IPv4 address"""
|
456
|
+
import ipaddress
|
457
|
+
try:
|
458
|
+
ipaddress.IPv4Address(value)
|
459
|
+
return True
|
460
|
+
except:
|
461
|
+
return False
|
462
|
+
|
463
|
+
def _validate_ipv6(self, value: str) -> bool:
|
464
|
+
"""Validate IPv6 address"""
|
465
|
+
import ipaddress
|
466
|
+
try:
|
467
|
+
ipaddress.IPv6Address(value)
|
468
|
+
return True
|
469
|
+
except:
|
470
|
+
return False
|
471
|
+
|
472
|
+
def _validate_semver(self, value: str) -> bool:
|
473
|
+
"""Validate semantic version"""
|
474
|
+
import re
|
475
|
+
pattern = r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
|
476
|
+
return bool(re.match(pattern, value))
|
477
|
+
|
478
|
+
|
479
|
+
class SchemaRegistry:
|
480
|
+
"""Registry for managing configuration schemas"""
|
481
|
+
|
482
|
+
def __init__(self):
|
483
|
+
self.logger = get_logger(self.__class__.__name__)
|
484
|
+
self.schemas: Dict[str, ConfigSchema] = {}
|
485
|
+
self.versions: Dict[str, Dict[str, ConfigSchema]] = {}
|
486
|
+
|
487
|
+
def register(self, schema: ConfigSchema, name: Optional[str] = None):
|
488
|
+
"""Register a schema"""
|
489
|
+
name = name or schema.title
|
490
|
+
|
491
|
+
# Store by name
|
492
|
+
self.schemas[name] = schema
|
493
|
+
|
494
|
+
# Store by version
|
495
|
+
if name not in self.versions:
|
496
|
+
self.versions[name] = {}
|
497
|
+
self.versions[name][schema.version] = schema
|
498
|
+
|
499
|
+
self.logger.info(f"Registered schema: {name} v{schema.version}")
|
500
|
+
|
501
|
+
def get(self, name: str, version: Optional[str] = None) -> Optional[ConfigSchema]:
|
502
|
+
"""Get schema by name and optionally version"""
|
503
|
+
if version:
|
504
|
+
return self.versions.get(name, {}).get(version)
|
505
|
+
return self.schemas.get(name)
|
506
|
+
|
507
|
+
def list_schemas(self) -> List[str]:
|
508
|
+
"""List all registered schemas"""
|
509
|
+
return list(self.schemas.keys())
|
510
|
+
|
511
|
+
def list_versions(self, name: str) -> List[str]:
|
512
|
+
"""List all versions of a schema"""
|
513
|
+
return list(self.versions.get(name, {}).keys())
|
514
|
+
|
515
|
+
|
516
|
+
class ConfigMigration:
|
517
|
+
"""Handles configuration migration between schema versions"""
|
518
|
+
|
519
|
+
def __init__(self):
|
520
|
+
self.logger = get_logger(self.__class__.__name__)
|
521
|
+
self.migrations: Dict[tuple, Callable] = {}
|
522
|
+
|
523
|
+
def register_migration(
|
524
|
+
self,
|
525
|
+
from_version: str,
|
526
|
+
to_version: str,
|
527
|
+
migration_func: Callable[[Dict], Dict]
|
528
|
+
):
|
529
|
+
"""Register a migration function"""
|
530
|
+
key = (from_version, to_version)
|
531
|
+
self.migrations[key] = migration_func
|
532
|
+
self.logger.info(f"Registered migration: {from_version} -> {to_version}")
|
533
|
+
|
534
|
+
def migrate(
|
535
|
+
self,
|
536
|
+
config: Dict[str, Any],
|
537
|
+
from_version: str,
|
538
|
+
to_version: str
|
539
|
+
) -> Dict[str, Any]:
|
540
|
+
"""Migrate configuration between versions"""
|
541
|
+
key = (from_version, to_version)
|
542
|
+
|
543
|
+
if key in self.migrations:
|
544
|
+
# Direct migration available
|
545
|
+
return self.migrations[key](config)
|
546
|
+
|
547
|
+
# Try to find migration path
|
548
|
+
path = self._find_migration_path(from_version, to_version)
|
549
|
+
|
550
|
+
if not path:
|
551
|
+
raise ValueError(f"No migration path from {from_version} to {to_version}")
|
552
|
+
|
553
|
+
# Apply migrations in sequence
|
554
|
+
current = config
|
555
|
+
for i in range(len(path) - 1):
|
556
|
+
key = (path[i], path[i + 1])
|
557
|
+
if key in self.migrations:
|
558
|
+
current = self.migrations[key](current)
|
559
|
+
self.logger.info(f"Applied migration: {path[i]} -> {path[i + 1]}")
|
560
|
+
|
561
|
+
return current
|
562
|
+
|
563
|
+
def _find_migration_path(self, from_version: str, to_version: str) -> Optional[List[str]]:
|
564
|
+
"""Find migration path between versions using BFS"""
|
565
|
+
from collections import deque
|
566
|
+
|
567
|
+
# Build graph of migrations
|
568
|
+
graph = {}
|
569
|
+
for (from_v, to_v) in self.migrations.keys():
|
570
|
+
if from_v not in graph:
|
571
|
+
graph[from_v] = []
|
572
|
+
graph[from_v].append(to_v)
|
573
|
+
|
574
|
+
# BFS to find path
|
575
|
+
queue = deque([(from_version, [from_version])])
|
576
|
+
visited = {from_version}
|
577
|
+
|
578
|
+
while queue:
|
579
|
+
current, path = queue.popleft()
|
580
|
+
|
581
|
+
if current == to_version:
|
582
|
+
return path
|
583
|
+
|
584
|
+
for next_v in graph.get(current, []):
|
585
|
+
if next_v not in visited:
|
586
|
+
visited.add(next_v)
|
587
|
+
queue.append((next_v, path + [next_v]))
|
588
|
+
|
589
|
+
return None
|
590
|
+
|
591
|
+
|
592
|
+
class TypedConfig(Generic[T]):
|
593
|
+
"""Type-safe configuration wrapper"""
|
594
|
+
|
595
|
+
def __init__(self, schema: ConfigSchema, data: Dict[str, Any]):
|
596
|
+
self.schema = schema
|
597
|
+
self._data = data
|
598
|
+
self._validator = SchemaValidator()
|
599
|
+
|
600
|
+
# Validate on initialization
|
601
|
+
if not self._validator.validate(data, schema):
|
602
|
+
raise ValueError(f"Invalid configuration: {self._validator.errors}")
|
603
|
+
|
604
|
+
def get(self, key: str, default: Any = None) -> Any:
|
605
|
+
"""Get configuration value"""
|
606
|
+
return self._data.get(key, default)
|
607
|
+
|
608
|
+
def set(self, key: str, value: Any):
|
609
|
+
"""Set configuration value with validation"""
|
610
|
+
# Create temporary config with new value
|
611
|
+
temp = self._data.copy()
|
612
|
+
temp[key] = value
|
613
|
+
|
614
|
+
# Validate
|
615
|
+
if not self._validator.validate(temp, self.schema):
|
616
|
+
raise ValueError(f"Invalid value for {key}: {self._validator.errors}")
|
617
|
+
|
618
|
+
self._data[key] = value
|
619
|
+
|
620
|
+
def to_dict(self) -> Dict[str, Any]:
|
621
|
+
"""Convert to dictionary"""
|
622
|
+
return self._data.copy()
|
623
|
+
|
624
|
+
def __getitem__(self, key: str) -> Any:
|
625
|
+
"""Dictionary-style access"""
|
626
|
+
return self._data[key]
|
627
|
+
|
628
|
+
def __setitem__(self, key: str, value: Any):
|
629
|
+
"""Dictionary-style setting with validation"""
|
630
|
+
self.set(key, value)
|
631
|
+
|
632
|
+
|
633
|
+
# Predefined common schemas
|
634
|
+
def create_database_schema() -> ConfigSchema:
|
635
|
+
"""Create common database configuration schema"""
|
636
|
+
return (SchemaBuilder("Database Configuration")
|
637
|
+
.string("host", required=True, default="localhost")
|
638
|
+
.integer("port", required=True, minimum=1, maximum=65535, default=5432)
|
639
|
+
.string("database", required=True)
|
640
|
+
.string("username", required=True)
|
641
|
+
.string("password", write_only=True)
|
642
|
+
.integer("pool_size", minimum=1, maximum=100, default=10)
|
643
|
+
.integer("timeout", minimum=1, default=30)
|
644
|
+
.boolean("ssl", default=False)
|
645
|
+
.build())
|
646
|
+
|
647
|
+
|
648
|
+
def create_api_schema() -> ConfigSchema:
|
649
|
+
"""Create common API configuration schema"""
|
650
|
+
return (SchemaBuilder("API Configuration")
|
651
|
+
.string("base_url", required=True, format=SchemaFormat.URI)
|
652
|
+
.string("api_key", required=True, write_only=True)
|
653
|
+
.integer("timeout", minimum=1, default=30)
|
654
|
+
.integer("retry_count", minimum=0, maximum=10, default=3)
|
655
|
+
.number("retry_delay", minimum=0, default=1.0)
|
656
|
+
.array("allowed_methods", default=["GET", "POST", "PUT", "DELETE"])
|
657
|
+
.object("headers", default={})
|
658
|
+
.boolean("verify_ssl", default=True)
|
659
|
+
.build())
|
660
|
+
|
661
|
+
|
662
|
+
def create_logging_schema() -> ConfigSchema:
|
663
|
+
"""Create common logging configuration schema"""
|
664
|
+
return (SchemaBuilder("Logging Configuration")
|
665
|
+
.enum("level", ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], default="INFO")
|
666
|
+
.string("format", default="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
667
|
+
.string("file", format=SchemaFormat.PATH)
|
668
|
+
.integer("max_size", minimum=1, default=10485760) # 10MB
|
669
|
+
.integer("backup_count", minimum=0, default=5)
|
670
|
+
.boolean("console", default=True)
|
671
|
+
.boolean("file_enabled", default=False)
|
672
|
+
.build())
|
673
|
+
|
674
|
+
|
675
|
+
# Export main components
|
676
|
+
__all__ = [
|
677
|
+
'ConfigSchema',
|
678
|
+
'SchemaProperty',
|
679
|
+
'SchemaBuilder',
|
680
|
+
'SchemaValidator',
|
681
|
+
'SchemaRegistry',
|
682
|
+
'ConfigMigration',
|
683
|
+
'TypedConfig',
|
684
|
+
'SchemaType',
|
685
|
+
'SchemaFormat',
|
686
|
+
'create_database_schema',
|
687
|
+
'create_api_schema',
|
688
|
+
'create_logging_schema'
|
689
|
+
]
|