class_docstr: Processes source nodes before other nodes are joined.
  
  Generally, the source nodes will be combined with other dataflow plan nodes to produce a new set of nodes to realize
  a condition of the query. For example, to realize a time range constraint, a ConstrainTimeRangeNode will be added
  to the source nodes.
  
  e.g.
  
  <SomeDataflowPlanNode/>
  
  ->
  
  <ConstrainTimeRangeNode>
      <SomeDataflowPlanNode/>
  </ConstrainTimeRangeNode>
docstr: Adds a time range constraint node to the input nodes.
docstr_tok: ["Adds","a","time","range","constraint","node","to","the","input","nodes","."]
code: def add_time_range_constraint(
      self,
      source_nodes: Sequence[BaseOutput],
      metric_time_dimension_reference: TimeDimensionReference,
      time_range_constraint: Optional[TimeRangeConstraint] = None,
  ) -> Sequence[BaseOutput]:
      """Adds a time range constraint node to the input nodes."""
      processed_nodes: List[BaseOutput] = []
      for source_node in source_nodes:
          # Constrain the time range if specified.
          if time_range_constraint:
              node_output_data_set = (
                  self._node_data_set_resolver.get_output_data_set(
                      source_node
                  )
              )
              constrain_time = False
              for (
                  time_dimension_instance
              ) in (
                  node_output_data_set.instance_set.time_dimension_instances
              ):
                  if (
                      time_dimension_instance.spec.reference
                      == metric_time_dimension_reference
                      and len(time_dimension_instance.spec.entity_links)
                      == 0
                  ):
                      constrain_time = True
                      break
              if constrain_time:
                  processed_nodes.append(
                      ConstrainTimeRangeNode(
                          parent_node=source_node,
                          time_range_constraint=time_range_constraint,
                      )
                  )
              else:
                  processed_nodes.append(source_node)
          else:
              processed_nodes.append(source_node)
      return processed_nodes
  
imports: from __future__ import annotations
  import logging
  from dataclasses import dataclass
  from typing import List, Optional, Sequence, Set
  from dbt_semantic_interfaces.pretty_print import pformat_big_objects
  from dbt_semantic_interfaces.references import EntityReference, TimeDimensionReference
  from metricflow.dataflow.builder.node_data_set import DataflowPlanNodeOutputDataSetResolver
  from metricflow.dataflow.builder.partitions import PartitionJoinResolver
  from metricflow.dataflow.dataflow_plan import BaseOutput, ConstrainTimeRangeNode, FilterElementsNode, JoinDescription, JoinToBaseOutputNode
  from metricflow.filters.time_constraint import TimeRangeConstraint
  from metricflow.model.semantics.semantic_model_join_evaluator import MAX_JOIN_HOPS, SemanticModelJoinEvaluator
  from metricflow.protocols.semantics import SemanticModelAccessor
  from metricflow.specs.spec_set_transforms import ToElementNameSet
  from metricflow.specs.specs import InstanceSpecSet, LinkableInstanceSpec, LinklessEntitySpec
  from metricflow.sql.sql_plan import SqlJoinType
label_desc: Use image node_id 2 for calling the PreJoinNodeProcessor obj's underlying member method code with example usage: obj.add_time_range_constraint(source_nodes, metric_time_dimension_reference, time_range_constraint) and returns: processed_nodes