def build_component_spec_from_structure( component_spec: structures.ComponentSpec, ) -> pipeline_spec_pb2.ComponentSpec: """Builds an IR ComponentSpec instance from structures.ComponentSpec. Args: component_spec: The structure component spec. Returns: An instance of IR ComponentSpec. """ result = pipeline_spec_pb2.ComponentSpec() result.executor_label = dsl_utils.sanitize_executor_label(component_spec.name) for input_spec in component_spec.inputs or []: if type_utils.is_parameter_type(input_spec.type): result.input_definitions.parameters[ input_spec.name].type = type_utils.get_parameter_type(input_spec.type) else: result.input_definitions.artifacts[ input_spec.name].artifact_type.instance_schema = ( type_utils.get_artifact_type_schema(input_spec.type)) for output_spec in component_spec.outputs or []: if type_utils.is_parameter_type(output_spec.type): result.output_definitions.parameters[ output_spec.name].type = type_utils.get_parameter_type( output_spec.type) else: result.output_definitions.artifacts[ output_spec.name].artifact_type.instance_schema = ( type_utils.get_artifact_type_schema(output_spec.type)) return result
def verify_type_compatibility(given_type: TypeSpecType, expected_type: TypeSpecType, error_message_prefix: str = ""): """verify_type_compatibility verifies that the given argument type is compatible with the expected input type. Args: given_type (str/dict): The type of the argument passed to the input expected_type (str/dict): The declared type of the input """ # Missing types are treated as being compatible with missing types. if given_type is None or expected_type is None: return True # Generic artifacts resulted from missing type or explicit "Artifact" type can # be passed to inputs expecting any artifact types. # However, generic artifacts resulted from arbitrary unknown types do not have # such "compatible" feature. if not type_utils.is_parameter_type(str(expected_type)) and ( given_type is None or str(given_type).lower() == "artifact"): return True types_are_compatible = check_types(given_type, expected_type) if not types_are_compatible: error_text = error_message_prefix + ( 'Argument type "{}" is incompatible with the input type "{}"').format( str(given_type), str(expected_type)) import kfp if kfp.TYPE_CHECK: raise InconsistentTypeException(error_text) else: warnings.warn(InconsistentTypeWarning(error_text)) return types_are_compatible
def _resolve_value_or_reference( self, value_or_reference: Union[str, dsl.PipelineParam]) -> str: """_resolve_value_or_reference resolves values and PipelineParams. The values and PipelineParams could be task parameters or input parameters. Args: value_or_reference: value or reference to be resolved. It could be basic python types or PipelineParam """ if isinstance(value_or_reference, dsl.PipelineParam): input_name = dsl_component_spec.additional_input_name_for_pipelineparam( value_or_reference) if type_utils.is_parameter_type(value_or_reference.param_type): return "inputs.parameters['{input_name}'].{value_field}".format( input_name=input_name, value_field=type_utils.get_parameter_type_field_name( value_or_reference.param_type)) else: raise NotImplementedError( 'Use artifact as dsl.Condition operand is not implemented yet.') else: if isinstance(value_or_reference, str): return "'{}'".format(value_or_reference) else: return str(value_or_reference)
def build_component_inputs_spec( component_spec: pipeline_spec_pb2.ComponentSpec, pipeline_params: List[_pipeline_param.PipelineParam], is_root_component: bool, ) -> None: """Builds component inputs spec from pipeline params. Args: component_spec: The component spec to fill in its inputs spec. pipeline_params: The list of pipeline params. is_root_component: Whether the component is the root. """ for param in pipeline_params: input_name = (param.full_name if is_root_component else additional_input_name_for_pipelineparam(param)) if type_utils.is_parameter_type(param.param_type): component_spec.input_definitions.parameters[ input_name].type = type_utils.get_parameter_type( param.param_type) elif input_name not in getattr(component_spec.input_definitions, 'parameters', []): component_spec.input_definitions.artifacts[ input_name].artifact_type.CopyFrom( type_utils.get_artifact_type_schema_message( param.param_type))
def _input_artifact_uri_placeholder(input_key: str) -> str: if kfp.COMPILING_FOR_V2 and type_utils.is_parameter_type( inputs_dict[input_key].type): raise TypeError('Input "{}" with type "{}" cannot be paired with ' 'InputUriPlaceholder.'.format( input_key, inputs_dict[input_key].type)) else: return "{{{{$.inputs.artifacts['{}'].uri}}}}".format(input_key)
def _input_parameter_placeholder(input_key: str) -> str: if kfp.COMPILING_FOR_V2 and not type_utils.is_parameter_type( inputs_dict[input_key].type): raise TypeError('Input "{}" with type "{}" cannot be paired with ' 'InputValuePlaceholder.'.format( input_key, inputs_dict[input_key].type)) else: return "{{{{$.inputs.parameters['{}']}}}}".format(input_key)
def _output_artifact_uri_placeholder(output_key: str) -> str: if is_compiling_for_v2 and type_utils.is_parameter_type( outputs_dict[output_key].type): raise TypeError('Output "{}" with type "{}" cannot be paired with ' 'OutputUriPlaceholder.'.format( output_key, outputs_dict[output_key].type)) else: return "{{{{$.outputs.artifacts['{}'].uri}}}}".format(output_key)
def _input_artifact_path_placeholder(input_key: str) -> str: if is_compiling_for_v2 and type_utils.is_parameter_type( inputs_dict[input_key].type): raise TypeError('Input "{}" with type "{}" cannot be paired with ' 'InputPathPlaceholder.'.format( input_key, inputs_dict[input_key].type)) else: return "{{{{$.inputs.artifacts['{}'].path}}}}".format(input_key)
def _output_artifact_uri_placeholder(output_key: str) -> str: if kfp.COMPILING_FOR_V2 and type_utils.is_parameter_type( outputs_dict[output_key].type): raise TypeError('Output "{}" with type "{}" cannot be paired with ' 'OutputUriPlaceholder.'.format( output_key, outputs_dict[output_key].type)) else: return _generate_output_uri_placeholder(output_key)
def build_component_spec_from_structure( component_spec: structures.ComponentSpec, executor_label: str, actual_inputs: List[str], ) -> pipeline_spec_pb2.ComponentSpec: """Builds an IR ComponentSpec instance from structures.ComponentSpec. Args: component_spec: The structure component spec. executor_label: The executor label. actual_inputs: The actual arugments passed to the task. This is used as a short term workaround to support optional inputs in component spec IR. Returns: An instance of IR ComponentSpec. """ result = pipeline_spec_pb2.ComponentSpec() result.executor_label = executor_label for input_spec in component_spec.inputs or []: # skip inputs not present if input_spec.name not in actual_inputs: continue if type_utils.is_parameter_type(input_spec.type): result.input_definitions.parameters[ input_spec.name].type = type_utils.get_parameter_type( input_spec.type) else: result.input_definitions.artifacts[ input_spec.name].artifact_type.CopyFrom( type_utils.get_artifact_type_schema_message( input_spec.type)) for output_spec in component_spec.outputs or []: if type_utils.is_parameter_type(output_spec.type): result.output_definitions.parameters[ output_spec.name].type = type_utils.get_parameter_type( output_spec.type) else: result.output_definitions.artifacts[ output_spec.name].artifact_type.CopyFrom( type_utils.get_artifact_type_schema_message( output_spec.type)) return result
def build_task_inputs_spec( task_spec: pipeline_spec_pb2.PipelineTaskSpec, pipeline_params: List[_pipeline_param.PipelineParam], tasks_in_current_dag: List[str], is_parent_component_root: bool, ) -> None: """Builds task inputs spec from pipeline params. Args: task_spec: The task spec to fill in its inputs spec. pipeline_params: The list of pipeline params. tasks_in_current_dag: The list of tasks names for tasks in the same dag. is_parent_component_root: Whether the task is in the root component. """ for param in pipeline_params or []: param_name, subvar_name = _exclude_loop_arguments_variables(param) input_name = additional_input_name_for_pipelineparam(param.full_name) if subvar_name: task_spec.inputs.parameters[ input_name].parameter_expression_selector = ( 'parseJson(string_value)["{}"]'.format(subvar_name)) if type_utils.is_parameter_type(param.param_type): if param.op_name and dsl_utils.sanitize_task_name( param.op_name) in tasks_in_current_dag: task_spec.inputs.parameters[ input_name].task_output_parameter.producer_task = ( dsl_utils.sanitize_task_name(param.op_name)) task_spec.inputs.parameters[ input_name].task_output_parameter.output_parameter_key = ( param.name) else: task_spec.inputs.parameters[ input_name].component_input_parameter = ( param_name if is_parent_component_root else additional_input_name_for_pipelineparam(param_name)) else: if param.op_name and dsl_utils.sanitize_task_name( param.op_name) in tasks_in_current_dag: task_spec.inputs.artifacts[ input_name].task_output_artifact.producer_task = ( dsl_utils.sanitize_task_name(param.op_name)) task_spec.inputs.artifacts[ input_name].task_output_artifact.output_artifact_key = ( param.name) else: task_spec.inputs.artifacts[ input_name].component_input_artifact = ( param_name if is_parent_component_root else input_name)
def _input_artifact_path_placeholder(input_key: str) -> str: if is_compiling_for_v2 and type_utils.is_parameter_type( inputs_dict[input_key].type): raise TypeError('Input "{}" with type "{}" cannot be paired with ' 'InputPathPlaceholder.'.format( input_key, inputs_dict[input_key].type)) elif is_compiling_for_v2 and input_key in importer_specs: raise TypeError( 'Input "{}" with type "{}" is not connected to any upstream output. ' 'However it is used with InputPathPlaceholder. ' 'If you want to import an existing artifact using a system-connected' ' importer node, use InputUriPlaceholder instead. ' 'Or if you just want to pass a string parameter, use string type and' ' InputValuePlaceholder instead.'.format( input_key, inputs_dict[input_key].type)) else: return "{{{{$.inputs.artifacts['{}'].path}}}}".format(input_key)
def build_task_inputs_spec( task_spec: pipeline_spec_pb2.PipelineTaskSpec, pipeline_params: List[_pipeline_param.PipelineParam], tasks_in_current_dag: List[str], is_parent_component_root: bool, ) -> None: """Builds task inputs spec from pipeline params. Args: task_spec: The task spec to fill in its inputs spec. pipeline_params: The list of pipeline params. tasks_in_current_dag: The list of tasks names for tasks in the same dag. is_parent_component_root: Whether the task is in the root component. """ for param in pipeline_params or []: input_name = additional_input_name_for_pipelineparam(param) if type_utils.is_parameter_type(param.param_type): if param.op_name and dsl_utils.sanitize_task_name( param.op_name) in tasks_in_current_dag: task_spec.inputs.parameters[ input_name].task_output_parameter.producer_task = ( dsl_utils.sanitize_task_name(param.op_name)) task_spec.inputs.parameters[ input_name].task_output_parameter.output_parameter_key = ( param.name) else: task_spec.inputs.parameters[ input_name].component_input_parameter = ( param.full_name if is_parent_component_root else input_name) else: if param.op_name and dsl_utils.sanitize_task_name( param.op_name) in tasks_in_current_dag: task_spec.inputs.artifacts[ input_name].task_output_artifact.producer_task = ( dsl_utils.sanitize_task_name(param.op_name)) task_spec.inputs.artifacts[ input_name].task_output_artifact.output_artifact_key = ( param.name) else: task_spec.inputs.artifacts[ input_name].component_input_artifact = ( param.full_name if is_parent_component_root else input_name)
def build_component_outputs_spec( component_spec: pipeline_spec_pb2.ComponentSpec, pipeline_params: List[_pipeline_param.PipelineParam], ) -> None: """Builds component outputs spec from pipeline params. Args: component_spec: The component spec to fill in its outputs spec. pipeline_params: The list of pipeline params. """ for param in pipeline_params or []: output_name = param.full_name if type_utils.is_parameter_type(param.param_type): component_spec.output_definitions.parameters[ output_name].type = type_utils.get_parameter_type(param.param_type) elif output_name not in getattr(component_spec.output_definitions, 'parameters', []): component_spec.output_definitions.artifacts[ output_name].artifact_type.CopyFrom( type_utils.get_artifact_type_schema(param.param_type))
def build_component_inputs_spec( component_spec: pipeline_spec_pb2.ComponentSpec, pipeline_params: List[_pipeline_param.PipelineParam], ) -> None: """Builds component inputs spec from pipeline params. Args: component_spec: The component spec to fill in its inputs spec. pipeline_params: The list of pipeline params. """ for param in pipeline_params: input_name = param.full_name if type_utils.is_parameter_type(param.param_type): component_spec.input_definitions.parameters[ input_name].type = type_utils.get_parameter_type( param.param_type) else: component_spec.input_definitions.artifacts[ input_name].artifact_type.instance_schema = ( type_utils.get_artifact_type_schema(param.param_type))
def _resolve_output_path_placeholder(output_key: str) -> str: if type_utils.is_parameter_type(outputs_dict[output_key].type): return _output_parameter_path_placeholder(output_key) else: return _output_artifact_path_placeholder(output_key)
def _attach_v2_specs( task: _container_op.ContainerOp, component_spec: _structures.ComponentSpec, arguments: Mapping[str, Any], ) -> None: """Attaches v2 specs to a ContainerOp object. Args: task: The ContainerOp object to attach IR specs. component_spec: The component spec object. arguments: The dictionary of component arguments. """ # Attach v2_specs to the ContainerOp object regardless whether the pipeline is # being compiled to v1 (Argo yaml) or v2 (IR json). # However, there're different behaviors for the two cases. Namely, resolved # commands and arguments, error handling, etc. # Regarding the difference in error handling, v2 has a stricter requirement on # input type annotation. For instance, an input without any type annotation is # viewed as an artifact, and if it's paired with InputValuePlaceholder, an # error will be thrown at compile time. However, we cannot raise such an error # in v1, as it wouldn't break existing pipelines. is_compiling_for_v2 = False for frame in inspect.stack(): if '_create_pipeline_v2' in frame: is_compiling_for_v2 = True break def _resolve_commands_and_args_v2( component_spec: _structures.ComponentSpec, arguments: Mapping[str, Any], ) -> _components._ResolvedCommandLineAndPaths: """Resolves the command line argument placeholders for v2 (IR). Args: component_spec: The component spec object. arguments: The dictionary of component arguments. Returns: A named tuple: _components._ResolvedCommandLineAndPaths. """ inputs_dict = { input_spec.name: input_spec for input_spec in component_spec.inputs or [] } outputs_dict = { output_spec.name: output_spec for output_spec in component_spec.outputs or [] } def _input_artifact_uri_placeholder(input_key: str) -> str: if is_compiling_for_v2 and type_utils.is_parameter_type( inputs_dict[input_key].type): raise TypeError( 'Input "{}" with type "{}" cannot be paired with ' 'InputUriPlaceholder.'.format(input_key, inputs_dict[input_key].type)) else: return "{{{{$.inputs.artifacts['{}'].uri}}}}".format(input_key) def _input_artifact_path_placeholder(input_key: str) -> str: if is_compiling_for_v2 and type_utils.is_parameter_type( inputs_dict[input_key].type): raise TypeError( 'Input "{}" with type "{}" cannot be paired with ' 'InputPathPlaceholder.'.format( input_key, inputs_dict[input_key].type)) elif is_compiling_for_v2 and input_key in importer_specs: raise TypeError( 'Input "{}" with type "{}" is not connected to any upstream output. ' 'However it is used with InputPathPlaceholder. ' 'If you want to import an existing artifact using a system-connected' ' importer node, use InputUriPlaceholder instead. ' 'Or if you just want to pass a string parameter, use string type and' ' InputValuePlaceholder instead.'.format( input_key, inputs_dict[input_key].type)) else: return "{{{{$.inputs.artifacts['{}'].path}}}}".format( input_key) def _input_parameter_placeholder(input_key: str) -> str: if is_compiling_for_v2 and not type_utils.is_parameter_type( inputs_dict[input_key].type): raise TypeError( 'Input "{}" with type "{}" cannot be paired with ' 'InputValuePlaceholder.'.format( input_key, inputs_dict[input_key].type)) else: return "{{{{$.inputs.parameters['{}']}}}}".format(input_key) def _output_artifact_uri_placeholder(output_key: str) -> str: if is_compiling_for_v2 and type_utils.is_parameter_type( outputs_dict[output_key].type): raise TypeError( 'Output "{}" with type "{}" cannot be paired with ' 'OutputUriPlaceholder.'.format( output_key, outputs_dict[output_key].type)) else: return "{{{{$.outputs.artifacts['{}'].uri}}}}".format( output_key) def _output_artifact_path_placeholder(output_key: str) -> str: return "{{{{$.outputs.artifacts['{}'].path}}}}".format(output_key) def _output_parameter_path_placeholder(output_key: str) -> str: return "{{{{$.outputs.parameters['{}'].output_file}}}}".format( output_key) def _resolve_output_path_placeholder(output_key: str) -> str: if type_utils.is_parameter_type(outputs_dict[output_key].type): return _output_parameter_path_placeholder(output_key) else: return _output_artifact_path_placeholder(output_key) placeholder_resolver = ExtraPlaceholderResolver() def _resolve_ir_placeholders_v2( arg, component_spec: _structures.ComponentSpec, arguments: dict, ) -> str: inputs_dict = { input_spec.name: input_spec for input_spec in component_spec.inputs or [] } if isinstance(arg, _structures.InputValuePlaceholder): input_name = arg.input_name input_value = arguments.get(input_name, None) if input_value is not None: return _input_parameter_placeholder(input_name) else: input_spec = inputs_dict[input_name] if input_spec.optional: return None else: raise ValueError( 'No value provided for input {}'.format( input_name)) elif isinstance(arg, _structures.InputUriPlaceholder): input_name = arg.input_name if input_name in arguments: input_uri = _input_artifact_uri_placeholder(input_name) return input_uri else: input_spec = inputs_dict[input_name] if input_spec.optional: return None else: raise ValueError( 'No value provided for input {}'.format( input_name)) elif isinstance(arg, _structures.OutputUriPlaceholder): output_name = arg.output_name output_uri = _output_artifact_uri_placeholder(output_name) return output_uri return placeholder_resolver.resolve_placeholder( arg=arg, component_spec=component_spec, arguments=arguments, ) resolved_cmd = _components._resolve_command_line_and_paths( component_spec=component_spec, arguments=arguments, input_path_generator=_input_artifact_path_placeholder, output_path_generator=_resolve_output_path_placeholder, placeholder_resolver=_resolve_ir_placeholders_v2, ) return resolved_cmd pipeline_task_spec = pipeline_spec_pb2.PipelineTaskSpec() # Keep track of auto-injected importer spec. importer_specs = {} # Check types of the reference arguments and serialize PipelineParams original_arguments = arguments arguments = arguments.copy() # Preserver input params for ContainerOp.inputs input_params = list( set([ param for param in arguments.values() if isinstance(param, _pipeline_param.PipelineParam) ])) for input_name, argument_value in arguments.items(): if isinstance(argument_value, _pipeline_param.PipelineParam): input_type = component_spec._inputs_dict[input_name].type reference_type = argument_value.param_type types.verify_type_compatibility( reference_type, input_type, 'Incompatible argument passed to the input "{}" of component "{}": ' .format(input_name, component_spec.name)) arguments[input_name] = str(argument_value) if type_utils.is_parameter_type(input_type): if argument_value.op_name: pipeline_task_spec.inputs.parameters[ input_name].task_output_parameter.producer_task = ( dsl_utils.sanitize_task_name( argument_value.op_name)) pipeline_task_spec.inputs.parameters[ input_name].task_output_parameter.output_parameter_key = ( argument_value.name) else: pipeline_task_spec.inputs.parameters[ input_name].component_input_parameter = argument_value.name else: if argument_value.op_name: pipeline_task_spec.inputs.artifacts[ input_name].task_output_artifact.producer_task = ( dsl_utils.sanitize_task_name( argument_value.op_name)) pipeline_task_spec.inputs.artifacts[ input_name].task_output_artifact.output_artifact_key = ( argument_value.name) elif is_compiling_for_v2: # argument_value.op_name could be none, in which case an importer node # will be inserted later. # Importer node is only applicable for v2 engine. pipeline_task_spec.inputs.artifacts[ input_name].task_output_artifact.producer_task = '' type_schema = type_utils.get_input_artifact_type_schema( input_name, component_spec.inputs) importer_specs[ input_name] = importer_node.build_importer_spec( input_type_schema=type_schema, pipeline_param_name=argument_value.name) elif isinstance(argument_value, str): pipeline_params = _pipeline_param.extract_pipelineparams_from_any( argument_value) if pipeline_params and is_compiling_for_v2: # argument_value contains PipelineParam placeholders which needs to be # replaced. And the input needs to be added to the task spec. for param in pipeline_params: # Form the name for the compiler injected input, and make sure it # doesn't collide with any existing input names. additional_input_name = ( dsl_component_spec. additional_input_name_for_pipelineparam(param)) for existing_input_name, _ in arguments.items(): if existing_input_name == additional_input_name: raise ValueError( 'Name collision between existing input name ' '{} and compiler injected input name {}'. format(existing_input_name, additional_input_name)) additional_input_placeholder = ( "{{{{$.inputs.parameters['{}']}}}}".format( additional_input_name)) argument_value = argument_value.replace( param.pattern, additional_input_placeholder) # The output references are subject to change -- the producer task may # not be whitin the same DAG. if param.op_name: pipeline_task_spec.inputs.parameters[ additional_input_name].task_output_parameter.producer_task = ( dsl_utils.sanitize_task_name(param.op_name)) pipeline_task_spec.inputs.parameters[ additional_input_name].task_output_parameter.output_parameter_key = param.name else: pipeline_task_spec.inputs.parameters[ additional_input_name].component_input_parameter = param.full_name input_type = component_spec._inputs_dict[input_name].type if type_utils.is_parameter_type(input_type): pipeline_task_spec.inputs.parameters[ input_name].runtime_value.constant_value.string_value = ( argument_value) elif is_compiling_for_v2: # An importer node with constant value artifact_uri will be inserted. # Importer node is only applicable for v2 engine. pipeline_task_spec.inputs.artifacts[ input_name].task_output_artifact.producer_task = '' type_schema = type_utils.get_input_artifact_type_schema( input_name, component_spec.inputs) importer_specs[input_name] = importer_node.build_importer_spec( input_type_schema=type_schema, constant_value=argument_value) elif isinstance(argument_value, int): pipeline_task_spec.inputs.parameters[ input_name].runtime_value.constant_value.int_value = argument_value elif isinstance(argument_value, float): pipeline_task_spec.inputs.parameters[ input_name].runtime_value.constant_value.double_value = argument_value elif isinstance(argument_value, _container_op.ContainerOp): raise TypeError( 'ContainerOp object {} was passed to component as an input argument. ' 'Pass a single output instead.'.format(input_name)) else: if is_compiling_for_v2: raise NotImplementedError( 'Input argument supports only the following types: PipelineParam' ', str, int, float. Got: "{}".'.format(argument_value)) if not component_spec.name: component_spec.name = _components._default_component_name # task.name is unique at this point. pipeline_task_spec.task_info.name = (dsl_utils.sanitize_task_name( task.name)) resolved_cmd = _resolve_commands_and_args_v2(component_spec=component_spec, arguments=original_arguments) task.container_spec = ( pipeline_spec_pb2.PipelineDeploymentConfig.PipelineContainerSpec( image=component_spec.implementation.container.image, command=resolved_cmd.command, args=resolved_cmd.args)) # TODO(chensun): dedupe IR component_spec and contaienr_spec pipeline_task_spec.component_ref.name = (dsl_utils.sanitize_component_name( task.name)) executor_label = dsl_utils.sanitize_executor_label(task.name) task.component_spec = dsl_component_spec.build_component_spec_from_structure( component_spec, executor_label, arguments.keys()) task.task_spec = pipeline_task_spec task.importer_specs = importer_specs # Override command and arguments if compiling to v2. if is_compiling_for_v2: task.command = resolved_cmd.command task.arguments = resolved_cmd.args # limit this to v2 compiling only to avoid possible behavior change in v1. task.inputs = input_params
def _attach_v2_specs( task: _container_op.ContainerOp, component_spec: _structures.ComponentSpec, arguments: Mapping[str, Any], ) -> None: """Attaches v2 specs to a ContainerOp object. Attach v2_specs to the ContainerOp object regardless whether the pipeline is being compiled to v1 (Argo yaml) or v2 (IR json). However, there're different behaviors for the two cases. Namely, resolved commands and arguments, error handling, etc. Regarding the difference in error handling, v2 has a stricter requirement on input type annotation. For instance, an input without any type annotation is viewed as an artifact, and if it's paired with InputValuePlaceholder, an error will be thrown at compile time. However, we cannot raise such an error in v1, as it wouldn't break existing pipelines. Args: task: The ContainerOp object to attach IR specs. component_spec: The component spec object. arguments: The dictionary of component arguments. """ def _resolve_commands_and_args_v2( component_spec: _structures.ComponentSpec, arguments: Mapping[str, Any], ) -> _components._ResolvedCommandLineAndPaths: """Resolves the command line argument placeholders for v2 (IR). Args: component_spec: The component spec object. arguments: The dictionary of component arguments. Returns: A named tuple: _components._ResolvedCommandLineAndPaths. """ inputs_dict = { input_spec.name: input_spec for input_spec in component_spec.inputs or [] } outputs_dict = { output_spec.name: output_spec for output_spec in component_spec.outputs or [] } def _input_artifact_uri_placeholder(input_key: str) -> str: if kfp.COMPILING_FOR_V2 and type_utils.is_parameter_type( inputs_dict[input_key].type): raise TypeError('Input "{}" with type "{}" cannot be paired with ' 'InputUriPlaceholder.'.format( input_key, inputs_dict[input_key].type)) else: return _generate_input_uri_placeholder(input_key) def _input_artifact_path_placeholder(input_key: str) -> str: if kfp.COMPILING_FOR_V2 and type_utils.is_parameter_type( inputs_dict[input_key].type): raise TypeError('Input "{}" with type "{}" cannot be paired with ' 'InputPathPlaceholder.'.format( input_key, inputs_dict[input_key].type)) else: return "{{{{$.inputs.artifacts['{}'].path}}}}".format(input_key) def _input_parameter_placeholder(input_key: str) -> str: if kfp.COMPILING_FOR_V2 and not type_utils.is_parameter_type( inputs_dict[input_key].type): raise TypeError('Input "{}" with type "{}" cannot be paired with ' 'InputValuePlaceholder.'.format( input_key, inputs_dict[input_key].type)) else: return "{{{{$.inputs.parameters['{}']}}}}".format(input_key) def _output_artifact_uri_placeholder(output_key: str) -> str: if kfp.COMPILING_FOR_V2 and type_utils.is_parameter_type( outputs_dict[output_key].type): raise TypeError('Output "{}" with type "{}" cannot be paired with ' 'OutputUriPlaceholder.'.format( output_key, outputs_dict[output_key].type)) else: return _generate_output_uri_placeholder(output_key) def _output_artifact_path_placeholder(output_key: str) -> str: return "{{{{$.outputs.artifacts['{}'].path}}}}".format(output_key) def _output_parameter_path_placeholder(output_key: str) -> str: return "{{{{$.outputs.parameters['{}'].output_file}}}}".format(output_key) def _resolve_output_path_placeholder(output_key: str) -> str: if type_utils.is_parameter_type(outputs_dict[output_key].type): return _output_parameter_path_placeholder(output_key) else: return _output_artifact_path_placeholder(output_key) placeholder_resolver = ExtraPlaceholderResolver() def _resolve_ir_placeholders_v2( arg, component_spec: _structures.ComponentSpec, arguments: dict, ) -> str: inputs_dict = {input_spec.name: input_spec for input_spec in component_spec.inputs or []} if isinstance(arg, _structures.InputValuePlaceholder): input_name = arg.input_name input_value = arguments.get(input_name, None) if input_value is not None: return _input_parameter_placeholder(input_name) else: input_spec = inputs_dict[input_name] if input_spec.optional: return None else: raise ValueError('No value provided for input {}'.format(input_name)) elif isinstance(arg, _structures.InputUriPlaceholder): input_name = arg.input_name if input_name in arguments: input_uri = _input_artifact_uri_placeholder(input_name) return input_uri else: input_spec = inputs_dict[input_name] if input_spec.optional: return None else: raise ValueError('No value provided for input {}'.format(input_name)) elif isinstance(arg, _structures.OutputUriPlaceholder): output_name = arg.output_name output_uri = _output_artifact_uri_placeholder(output_name) return output_uri return placeholder_resolver.resolve_placeholder( arg=arg, component_spec=component_spec, arguments=arguments, ) resolved_cmd = _components._resolve_command_line_and_paths( component_spec=component_spec, arguments=arguments, input_path_generator=_input_artifact_path_placeholder, output_path_generator=_resolve_output_path_placeholder, placeholder_resolver=_resolve_ir_placeholders_v2, ) return resolved_cmd pipeline_task_spec = pipeline_spec_pb2.PipelineTaskSpec() # Check types of the reference arguments and serialize PipelineParams arguments = arguments.copy() # Preserve input params for ContainerOp.inputs input_params_set = set([ param for param in arguments.values() if isinstance(param, _pipeline_param.PipelineParam) ]) for input_name, argument_value in arguments.items(): input_type = component_spec._inputs_dict[input_name].type argument_type = None if isinstance(argument_value, _pipeline_param.PipelineParam): argument_type = argument_value.param_type types.verify_type_compatibility( argument_type, input_type, 'Incompatible argument passed to the input "{}" of component "{}": ' .format(input_name, component_spec.name)) # Loop arguments defaults to 'String' type if type is unknown. # This has to be done after the type compatiblity check. if argument_type is None and isinstance( argument_value, (_for_loop.LoopArguments, _for_loop.LoopArgumentVariable)): argument_type = 'String' arguments[input_name] = str(argument_value) if type_utils.is_parameter_type(input_type): if argument_value.op_name: pipeline_task_spec.inputs.parameters[ input_name].task_output_parameter.producer_task = ( dsl_utils.sanitize_task_name(argument_value.op_name)) pipeline_task_spec.inputs.parameters[ input_name].task_output_parameter.output_parameter_key = ( argument_value.name) else: pipeline_task_spec.inputs.parameters[ input_name].component_input_parameter = argument_value.name else: if argument_value.op_name: pipeline_task_spec.inputs.artifacts[ input_name].task_output_artifact.producer_task = ( dsl_utils.sanitize_task_name(argument_value.op_name)) pipeline_task_spec.inputs.artifacts[ input_name].task_output_artifact.output_artifact_key = ( argument_value.name) elif isinstance(argument_value, str): argument_type = 'String' pipeline_params = _pipeline_param.extract_pipelineparams_from_any( argument_value) if pipeline_params and kfp.COMPILING_FOR_V2: # argument_value contains PipelineParam placeholders which needs to be # replaced. And the input needs to be added to the task spec. for param in pipeline_params: # Form the name for the compiler injected input, and make sure it # doesn't collide with any existing input names. additional_input_name = ( dsl_component_spec.additional_input_name_for_pipelineparam(param)) for existing_input_name, _ in arguments.items(): if existing_input_name == additional_input_name: raise ValueError('Name collision between existing input name ' '{} and compiler injected input name {}'.format( existing_input_name, additional_input_name)) # Add the additional param to the input params set. Otherwise, it will # not be included when the params set is not empty. input_params_set.add(param) additional_input_placeholder = ( "{{{{$.inputs.parameters['{}']}}}}".format(additional_input_name)) argument_value = argument_value.replace(param.pattern, additional_input_placeholder) # The output references are subject to change -- the producer task may # not be whitin the same DAG. if param.op_name: pipeline_task_spec.inputs.parameters[ additional_input_name].task_output_parameter.producer_task = ( dsl_utils.sanitize_task_name(param.op_name)) pipeline_task_spec.inputs.parameters[ additional_input_name].task_output_parameter.output_parameter_key = param.name else: pipeline_task_spec.inputs.parameters[ additional_input_name].component_input_parameter = param.full_name input_type = component_spec._inputs_dict[input_name].type if type_utils.is_parameter_type(input_type): pipeline_task_spec.inputs.parameters[ input_name].runtime_value.constant_value.string_value = ( argument_value) elif isinstance(argument_value, int): argument_type = 'Integer' pipeline_task_spec.inputs.parameters[ input_name].runtime_value.constant_value.int_value = argument_value elif isinstance(argument_value, float): argument_type = 'Float' pipeline_task_spec.inputs.parameters[ input_name].runtime_value.constant_value.double_value = argument_value elif isinstance(argument_value, _container_op.ContainerOp): raise TypeError( 'ContainerOp object {} was passed to component as an input argument. ' 'Pass a single output instead.'.format(input_name)) else: if kfp.COMPILING_FOR_V2: raise NotImplementedError( 'Input argument supports only the following types: PipelineParam' ', str, int, float. Got: "{}".'.format(argument_value)) argument_is_parameter_type = type_utils.is_parameter_type(argument_type) input_is_parameter_type = type_utils.is_parameter_type(input_type) if kfp.COMPILING_FOR_V2 and (argument_is_parameter_type != input_is_parameter_type): if isinstance(argument_value, dsl.PipelineParam): param_or_value_msg = 'PipelineParam "{}"'.format( argument_value.full_name) else: param_or_value_msg = 'value "{}"'.format(argument_value) raise TypeError( 'Passing ' '{param_or_value} with type "{arg_type}" (as "{arg_category}") to ' 'component input ' '"{input_name}" with type "{input_type}" (as "{input_category}") is ' 'incompatible. Please fix the type of the component input.'.format( param_or_value=param_or_value_msg, arg_type=argument_type, arg_category='Parameter' if argument_is_parameter_type else 'Artifact', input_name=input_name, input_type=input_type, input_category='Paramter' if input_is_parameter_type else 'Artifact', )) if not component_spec.name: component_spec.name = _components._default_component_name # task.name is unique at this point. pipeline_task_spec.task_info.name = (dsl_utils.sanitize_task_name(task.name)) resolved_cmd = _resolve_commands_and_args_v2( component_spec=component_spec, arguments=arguments) task.container_spec = ( pipeline_spec_pb2.PipelineDeploymentConfig.PipelineContainerSpec( image=component_spec.implementation.container.image, command=resolved_cmd.command, args=resolved_cmd.args)) # TODO(chensun): dedupe IR component_spec and contaienr_spec pipeline_task_spec.component_ref.name = ( dsl_utils.sanitize_component_name(task.name)) executor_label = dsl_utils.sanitize_executor_label(task.name) task.component_spec = dsl_component_spec.build_component_spec_from_structure( component_spec, executor_label, arguments.keys()) task.task_spec = pipeline_task_spec # Override command and arguments if compiling to v2. if kfp.COMPILING_FOR_V2: task.command = resolved_cmd.command task.arguments = resolved_cmd.args # limit this to v2 compiling only to avoid possible behavior change in v1. task.inputs = list(input_params_set)
def _is_output_parameter(output_key: str) -> bool: for output in component_spec.component_spec.outputs: if output.name == output_key: return type_utils.is_parameter_type(output.type) return False
def test_is_parameter_type(self): for type_name in _PARAMETER_TYPES: self.assertTrue(type_utils.is_parameter_type(type_name)) for type_name in _KNOWN_ARTIFACT_TYPES + _UNKNOWN_ARTIFACT_TYPES: self.assertFalse(type_utils.is_parameter_type(type_name))
def _create_pipeline_v2( self, pipeline_func: Callable[..., Any], pipeline_name: Optional[str] = None, pipeline_parameters_override: Optional[Mapping[str, Any]] = None, ) -> pipeline_spec_pb2.PipelineJob: """Creates a pipeline instance and constructs the pipeline spec from it. Args: pipeline_func: Pipeline function with @dsl.pipeline decorator. pipeline_name: The name of the pipeline. Optional. pipeline_parameters_override: The mapping from parameter names to values. Optional. Returns: A PipelineJob proto representing the compiled pipeline. """ # Create the arg list with no default values and call pipeline function. # Assign type information to the PipelineParam pipeline_meta = _python_op._extract_component_interface(pipeline_func) pipeline_name = pipeline_name or pipeline_meta.name pipeline_root = getattr(pipeline_func, 'pipeline_root', None) args_list = [] signature = inspect.signature(pipeline_func) for arg_name in signature.parameters: arg_type = None for pipeline_input in pipeline_meta.inputs or []: if arg_name == pipeline_input.name: arg_type = pipeline_input.type break if not type_utils.is_parameter_type(arg_type): raise TypeError( 'The pipeline argument "{arg_name}" is viewed as an artifact due to ' 'its type "{arg_type}". And we currently do not support passing ' 'artifacts as pipeline inputs. Consider type annotating the argument' ' with a primitive type, such as "str", "int", and "float".'.format( arg_name=arg_name, arg_type=arg_type)) args_list.append( dsl.PipelineParam( sanitize_k8s_name(arg_name, True), param_type=arg_type)) with dsl.Pipeline(pipeline_name) as dsl_pipeline: pipeline_func(*args_list) self._validate_exit_handler(dsl_pipeline) self._sanitize_and_inject_artifact(dsl_pipeline) # Fill in the default values. args_list_with_defaults = [] if pipeline_meta.inputs: args_list_with_defaults = [ dsl.PipelineParam( sanitize_k8s_name(input_spec.name, True), param_type=input_spec.type, value=input_spec.default) for input_spec in pipeline_meta.inputs ] # Making the pipeline group name unique to prevent name clashes with templates pipeline_group = dsl_pipeline.groups[0] temp_pipeline_group_name = uuid.uuid4().hex pipeline_group.name = temp_pipeline_group_name pipeline_spec = self._create_pipeline_spec( args_list_with_defaults, dsl_pipeline, ) pipeline_parameters = { param.name: param for param in args_list_with_defaults } # Update pipeline parameters override if there were any. pipeline_parameters_override = pipeline_parameters_override or {} for k, v in pipeline_parameters_override.items(): if k not in pipeline_parameters: raise ValueError('Pipeline parameter {} does not match any known ' 'pipeline argument.'.format(k)) pipeline_parameters[k].value = v runtime_config = compiler_utils.build_runtime_config_spec( output_directory=pipeline_root, pipeline_parameters=pipeline_parameters) pipeline_job = pipeline_spec_pb2.PipelineJob(runtime_config=runtime_config) pipeline_job.pipeline_spec.update(json_format.MessageToDict(pipeline_spec)) return pipeline_job