Source code for ax.storage.sqa_store.validation

#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

from logging import Logger
from typing import Any, Callable, List, TypeVar

from ax.storage.sqa_store.db import SQABase
from ax.storage.sqa_store.reduced_state import GR_LARGE_MODEL_ATTRS
from ax.storage.sqa_store.sqa_classes import (
    ONLY_ONE_FIELDS,
    ONLY_ONE_METRIC_FIELDS,
    SQAMetric,
    SQAParameter,
    SQAParameterConstraint,
    SQARunner,
)
from ax.utils.common.logger import get_logger
from sqlalchemy import event
from sqlalchemy.engine import Connection
from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.orm.base import NO_VALUE
from sqlalchemy.orm.mapper import Mapper

T = TypeVar("T")


logger: Logger = get_logger(__name__)


[docs]def listens_for_multiple( targets: List[InstrumentedAttribute], identifier: str, *args: Any, **kwargs: Any # pyre-fixme[24]: Generic type `Callable` expects 2 type parameters. ) -> Callable: """Analogue of SQLAlchemy `listen_for`, but applies the same listening handler function to multiple instrumented attributes. """ # pyre-fixme[3]: Return type must be annotated. # pyre-fixme[24]: Generic type `Callable` expects 2 type parameters. def wrapper(fn: Callable): for target in targets: event.listen(target, identifier, fn, *args, **kwargs) return fn return wrapper
# pyre-fixme[3]: Return annotation cannot be `Any`.
[docs]def consistency_exactly_one(instance: SQABase, exactly_one_fields: List[str]) -> Any: """Ensure that exactly one of `exactly_one_fields` has a value set.""" values = [getattr(instance, field) is not None for field in exactly_one_fields] if sum(values) != 1: raise ValueError( f"{instance.__class__.__name__} must have exactly one of the following " f"fields set: {', '.join(exactly_one_fields)}." )
[docs]@listens_for_multiple( targets=GR_LARGE_MODEL_ATTRS, identifier="set", # `retval=True` instruct the operation ('set' on attributes in `targets`) to use # the return value of decorated function to set the attribute. retval=True, # `propagate=True` ensures that targets with subclasses of SQA classes used by # default Ax OSS encoder inherit the event listeners. propagate=True, ) def do_not_set_existing_value_to_null( instance: SQABase, new_value: T, old_value: T, initiator_event: event.Events ) -> T: no_value = [None, NO_VALUE] if new_value in no_value and old_value not in no_value: logger.debug( f"New value for attribute is `None` or has no value, but old value " f"was set, so keeping the old value ({old_value})." ) return old_value return new_value
@event.listens_for( SQAParameter, "before_insert", ) @event.listens_for( SQAParameter, "before_update", ) # pyre-fixme[11]: Annotation `Mapper` is not defined as a type. def validate_parameter(mapper: Mapper, connection: Connection, target: SQABase) -> None: consistency_exactly_one(target, ONLY_ONE_FIELDS) @event.listens_for(SQAParameterConstraint, "before_insert") @event.listens_for(SQAParameterConstraint, "before_update") def validate_parameter_constraint( mapper: Mapper, connection: Connection, target: SQABase ) -> None: consistency_exactly_one(target, ONLY_ONE_FIELDS) @event.listens_for(SQAMetric, "before_insert") @event.listens_for(SQAMetric, "before_update") def validate_metric(mapper: Mapper, connection: Connection, target: SQABase) -> None: consistency_exactly_one(target, ONLY_ONE_FIELDS + ONLY_ONE_METRIC_FIELDS) @event.listens_for(SQARunner, "before_insert") @event.listens_for(SQARunner, "before_update") def validate_runner(mapper: Mapper, connection: Connection, target: SQABase) -> None: consistency_exactly_one(target, ["experiment_id", "trial_id"])