|
@@ -1,23 +1,9 @@
|
|
|
-"""Common objects and base classes in the CI hierarchy"""
|
|
|
|
|
|
|
+"""Common objects in the CI hierarchy"""
|
|
|
|
|
|
|
|
-import abc
|
|
|
|
|
-import dataclasses
|
|
|
|
|
-import logging
|
|
|
|
|
-import os
|
|
|
|
|
-
|
|
|
|
|
-from contextlib import contextmanager
|
|
|
|
|
-from functools import reduce
|
|
|
|
|
-from types import SimpleNamespace, NoneType
|
|
|
|
|
-from typing import Annotated, Any, Callable, Iterator, Literal, Self, final, overload
|
|
|
|
|
|
|
+from dataclasses import dataclass
|
|
|
|
|
+from typing import Annotated, Self, final
|
|
|
|
|
|
|
|
from pydantic import BaseModel, Field
|
|
from pydantic import BaseModel, Field
|
|
|
-from pydantic_core import PydanticUndefined
|
|
|
|
|
-
|
|
|
|
|
-from cipy.status import Status
|
|
|
|
|
-
|
|
|
|
|
-type Scalar = bool | int | float | str
|
|
|
|
|
-type Computed = Ref | Factory
|
|
|
|
|
-type Value = Scalar | Computed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Inputs(BaseModel):
|
|
class Inputs(BaseModel):
|
|
@@ -33,7 +19,7 @@ class Outputs(BaseModel):
|
|
|
return self.model_validate(self, extra="forbid")
|
|
return self.model_validate(self, extra="forbid")
|
|
|
|
|
|
|
|
|
|
|
|
|
-@dataclasses.dataclass
|
|
|
|
|
|
|
+@dataclass
|
|
|
class Ref:
|
|
class Ref:
|
|
|
"""Annotation class describing a reference into Context or another place"""
|
|
"""Annotation class describing a reference into Context or another place"""
|
|
|
|
|
|
|
@@ -43,131 +29,3 @@ class Ref:
|
|
|
self.path = pathstr.split(".")
|
|
self.path = pathstr.split(".")
|
|
|
if not self.path:
|
|
if not self.path:
|
|
|
raise ValueError("References must be of the form A.B.C etc.")
|
|
raise ValueError("References must be of the form A.B.C etc.")
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-@dataclasses.dataclass
|
|
|
|
|
-class Factory:
|
|
|
|
|
- """Annotation class describing a non-trivial synthesized property"""
|
|
|
|
|
-
|
|
|
|
|
- __call__: Callable[[Context], Scalar | None]
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-class Results(SimpleNamespace):
|
|
|
|
|
- """
|
|
|
|
|
- Holder object for tracking the result of an action for Composite/Workflow actions
|
|
|
|
|
- """
|
|
|
|
|
-
|
|
|
|
|
- @dataclasses.dataclass
|
|
|
|
|
- class Item:
|
|
|
|
|
- """Result of a single action that needs to be tracked"""
|
|
|
|
|
-
|
|
|
|
|
- conclusion: Status = Status.NOT_RUN
|
|
|
|
|
- outputs: Outputs = dataclasses.field(default_factory=Outputs)
|
|
|
|
|
-
|
|
|
|
|
- def __contains__(self, subscript: str) -> bool:
|
|
|
|
|
- return hasattr(self, subscript)
|
|
|
|
|
-
|
|
|
|
|
- def __setitem__(self, subscript: str, value: Results.Item) -> None:
|
|
|
|
|
- if subscript:
|
|
|
|
|
- self.__setattr__(subscript, value)
|
|
|
|
|
-
|
|
|
|
|
- def __getitem__(self, subscript: str) -> Results.Item:
|
|
|
|
|
- return self.__getattribute__(subscript)
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-class Context(SimpleNamespace):
|
|
|
|
|
- """Wrapper class for the context of the CI runtime"""
|
|
|
|
|
-
|
|
|
|
|
- def __call__(self, arg: Value | None) -> Scalar | None:
|
|
|
|
|
- """Accessor for context state with a dot-separated path"""
|
|
|
|
|
- if arg is None:
|
|
|
|
|
- return None
|
|
|
|
|
-
|
|
|
|
|
- if isinstance(arg, Factory):
|
|
|
|
|
- return arg(self)
|
|
|
|
|
-
|
|
|
|
|
- if not isinstance(arg, Ref):
|
|
|
|
|
- return arg
|
|
|
|
|
-
|
|
|
|
|
- if arg.path[0] == "env":
|
|
|
|
|
- assert len(arg.path) == 2
|
|
|
|
|
- return os.environ.get(arg.path[1])
|
|
|
|
|
-
|
|
|
|
|
- return reduce( # type: ignore[return-value]
|
|
|
|
|
- lambda o, a: o[a] if isinstance(o, dict) else getattr(o, a), arg.path, self
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- @overload
|
|
|
|
|
- def fabricate(
|
|
|
|
|
- self,
|
|
|
|
|
- state: BaseModel,
|
|
|
|
|
- attr: Literal["inputs"],
|
|
|
|
|
- extra: dict[str, Any] | None = None,
|
|
|
|
|
- ) -> Inputs: ...
|
|
|
|
|
-
|
|
|
|
|
- @overload
|
|
|
|
|
- def fabricate(
|
|
|
|
|
- self,
|
|
|
|
|
- state: BaseModel,
|
|
|
|
|
- attr: Literal["outputs"],
|
|
|
|
|
- extra: dict[str, Any] | None = None,
|
|
|
|
|
- ) -> Outputs: ...
|
|
|
|
|
-
|
|
|
|
|
- def fabricate(
|
|
|
|
|
- self,
|
|
|
|
|
- state: BaseModel,
|
|
|
|
|
- attr: Literal["inputs"] | Literal["outputs"],
|
|
|
|
|
- extra: dict[str, Ref | Factory] | None = None,
|
|
|
|
|
- ) -> Inputs | Outputs:
|
|
|
|
|
- """Fabricate and validate an Inputs or Outputs object"""
|
|
|
|
|
- if extra is None:
|
|
|
|
|
- extra = {}
|
|
|
|
|
-
|
|
|
|
|
- annotation = state.__pydantic_fields__[attr].annotation
|
|
|
|
|
- assert annotation is not None
|
|
|
|
|
-
|
|
|
|
|
- fields: dict[str, Any] = {}
|
|
|
|
|
- if (model := getattr(state, attr)) is not None:
|
|
|
|
|
- annotation = model.__class__
|
|
|
|
|
- fields = vars(model)
|
|
|
|
|
-
|
|
|
|
|
- for name, fld in annotation.__pydantic_fields__.items():
|
|
|
|
|
- if name in extra:
|
|
|
|
|
- fields[name] = self(extra[name])
|
|
|
|
|
- elif fld.default is not PydanticUndefined:
|
|
|
|
|
- fields[name] = self(fld.default)
|
|
|
|
|
-
|
|
|
|
|
- model = annotation(**fields)
|
|
|
|
|
- setattr(state, attr, model)
|
|
|
|
|
- return model
|
|
|
|
|
-
|
|
|
|
|
- @contextmanager
|
|
|
|
|
- def extend(self, **kwargs: Any) -> Iterator[Context]:
|
|
|
|
|
- """Create a new context that inherits an extra property"""
|
|
|
|
|
- yield Context(**vars(self), **kwargs)
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-class Action(BaseModel, abc.ABC):
|
|
|
|
|
- """Base class describing all individual steps in a CI workflow"""
|
|
|
|
|
-
|
|
|
|
|
- name: str
|
|
|
|
|
- id: str = Field(default="", exclude_if=lambda v: not v)
|
|
|
|
|
- inputs: Inputs = Inputs()
|
|
|
|
|
- outputs: Outputs = Outputs()
|
|
|
|
|
-
|
|
|
|
|
- @property
|
|
|
|
|
- def logger(self) -> logging.Logger:
|
|
|
|
|
- """Get this class's logger"""
|
|
|
|
|
- return logging.getLogger(self.__class__.__name__)
|
|
|
|
|
-
|
|
|
|
|
- # pylint: disable=unused-argument
|
|
|
|
|
- def enabled(self, status: Status, context: Context) -> bool:
|
|
|
|
|
- """Should this action even be run?"""
|
|
|
|
|
- return status.value <= Status.SUCCESS.value
|
|
|
|
|
-
|
|
|
|
|
- @abc.abstractmethod
|
|
|
|
|
- def run(self, context: Context) -> Status:
|
|
|
|
|
- """Run this action, updating outputs internally or via the cipy.runner.run wrapper"""
|
|
|
|
|
-
|
|
|
|
|
- def cleanup(self, context: Context) -> None:
|
|
|
|
|
- """Optionally clean up after ourselves"""
|
|
|