55import sys
66from collections .abc import Mapping , MutableMapping
77from pathlib import Path
8- from typing import Any
8+ from typing import Any , override
99
1010import humps
1111import tomli
1212import trafaret as t
1313from pydantic import (
1414 AliasChoices ,
15- BaseModel ,
1615 ConfigDict ,
1716 Field ,
1817 field_validator ,
1918)
2019
2120from . import validators as tx
2221from .etcd import AsyncEtcd , ConfigScopes
23- from .exception import ConfigurationError
24- from .types import RedisHelperConfig
22+ from .exception import BackendAIError , ConfigurationError , ModelDefinitionValidationError
23+ from .types import BackendAISchema , RedisHelperConfig , SchemaValidationFailureInfo
2524
2625__all__ = (
2726 "ConfigurationError" ,
4039)
4140
4241
43- class BaseConfigSchema (BaseModel ):
42+ class BaseConfigSchema (BackendAISchema ):
4443 @staticmethod
4544 def snake_to_kebab_case (string : str ) -> str :
4645 return string .replace ("_" , "-" )
@@ -53,7 +52,7 @@ def snake_to_kebab_case(string: str) -> str:
5352 )
5453
5554
56- class BaseConfigModel (BaseModel ):
55+ class BaseConfigModel (BackendAISchema ):
5756 @staticmethod
5857 def snake_to_kebab_case (string : str ) -> str :
5958 return string .replace ("_" , "-" )
@@ -478,6 +477,14 @@ class ModelDefinition(BaseConfigModel):
478477 description = "List of models in the model definition." ,
479478 )
480479
480+ @override
481+ @classmethod
482+ def build_validation_error (cls , info : SchemaValidationFailureInfo ) -> BackendAIError :
483+ return ModelDefinitionValidationError (
484+ extra_msg = info .summary ,
485+ extra_data = {"errors" : info .errors },
486+ )
487+
481488 def merge (self , override : ModelDefinition ) -> ModelDefinition :
482489 """Merge the given override into this definition, returning a new instance."""
483490 return _merge_definition (self , override )
@@ -532,10 +539,10 @@ class ModelHealthCheckDraft(BaseConfigModel):
532539 initial_delay : float | None = None
533540
534541 def to_resolved (self ) -> ModelHealthCheck :
535- if self .path is None :
536- raise ValueError ("ModelHealthCheck.path is required" )
537542 # Drop unset (None) fields so the strict type's ``Field(default=...)``
538543 # declarations remain the single source of truth for default values.
544+ # Missing required fields (e.g. ``path``) surface as the strict
545+ # type's ``BackendAISchemaValidationFailed`` via ``model_validate``.
539546 return ModelHealthCheck .model_validate (self .model_dump (exclude_none = True ))
540547
541548
@@ -552,16 +559,15 @@ def _coerce_start_command(cls, value: Any) -> Any:
552559 return _normalize_start_command (value )
553560
554561 def to_resolved (self ) -> ModelServiceConfig :
555- if self .port is None :
556- raise ValueError ("ModelServiceConfig.port is required" )
557562 # Drop unset (None) scalars so the strict type's ``Field(default=...)``
558563 # declarations remain the single source of truth for default values;
559564 # resolve the nested ``health_check`` draft explicitly so its own
560- # required-field check (``path``) fires with a clear error message.
561- return ModelServiceConfig (
562- ** self .model_dump (exclude_none = True , exclude = {"health_check" }),
563- health_check = self .health_check .to_resolved () if self .health_check else None ,
564- )
565+ # required-field check (``path``) fires through its own
566+ # ``model_validate``. Missing required fields (e.g. ``port``)
567+ # surface as ``BackendAISchemaValidationFailed``.
568+ payload = self .model_dump (exclude_none = True , exclude = {"health_check" })
569+ payload ["health_check" ] = self .health_check .to_resolved () if self .health_check else None
570+ return ModelServiceConfig .model_validate (payload )
565571
566572
567573class ModelConfigDraft (BaseConfigModel ):
@@ -571,25 +577,18 @@ class ModelConfigDraft(BaseConfigModel):
571577 metadata : ModelMetadata | None = None # ModelMetadata is already all-Optional.
572578
573579 def to_resolved (self ) -> ModelConfig :
574- if self .name is None :
575- raise ValueError ("ModelConfig.name is required" )
576- if self .model_path is None :
577- raise ValueError ("ModelConfig.model_path is required" )
578580 service = self .service .to_resolved () if self .service else None
579- if service is not None and service .start_command :
581+ if service is not None and service .start_command and self . model_path is not None :
580582 # ``{model_path}`` placeholders in the variant baseline's
581583 # ``start_command`` are resolved here, at the same moment the
582584 # draft becomes a strict ``ModelConfig`` and ``model_path`` is
583585 # finalized. Placeholders therefore never propagate downstream.
584586 service .start_command = [
585587 token .replace ("{model_path}" , self .model_path ) for token in service .start_command
586588 ]
587- return ModelConfig (
588- name = self .name ,
589- model_path = self .model_path ,
590- service = service ,
591- metadata = self .metadata ,
592- )
589+ payload = self .model_dump (exclude_none = True , exclude = {"service" })
590+ payload ["service" ] = service
591+ return ModelConfig .model_validate (payload )
593592
594593
595594def _merge_health_check_draft (
@@ -665,6 +664,14 @@ class ModelDefinitionDraft(BaseConfigModel):
665664
666665 models : list [ModelConfigDraft ] | None = None
667666
667+ @override
668+ @classmethod
669+ def build_validation_error (cls , info : SchemaValidationFailureInfo ) -> BackendAIError :
670+ return ModelDefinitionValidationError (
671+ extra_msg = info .summary ,
672+ extra_data = {"errors" : info .errors },
673+ )
674+
668675 def merge (self , override : ModelDefinitionDraft ) -> ModelDefinitionDraft :
669676 """Merge ``override`` over ``self`` and return a new draft.
670677
@@ -689,13 +696,9 @@ def merge(self, override: ModelDefinitionDraft) -> ModelDefinitionDraft:
689696 return ModelDefinitionDraft .model_construct (models = merged )
690697
691698 def to_resolved (self ) -> ModelDefinition :
692- """Build the strict ``ModelDefinition`` from this draft.
693-
694- Each child draft is converted via its own ``to_resolved`` and the
695- strict type's constructor performs Pydantic validation; missing
696- required fields propagate as ``pydantic.ValidationError``.
697- """
698- return ModelDefinition (models = [m .to_resolved () for m in (self .models or [])])
699+ return ModelDefinition .model_validate ({
700+ "models" : [m .to_resolved () for m in (self .models or [])],
701+ })
699702
700703
701704def find_config_file (daemon_name : str ) -> Path :
0 commit comments