2828from .pipeline_dehydrator import DehydrateChoice , PipelineDehydrator
2929from .pipeline_hydrator import HydrationError , PipelineHydrator
3030from .pipeline_run_details import PipelineRunDetails
31+ from .pipelines import collect_pipeline_spec_errors
3132from .pipeline_run_search import PipelineRunSearch
3233from .utils import dump_yaml
3334
@@ -378,6 +379,26 @@ def prepare_run_arguments(
378379 """Hook for TD JOB_CONFIG time input / scheduled runtime behavior."""
379380 return run_args
380381
382+ def validate_pipeline_for_run (
383+ self ,
384+ pipeline_spec : dict [str , Any ],
385+ * ,
386+ pipeline_path : str | Path | None ,
387+ effective_path : str | Path | None ,
388+ skip_validation : bool ,
389+ ) -> list [str ]:
390+ """Return submit-time validation errors for a prepared pipeline spec.
391+
392+ The OSS default enforces the same local authoring validator used by
393+ ``tangle pipeline validate``. Downstreams can override or extend this
394+ hook with stricter schema/input validators.
395+ """
396+
397+ del pipeline_path , effective_path
398+ if skip_validation :
399+ return []
400+ return collect_pipeline_spec_errors (pipeline_spec )
401+
381402 def transform_run_name (
382403 self ,
383404 run_name : str ,
@@ -834,6 +855,11 @@ def _accepts_client_keyword(method: Any) -> bool:
834855 for parameter in parameters .values ()
835856 )
836857
858+ @staticmethod
859+ def _raise_pipeline_validation_error (validation_errors : list [str ]) -> None :
860+ if validation_errors :
861+ raise PipelineRunError ("Pipeline validation failed:\n - " + "\n - " .join (validation_errors ))
862+
837863 def load_pipeline_for_submit (
838864 self ,
839865 pipeline_path : str | Path ,
@@ -909,13 +935,15 @@ def prepare_submit_payload_from_spec(
909935 pipeline_path : str | Path | None = None ,
910936 run_as : str | None = None ,
911937 hydrate : bool = True ,
938+ skip_validation : bool = False ,
912939 ) -> PipelineSubmitPayload :
913940 """Prepare the generic submit payload from a pipeline spec.
914941
915942 The order here is the submit-body contract shared by OSS and TD:
916943 prepare the spec, prepare runtime arguments, expand run-name templates,
917- convert/sanitize the payload, then merge downstream/default annotations
918- before caller-supplied annotations override them.
944+ validate the prepared authoring spec, convert/sanitize the payload, then
945+ merge downstream/default annotations before caller-supplied annotations
946+ override them.
919947 """
920948
921949 prepared_spec = self .prepare_pipeline_spec_for_submit (
@@ -926,6 +954,13 @@ def prepare_submit_payload_from_spec(
926954 )
927955 prepared_run_args = self .hooks .prepare_run_arguments (prepared_spec , run_args )
928956 prepared_spec = self .apply_run_name_template (prepared_spec , prepared_run_args )
957+ validation_errors = self .hooks .validate_pipeline_for_run (
958+ prepared_spec ,
959+ pipeline_path = pipeline_path ,
960+ effective_path = None ,
961+ skip_validation = skip_validation ,
962+ )
963+ self ._raise_pipeline_validation_error (validation_errors )
929964 payload = self .convert_yaml_to_payload (copy .deepcopy (prepared_spec ), prepared_run_args )
930965 payload = self .sanitize_submit_payload (payload )
931966 root_task = payload ["root_task" ]
@@ -961,6 +996,7 @@ def build_submit_body_from_spec(
961996 pipeline_path : str | Path | None = None ,
962997 run_as : str | None = None ,
963998 hydrate : bool = True ,
999+ skip_validation : bool = False ,
9641000 ) -> dict [str , Any ]:
9651001 """Build a submit body from an already-prepared pipeline spec."""
9661002
@@ -971,6 +1007,7 @@ def build_submit_body_from_spec(
9711007 pipeline_path = pipeline_path ,
9721008 run_as = run_as ,
9731009 hydrate = hydrate ,
1010+ skip_validation = skip_validation ,
9741011 ).to_body ()
9751012
9761013 def prepare_submit_payload (
@@ -982,6 +1019,7 @@ def prepare_submit_payload(
9821019 hydrate : bool = True ,
9831020 run_as : str | None = None ,
9841021 resolution_overrides : dict [str , Any ] | None = None ,
1022+ skip_validation : bool = False ,
9851023 ) -> PipelineSubmitPayload :
9861024 pipeline_spec = self .load_pipeline_for_submit (
9871025 pipeline_path ,
@@ -995,6 +1033,7 @@ def prepare_submit_payload(
9951033 pipeline_path = pipeline_path ,
9961034 run_as = run_as ,
9971035 hydrate = hydrate ,
1036+ skip_validation = skip_validation ,
9981037 )
9991038
10001039 def build_submit_body (
@@ -1006,6 +1045,7 @@ def build_submit_body(
10061045 hydrate : bool = True ,
10071046 run_as : str | None = None ,
10081047 resolution_overrides : dict [str , Any ] | None = None ,
1048+ skip_validation : bool = False ,
10091049 ) -> dict [str , Any ]:
10101050 return self .prepare_submit_payload (
10111051 pipeline_path ,
@@ -1014,6 +1054,7 @@ def build_submit_body(
10141054 hydrate = hydrate ,
10151055 run_as = run_as ,
10161056 resolution_overrides = resolution_overrides ,
1057+ skip_validation = skip_validation ,
10171058 ).to_body ()
10181059
10191060 @staticmethod
@@ -1117,6 +1158,7 @@ def submit_pipeline_spec(
11171158 run_as : str | None = None ,
11181159 hydrate : bool = True ,
11191160 attempt : int = 1 ,
1161+ skip_validation : bool = False ,
11201162 ) -> dict [str , Any ]:
11211163 payload = self .prepare_submit_payload_from_spec (
11221164 pipeline_spec ,
@@ -1125,6 +1167,7 @@ def submit_pipeline_spec(
11251167 pipeline_path = pipeline_path ,
11261168 run_as = run_as ,
11271169 hydrate = hydrate ,
1170+ skip_validation = skip_validation ,
11281171 )
11291172 return self .submit_prepared_payload (payload , pipeline_path = pipeline_path , attempt = attempt )
11301173
@@ -1138,6 +1181,7 @@ def submit_pipeline(
11381181 run_as : str | None = None ,
11391182 resolution_overrides : dict [str , Any ] | None = None ,
11401183 attempt : int = 1 ,
1184+ skip_validation : bool = False ,
11411185 ) -> dict [str , Any ]:
11421186 payload = self .prepare_submit_payload (
11431187 pipeline_path ,
@@ -1146,6 +1190,7 @@ def submit_pipeline(
11461190 hydrate = hydrate ,
11471191 run_as = run_as ,
11481192 resolution_overrides = resolution_overrides ,
1193+ skip_validation = skip_validation ,
11491194 )
11501195 return self .submit_prepared_payload (payload , pipeline_path = pipeline_path , attempt = attempt )
11511196
@@ -1909,6 +1954,7 @@ def run_pipeline_spec(
19091954 exit_on_first_failure : bool = False ,
19101955 metadata : dict [str , Any ] | None = None ,
19111956 submit_recovery_attempts : int = _DEFAULT_SUBMIT_RECOVERY_ATTEMPTS ,
1957+ skip_validation : bool = False ,
19121958 ) -> dict [str , Any ]:
19131959 """Submit/wait/retry an already hydrated/validated in-memory spec."""
19141960
@@ -1924,6 +1970,7 @@ def body_factory(
19241970 pipeline_path = pipeline_path ,
19251971 run_as = run_as ,
19261972 hydrate = hydrate ,
1973+ skip_validation = skip_validation ,
19271974 ).to_body ()
19281975
19291976 return self ._run_body_factory (
@@ -1960,6 +2007,7 @@ def run_pipeline(
19602007 exit_on_first_failure : bool = False ,
19612008 metadata : dict [str , Any ] | None = None ,
19622009 submit_recovery_attempts : int = _DEFAULT_SUBMIT_RECOVERY_ATTEMPTS ,
2010+ skip_validation : bool = False ,
19632011 ) -> dict [str , Any ]:
19642012 """Submit (and optionally wait for) a pipeline with lifecycle hooks.
19652013
@@ -1980,6 +2028,7 @@ def body_factory(
19802028 hydrate = hydrate ,
19812029 run_as = run_as ,
19822030 resolution_overrides = resolution_overrides ,
2031+ skip_validation = skip_validation ,
19832032 ).to_body ()
19842033
19852034 return self ._run_body_factory (
0 commit comments