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