_validators.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. from __future__ import annotations
  2. from enum import Enum
  3. from typing import Annotated, Any, TypeVar
  4. from pydantic import BeforeValidator, Json, PlainSerializer
  5. from pydantic_core import PydanticUseDefault
  6. from wandb._pydantic import to_json
  7. from ._filters import And, MongoLikeFilter, Or
  8. from ._filters.filterutils import simplify_expr
  9. T = TypeVar("T")
  10. def ensure_json(v: Any) -> Any:
  11. """In case the incoming value isn't serialized JSON, reserialize it.
  12. This lets us use `Json[...]` fields with values that are already deserialized.
  13. """
  14. # NOTE: Assumes that the deserialized type is not itself a string.
  15. # Revisit this if we need to support deserialized types that are str/bytes.
  16. return v if isinstance(v, (str, bytes)) else to_json(v)
  17. JsonEncoded = Annotated[Json[T], BeforeValidator(ensure_json), PlainSerializer(to_json)]
  18. """A Pydantic type that's always serialized to a JSON string.
  19. Unlike `pydantic.Json[T]`, this is more lenient on validation and instantiation.
  20. It doesn't strictly require the incoming value to be an encoded JSON string, and
  21. accepts values that may _already_ be deserialized from JSON (e.g. a dict).
  22. """
  23. class LenientStrEnum(str, Enum):
  24. """A string enum allowing for case-insensitive lookups by value.
  25. May include other internal customizations if needed.
  26. Note: This is a bespoke, internal implementation and NOT intended as a
  27. backport of `enum.StrEnum` from Python 3.11+.
  28. """
  29. def __repr__(self) -> str:
  30. return self.name
  31. @classmethod
  32. def _missing_(cls, value: object) -> Any:
  33. # Accept case-insensitive enum values
  34. if isinstance(value, str):
  35. v = value.lower()
  36. return next((e for e in cls if e.value.lower() == v), None)
  37. return None
  38. def default_if_none(v: Any) -> Any:
  39. """A "before"-mode field validator that coerces `None` to the field default.
  40. See: https://docs.pydantic.dev/2.11/api/pydantic_core/#pydantic_core.PydanticUseDefault
  41. """
  42. if v is None:
  43. raise PydanticUseDefault
  44. return v
  45. def upper_if_str(v: Any) -> Any:
  46. return v.strip().upper() if isinstance(v, str) else v
  47. # ----------------------------------------------------------------------------
  48. def parse_scope(v: Any) -> Any:
  49. """Convert eligible objects (including wandb types) to an automation scope."""
  50. from wandb.apis.public import ArtifactCollection, Project
  51. from .scopes import ProjectScope, _ArtifactPortfolioScope, _ArtifactSequenceScope
  52. if isinstance(v, Project):
  53. return ProjectScope.model_validate(v)
  54. if isinstance(v, ArtifactCollection):
  55. typ = _ArtifactSequenceScope if v.is_sequence() else _ArtifactPortfolioScope
  56. return typ.model_validate(v)
  57. return v
  58. def parse_saved_action(v: Any) -> Any:
  59. """If necessary (and possible), convert the object to a saved action."""
  60. from .actions import (
  61. DoNothing,
  62. SavedNoOpAction,
  63. SavedNotificationAction,
  64. SavedWebhookAction,
  65. SendNotification,
  66. SendWebhook,
  67. )
  68. if isinstance(v, SendNotification):
  69. return SavedNotificationAction(
  70. integration={"id": v.integration_id}, **v.model_dump()
  71. )
  72. if isinstance(v, SendWebhook):
  73. return SavedWebhookAction(
  74. integration={"id": v.integration_id}, **v.model_dump()
  75. )
  76. if isinstance(v, DoNothing):
  77. return SavedNoOpAction(**v.model_dump())
  78. return v
  79. def parse_input_action(v: Any) -> Any:
  80. """If necessary (and possible), convert the object to an input action."""
  81. from .actions import (
  82. DoNothing,
  83. SavedNoOpAction,
  84. SavedNotificationAction,
  85. SavedWebhookAction,
  86. SendNotification,
  87. SendWebhook,
  88. )
  89. if isinstance(v, SavedNotificationAction):
  90. return SendNotification(integration_id=v.integration.id, **v.model_dump())
  91. if isinstance(v, SavedWebhookAction):
  92. return SendWebhook(integration_id=v.integration.id, **v.model_dump())
  93. if isinstance(v, SavedNoOpAction):
  94. return DoNothing(**v.model_dump())
  95. return v
  96. # ----------------------------------------------------------------------------
  97. def wrap_run_event_run_filter(f: MongoLikeFilter) -> MongoLikeFilter:
  98. """Wrap a run filter in an `And` operator if it's not already.
  99. This is a necessary constraint imposed elsewhere by backend/frontend code.
  100. """
  101. return And.wrap(simplify_expr(f)) # simplify/flatten first if needed
  102. def wrap_mutation_event_filter(f: MongoLikeFilter) -> MongoLikeFilter:
  103. """Wrap filters as `{"$or": [{"$and": [<original_filter>]}]}`.
  104. This awkward format is necessary because the frontend expects it.
  105. """
  106. return Or.wrap(And.wrap(simplify_expr(f))) # simplify/flatten first if needed