Coverage for fastblocks/mcp/configuration.py: 0%

318 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-09 00:47 -0700

1"""Advanced adapter configuration management for FastBlocks MCP system.""" 

2 

3import json 

4import os 

5from contextlib import suppress 

6from dataclasses import dataclass, field 

7from datetime import datetime 

8from enum import Enum 

9from pathlib import Path 

10from typing import Any 

11from uuid import uuid4 

12 

13import yaml 

14from pydantic import BaseModel, Field, ValidationError, validator 

15 

16from .discovery import AdapterInfo 

17from .registry import AdapterRegistry 

18 

19 

20class ConfigurationProfile(str, Enum): 

21 """Configuration deployment profiles.""" 

22 

23 DEVELOPMENT = "development" 

24 STAGING = "staging" 

25 PRODUCTION = "production" 

26 

27 

28class ConfigurationStatus(str, Enum): 

29 """Configuration validation status.""" 

30 

31 VALID = "valid" 

32 WARNING = "warning" 

33 ERROR = "error" 

34 UNKNOWN = "unknown" 

35 

36 

37@dataclass 

38class EnvironmentVariable: 

39 """Environment variable configuration.""" 

40 

41 name: str 

42 value: str | None = None 

43 required: bool = True 

44 description: str = "" 

45 secret: bool = False 

46 default: str | None = None 

47 validator_pattern: str | None = None 

48 

49 

50@dataclass 

51class AdapterConfiguration: 

52 """Individual adapter configuration.""" 

53 

54 name: str 

55 enabled: bool = True 

56 settings: dict[str, Any] = field(default_factory=dict) 

57 environment_variables: list[EnvironmentVariable] = field(default_factory=list) 

58 dependencies: set[str] = field(default_factory=set) 

59 profile_overrides: dict[ConfigurationProfile, dict[str, Any]] = field( 

60 default_factory=dict 

61 ) 

62 health_check_config: dict[str, Any] = field(default_factory=dict) 

63 metadata: dict[str, Any] = field(default_factory=dict) 

64 

65 

66class ConfigurationSchema(BaseModel): 

67 """Pydantic schema for configuration validation.""" 

68 

69 version: str = "1.0" 

70 profile: ConfigurationProfile = ConfigurationProfile.DEVELOPMENT 

71 created_at: datetime = Field(default_factory=datetime.now) 

72 updated_at: datetime = Field(default_factory=datetime.now) 

73 adapters: dict[str, AdapterConfiguration] = Field(default_factory=dict) 

74 global_settings: dict[str, Any] = Field(default_factory=dict) 

75 global_environment: list[EnvironmentVariable] = Field(default_factory=list) 

76 

77 @validator("adapters", pre=True) 

78 def validate_adapters(cls, v: Any) -> dict[str, AdapterConfiguration]: 

79 if isinstance(v, dict): 

80 # Convert dict values to AdapterConfiguration objects if needed 

81 result: dict[str, AdapterConfiguration] = {} 

82 for key, value in v.items(): 

83 if isinstance(value, dict): 

84 result[key] = AdapterConfiguration(name=key, **value) 

85 else: 

86 result[key] = value 

87 return result 

88 # v should already be dict[str, AdapterConfiguration] if not dict 

89 return v if isinstance(v, dict) else {} 

90 

91 class Config: 

92 arbitrary_types_allowed = True 

93 

94 

95@dataclass 

96class ConfigurationValidationResult: 

97 """Result of configuration validation.""" 

98 

99 status: ConfigurationStatus 

100 errors: list[str] = field(default_factory=list) 

101 warnings: list[str] = field(default_factory=list) 

102 info: dict[str, Any] = field(default_factory=dict) 

103 adapter_results: dict[str, dict[str, Any]] = field(default_factory=dict) 

104 

105 

106@dataclass 

107class ConfigurationBackup: 

108 """Configuration backup metadata.""" 

109 

110 id: str 

111 name: str 

112 description: str 

113 created_at: datetime 

114 profile: ConfigurationProfile 

115 file_path: Path 

116 checksum: str 

117 

118 

119class ConfigurationManager: 

120 """Advanced configuration management for FastBlocks adapters.""" 

121 

122 def __init__(self, registry: AdapterRegistry, base_path: Path | None = None): 

123 """Initialize configuration manager.""" 

124 self.registry = registry 

125 self.base_path = base_path or Path.cwd() / ".fastblocks" 

126 self.config_dir = self.base_path / "config" 

127 self.backup_dir = self.base_path / "backups" 

128 self.templates_dir = self.base_path / "templates" 

129 

130 # Ensure directories exist 

131 for directory in (self.config_dir, self.backup_dir, self.templates_dir): 

132 directory.mkdir(parents=True, exist_ok=True) 

133 

134 async def initialize(self) -> None: 

135 """Initialize configuration manager.""" 

136 await self.registry.initialize() 

137 await self._ensure_default_templates() 

138 

139 async def get_available_adapters(self) -> dict[str, AdapterInfo]: 

140 """Get all available adapters for configuration.""" 

141 return await self.registry.list_available_adapters() 

142 

143 async def get_adapter_configuration_schema( 

144 self, adapter_name: str 

145 ) -> dict[str, Any]: 

146 """Get configuration schema for a specific adapter.""" 

147 adapter_info = await self.registry.get_adapter_info(adapter_name) 

148 if not adapter_info: 

149 raise ValueError(f"Adapter '{adapter_name}' not found") 

150 

151 # Build base schema 

152 schema = self._build_base_schema(adapter_name, adapter_info) 

153 

154 # Try to introspect adapter settings 

155 with suppress(Exception): 

156 adapter = await self.registry.get_adapter(adapter_name) 

157 self._introspect_adapter_settings(adapter, schema) 

158 

159 return schema 

160 

161 def _build_base_schema( 

162 self, adapter_name: str, adapter_info: AdapterInfo 

163 ) -> dict[str, Any]: 

164 """Build base schema structure.""" 

165 return { 

166 "name": adapter_name, 

167 "description": adapter_info.description, 

168 "category": adapter_info.category, 

169 "required_settings": [], 

170 "optional_settings": [], 

171 "environment_variables": [], 

172 "dependencies": [], 

173 } 

174 

175 def _introspect_adapter_settings( 

176 self, adapter: Any, schema: dict[str, Any] 

177 ) -> None: 

178 """Introspect adapter settings and populate schema.""" 

179 if not adapter or not hasattr(adapter, "settings"): 

180 return 

181 

182 settings = adapter.settings 

183 if not hasattr(settings, "__dict__"): 

184 return 

185 

186 # Categorize settings by requirement 

187 categorized = self._categorize_settings(settings.__dict__) 

188 schema["required_settings"] = categorized["required"] 

189 schema["optional_settings"] = categorized["optional"] 

190 

191 def _categorize_settings( 

192 self, settings_dict: dict[str, Any] 

193 ) -> dict[str, list[dict[str, Any]]]: 

194 """Categorize settings into required and optional.""" 

195 categorized: dict[str, list[dict[str, Any]]] = { 

196 "required": [], 

197 "optional": [], 

198 } 

199 

200 for key, value in settings_dict.items(): 

201 if key.startswith("_"): 

202 continue 

203 

204 setting_info = { 

205 "name": key, 

206 "type": type(value).__name__, 

207 "default": value, 

208 "required": value is None, 

209 } 

210 

211 category = "required" if setting_info["required"] else "optional" 

212 categorized[category].append(setting_info) 

213 

214 return categorized 

215 

216 async def create_configuration( 

217 self, 

218 profile: ConfigurationProfile = ConfigurationProfile.DEVELOPMENT, 

219 adapters: list[str] | None = None, 

220 ) -> ConfigurationSchema: 

221 """Create a new configuration.""" 

222 config = ConfigurationSchema(profile=profile) 

223 

224 if adapters: 

225 for adapter_name in adapters: 

226 adapter_config = await self._create_adapter_configuration(adapter_name) 

227 config.adapters[adapter_name] = adapter_config 

228 

229 return config 

230 

231 async def _create_adapter_configuration( 

232 self, adapter_name: str 

233 ) -> AdapterConfiguration: 

234 """Create configuration for a specific adapter.""" 

235 schema = await self.get_adapter_configuration_schema(adapter_name) 

236 

237 adapter_config = AdapterConfiguration(name=adapter_name) 

238 

239 # Set up environment variables based on schema 

240 for setting in schema.get("required_settings", []): 

241 env_var = EnvironmentVariable( 

242 name=f"FB_{adapter_name.upper()}_{setting['name'].upper()}", 

243 required=True, 

244 description=f"Required setting for {adapter_name}: {setting['name']}", 

245 ) 

246 adapter_config.environment_variables.append(env_var) 

247 

248 for setting in schema.get("optional_settings", []): 

249 env_var = EnvironmentVariable( 

250 name=f"FB_{adapter_name.upper()}_{setting['name'].upper()}", 

251 required=False, 

252 default=str(setting.get("default", "")), 

253 description=f"Optional setting for {adapter_name}: {setting['name']}", 

254 ) 

255 adapter_config.environment_variables.append(env_var) 

256 

257 return adapter_config 

258 

259 async def validate_configuration( 

260 self, config: ConfigurationSchema 

261 ) -> ConfigurationValidationResult: 

262 """Validate a configuration comprehensively.""" 

263 result = ConfigurationValidationResult(status=ConfigurationStatus.VALID) 

264 

265 try: 

266 # Validate configuration schema 

267 config_dict = config.dict() if hasattr(config, "dict") else config.__dict__ 

268 ConfigurationSchema(**config_dict) 

269 except ValidationError as e: 

270 result.status = ConfigurationStatus.ERROR 

271 result.errors.extend([str(error) for error in e.errors()]) 

272 except Exception as e: 

273 result.status = ConfigurationStatus.ERROR 

274 result.errors.append(f"Configuration validation error: {e}") 

275 

276 # Validate individual adapters 

277 for adapter_name, adapter_config in config.adapters.items(): 

278 adapter_result = await self._validate_adapter_configuration( 

279 adapter_name, adapter_config 

280 ) 

281 result.adapter_results[adapter_name] = adapter_result 

282 

283 if adapter_result.get("errors"): 

284 result.errors.extend( 

285 f"{adapter_name}: {error}" for error in adapter_result["errors"] 

286 ) 

287 result.status = ConfigurationStatus.ERROR 

288 

289 if adapter_result.get("warnings"): 

290 result.warnings.extend( 

291 f"{adapter_name}: {warning}" 

292 for warning in adapter_result["warnings"] 

293 ) 

294 if result.status == ConfigurationStatus.VALID: 

295 result.status = ConfigurationStatus.WARNING 

296 

297 # Validate dependencies 

298 await self._validate_dependencies(config, result) 

299 

300 # Validate environment variables 

301 await self._validate_environment_variables(config, result) 

302 

303 return result 

304 

305 async def _validate_adapter_configuration( 

306 self, adapter_name: str, adapter_config: AdapterConfiguration 

307 ) -> dict[str, Any]: 

308 """Validate individual adapter configuration.""" 

309 validation_result = await self.registry.validate_adapter(adapter_name) 

310 

311 result = { 

312 "valid": validation_result.get("valid", False), 

313 "errors": validation_result.get("errors", []), 

314 "warnings": validation_result.get("warnings", []), 

315 "info": validation_result.get("info", {}), 

316 } 

317 

318 # Additional configuration-specific validations 

319 if adapter_config.enabled: 

320 # Check required environment variables 

321 for env_var in adapter_config.environment_variables: 

322 if env_var.required and not env_var.value and not env_var.default: 

323 if env_var.name not in os.environ: 

324 result["warnings"].append( 

325 f"Required environment variable {env_var.name} is not set" 

326 ) 

327 

328 return result 

329 

330 async def _validate_dependencies( 

331 self, config: ConfigurationSchema, result: ConfigurationValidationResult 

332 ) -> None: 

333 """Validate adapter dependencies.""" 

334 enabled_adapters = { 

335 name for name, adapter in config.adapters.items() if adapter.enabled 

336 } 

337 

338 for adapter_name, adapter_config in config.adapters.items(): 

339 if not adapter_config.enabled: 

340 continue 

341 

342 for dependency in adapter_config.dependencies: 

343 if dependency not in enabled_adapters: 

344 result.errors.append( 

345 f"Adapter '{adapter_name}' depends on '{dependency}' which is not enabled" 

346 ) 

347 result.status = ConfigurationStatus.ERROR 

348 

349 def _check_duplicate_env_vars( 

350 self, 

351 config: ConfigurationSchema, 

352 result: ConfigurationValidationResult, 

353 all_env_vars: set[str], 

354 ) -> None: 

355 """Check for duplicate environment variable names.""" 

356 for adapter_config in config.adapters.values(): 

357 for env_var in adapter_config.environment_variables: 

358 if env_var.name in all_env_vars: 

359 result.warnings.append( 

360 f"Duplicate environment variable: {env_var.name}" 

361 ) 

362 all_env_vars.add(env_var.name) 

363 

364 def _is_env_var_missing(self, env_var: EnvironmentVariable) -> bool: 

365 """Check if a required environment variable is missing.""" 

366 return ( 

367 env_var.required 

368 and not env_var.value 

369 and not env_var.default 

370 and env_var.name not in os.environ 

371 ) 

372 

373 def _check_missing_required_vars( 

374 self, config: ConfigurationSchema, result: ConfigurationValidationResult 

375 ) -> None: 

376 """Check for missing required environment variables.""" 

377 for adapter_name, adapter_config in config.adapters.items(): 

378 if not adapter_config.enabled: 

379 continue 

380 

381 for env_var in adapter_config.environment_variables: 

382 if self._is_env_var_missing(env_var): 

383 result.warnings.append( 

384 f"Required environment variable {env_var.name} for {adapter_name} is not set" 

385 ) 

386 

387 async def _validate_environment_variables( 

388 self, config: ConfigurationSchema, result: ConfigurationValidationResult 

389 ) -> None: 

390 """Validate environment variable configuration.""" 

391 all_env_vars: set[str] = set() 

392 

393 # Check for duplicate environment variable names 

394 self._check_duplicate_env_vars(config, result, all_env_vars) 

395 

396 # Check for missing required variables 

397 self._check_missing_required_vars(config, result) 

398 

399 async def save_configuration( 

400 self, config: ConfigurationSchema, name: str | None = None 

401 ) -> Path: 

402 """Save configuration to YAML file.""" 

403 if not name: 

404 name = f"{config.profile.value}_{datetime.now().strftime('%Y%m%d_%H%M%S')}" 

405 

406 config_file = self.config_dir / f"{name}.yaml" 

407 

408 # Convert to serializable dict 

409 config_dict = self._serialize_configuration(config) 

410 

411 with config_file.open("w") as f: 

412 yaml.dump(config_dict, f, default_flow_style=False, sort_keys=False) 

413 

414 return config_file 

415 

416 async def load_configuration(self, name_or_path: str | Path) -> ConfigurationSchema: 

417 """Load configuration from YAML file.""" 

418 if isinstance(name_or_path, str): 

419 config_file = self.config_dir / f"{name_or_path}.yaml" 

420 if not config_file.exists(): 

421 config_file = Path(name_or_path) 

422 else: 

423 config_file = name_or_path 

424 

425 if not config_file.exists(): 

426 raise FileNotFoundError(f"Configuration file not found: {config_file}") 

427 

428 with config_file.open() as f: 

429 config_dict = yaml.safe_load(f) 

430 

431 return self._deserialize_configuration(config_dict) 

432 

433 def _serialize_configuration(self, config: ConfigurationSchema) -> dict[str, Any]: 

434 """Convert configuration to serializable dictionary.""" 

435 result = { 

436 "version": config.version, 

437 "profile": config.profile.value, 

438 "created_at": config.created_at.isoformat(), 

439 "updated_at": config.updated_at.isoformat(), 

440 "global_settings": config.global_settings, 

441 "global_environment": [ 

442 { 

443 "name": env_var.name, 

444 "value": env_var.value, 

445 "required": env_var.required, 

446 "description": env_var.description, 

447 "secret": env_var.secret, 

448 "default": env_var.default, 

449 "validator_pattern": env_var.validator_pattern, 

450 } 

451 for env_var in config.global_environment 

452 ], 

453 "adapters": {}, 

454 } 

455 

456 adapters_dict: dict[str, Any] = {} 

457 for adapter_name, adapter_config in config.adapters.items(): 

458 adapters_dict[adapter_name] = { 

459 "enabled": adapter_config.enabled, 

460 "settings": adapter_config.settings, 

461 "environment_variables": [ 

462 { 

463 "name": env_var.name, 

464 "value": env_var.value, 

465 "required": env_var.required, 

466 "description": env_var.description, 

467 "secret": env_var.secret, 

468 "default": env_var.default, 

469 "validator_pattern": env_var.validator_pattern, 

470 } 

471 for env_var in adapter_config.environment_variables 

472 ], 

473 "dependencies": list(adapter_config.dependencies), 

474 "profile_overrides": { 

475 profile.value: overrides 

476 for profile, overrides in adapter_config.profile_overrides.items() 

477 }, 

478 "health_check_config": adapter_config.health_check_config, 

479 "metadata": adapter_config.metadata, 

480 } 

481 

482 result["adapters"] = adapters_dict 

483 return result 

484 

485 def _deserialize_configuration( 

486 self, config_dict: dict[str, Any] 

487 ) -> ConfigurationSchema: 

488 """Convert dictionary to configuration object.""" 

489 # Convert string dates back to datetime objects 

490 if "created_at" in config_dict: 

491 config_dict["created_at"] = datetime.fromisoformat( 

492 config_dict["created_at"] 

493 ) 

494 if "updated_at" in config_dict: 

495 config_dict["updated_at"] = datetime.fromisoformat( 

496 config_dict["updated_at"] 

497 ) 

498 

499 # Convert profile string to enum 

500 if "profile" in config_dict: 

501 config_dict["profile"] = ConfigurationProfile(config_dict["profile"]) 

502 

503 # Convert global environment variables 

504 global_env = [ 

505 EnvironmentVariable(**env_data) 

506 for env_data in config_dict.get("global_environment", []) 

507 ] 

508 config_dict["global_environment"] = global_env 

509 

510 # Convert adapter configurations 

511 adapters = {} 

512 for adapter_name, adapter_data in config_dict.get("adapters", {}).items(): 

513 # Convert environment variables 

514 env_vars = [ 

515 EnvironmentVariable(**env_data) 

516 for env_data in adapter_data.get("environment_variables", []) 

517 ] 

518 adapter_data["environment_variables"] = env_vars 

519 

520 # Convert profile overrides 

521 profile_overrides = {} 

522 for profile_str, overrides in adapter_data.get( 

523 "profile_overrides", {} 

524 ).items(): 

525 profile_overrides[ConfigurationProfile(profile_str)] = overrides 

526 adapter_data["profile_overrides"] = profile_overrides 

527 

528 # Convert dependencies to set 

529 adapter_data["dependencies"] = set(adapter_data.get("dependencies", [])) 

530 

531 adapters[adapter_name] = AdapterConfiguration( 

532 name=adapter_name, **adapter_data 

533 ) 

534 

535 config_dict["adapters"] = adapters 

536 

537 return ConfigurationSchema(**config_dict) 

538 

539 async def generate_environment_file( 

540 self, config: ConfigurationSchema, output_path: Path | None = None 

541 ) -> Path: 

542 """Generate .env file from configuration.""" 

543 if not output_path: 

544 output_path = self.base_path / f".env.{config.profile.value}" 

545 

546 # Add global environment variables 

547 env_vars = [ 

548 self._format_env_var(env_var) for env_var in config.global_environment 

549 ] 

550 

551 # Add adapter environment variables 

552 for adapter_name, adapter_config in config.adapters.items(): 

553 if not adapter_config.enabled: 

554 continue 

555 

556 env_vars.append(f"\n# {adapter_name.upper()} ADAPTER") 

557 for env_var in adapter_config.environment_variables: 

558 env_vars.append(self._format_env_var(env_var)) 

559 

560 with output_path.open("w") as f: 

561 f.write("\n".join(env_vars)) 

562 

563 return output_path 

564 

565 def _format_env_var(self, env_var: EnvironmentVariable) -> str: 

566 """Format environment variable for .env file.""" 

567 lines = [] 

568 

569 if env_var.description: 

570 lines.append(f"# {env_var.description}") 

571 

572 if env_var.required: 

573 lines.append("# REQUIRED") 

574 

575 value = env_var.value or env_var.default or "" 

576 if env_var.secret and value: 

577 value = "***REDACTED***" 

578 

579 lines.append(f"{env_var.name}={value}") 

580 

581 return "\n".join(lines) 

582 

583 async def backup_configuration( 

584 self, config: ConfigurationSchema, name: str, description: str = "" 

585 ) -> ConfigurationBackup: 

586 """Create a backup of the configuration.""" 

587 backup_id = str(uuid4()) 

588 backup_file = self.backup_dir / f"{backup_id}_{name}.yaml" 

589 

590 # Save configuration 

591 config_dict = self._serialize_configuration(config) 

592 with backup_file.open("w") as f: 

593 yaml.dump(config_dict, f, default_flow_style=False) 

594 

595 # Calculate checksum 

596 import hashlib 

597 

598 with backup_file.open("rb") as fb: 

599 checksum = hashlib.sha256(fb.read()).hexdigest() 

600 

601 backup = ConfigurationBackup( 

602 id=backup_id, 

603 name=name, 

604 description=description, 

605 created_at=datetime.now(), 

606 profile=config.profile, 

607 file_path=backup_file, 

608 checksum=checksum, 

609 ) 

610 

611 # Save backup metadata 

612 metadata_file = self.backup_dir / f"{backup_id}_metadata.json" 

613 with metadata_file.open("w") as f: 

614 json.dump( 

615 { 

616 "id": backup.id, 

617 "name": backup.name, 

618 "description": backup.description, 

619 "created_at": backup.created_at.isoformat(), 

620 "profile": backup.profile.value, 

621 "file_path": str(backup.file_path), 

622 "checksum": backup.checksum, 

623 }, 

624 f, 

625 indent=2, 

626 ) 

627 

628 return backup 

629 

630 async def list_backups(self) -> list[ConfigurationBackup]: 

631 """List all configuration backups.""" 

632 backups = [] 

633 

634 for metadata_file in self.backup_dir.glob("*_metadata.json"): 

635 try: 

636 with metadata_file.open() as f: 

637 data = json.load(f) 

638 

639 backup = ConfigurationBackup( 

640 id=data["id"], 

641 name=data["name"], 

642 description=data["description"], 

643 created_at=datetime.fromisoformat(data["created_at"]), 

644 profile=ConfigurationProfile(data["profile"]), 

645 file_path=Path(data["file_path"]), 

646 checksum=data["checksum"], 

647 ) 

648 

649 # Verify file still exists 

650 if backup.file_path.exists(): 

651 backups.append(backup) 

652 except Exception: 

653 # Skip corrupted metadata files 

654 continue 

655 

656 return sorted(backups, key=lambda b: b.created_at, reverse=True) 

657 

658 async def restore_backup(self, backup_id: str) -> ConfigurationSchema: 

659 """Restore configuration from backup.""" 

660 backups = await self.list_backups() 

661 backup = next((b for b in backups if b.id == backup_id), None) 

662 

663 if not backup: 

664 raise ValueError(f"Backup '{backup_id}' not found") 

665 

666 return await self.load_configuration(backup.file_path) 

667 

668 async def _ensure_default_templates(self) -> None: 

669 """Ensure default configuration templates exist.""" 

670 templates = { 

671 "minimal.yaml": self._create_minimal_template(), 

672 "development.yaml": self._create_development_template(), 

673 "production.yaml": self._create_production_template(), 

674 } 

675 

676 for template_name, template_config in templates.items(): 

677 template_file = self.templates_dir / template_name 

678 if not template_file.exists(): 

679 config_dict = self._serialize_configuration(template_config) 

680 with template_file.open("w") as f: 

681 yaml.dump(config_dict, f, default_flow_style=False) 

682 

683 def _create_minimal_template(self) -> ConfigurationSchema: 

684 """Create minimal configuration template.""" 

685 return ConfigurationSchema( 

686 profile=ConfigurationProfile.DEVELOPMENT, 

687 global_settings={"debug": True, "log_level": "INFO"}, 

688 ) 

689 

690 def _create_development_template(self) -> ConfigurationSchema: 

691 """Create development configuration template.""" 

692 config = ConfigurationSchema( 

693 profile=ConfigurationProfile.DEVELOPMENT, 

694 global_settings={"debug": True, "log_level": "DEBUG", "hot_reload": True}, 

695 ) 

696 

697 # Add common development adapters 

698 config.adapters["app"] = AdapterConfiguration( 

699 name="app", enabled=True, settings={"debug": True} 

700 ) 

701 

702 return config 

703 

704 def _create_production_template(self) -> ConfigurationSchema: 

705 """Create production configuration template.""" 

706 config = ConfigurationSchema( 

707 profile=ConfigurationProfile.PRODUCTION, 

708 global_settings={ 

709 "debug": False, 

710 "log_level": "WARNING", 

711 "hot_reload": False, 

712 }, 

713 ) 

714 

715 return config