class_docstr: Helps to evaluate if linkable instances can be obtained using the given node, with joins if necessary.
  
  For example, consider a "start_node" containing the "bookings" measure, "is_instant" dimension, and "listing_id"
  entity with nodes_available_for_joins including a node with the "listing_id" entity, and the "country"
  dimension.
  
  We want to know if we can get "bookings", "is_instant", "listing_id__country" using the start_node. The result
  should be that we know: "is_instant" is available locally (i.e. in the same node), and if we join another node
  containing "listing_id" and "country" by "listing_id", we can get "listing_id__country".
  
  Since it's used on the left side of the join, the items in the "start_node" are sometimes labeled "left".
class_docstr_tok: ["Helps","to","evaluate","if","linkable","instances","can","be","obtained","using","the","given","node",",","with","joins","if","necessary",".","For","example",",","consider","a","``","start_node","''","containing","the","``","bookings","''","measure",",","``","is_instant","''","dimension",",","and","``","listing_id","''","entity","with","nodes_available_for_joins","including","a","node","with","the","``","listing_id","''","entity",",","and","the","``","country","''","dimension",".","We","want","to","know","if","we","can","get","``","bookings","''",",","``","is_instant","''",",","``","listing_id__country","''","using","the","start_node",".","The","result","should","be","that","we","know",":","``","is_instant","''","is","available","locally","(","i.e",".","in","the","same","node",")",",","and","if","we","join","another","node","containing","``","listing_id","''","and","``","country","''","by","``","listing_id","''",",","we","can","get","``","listing_id__country","''",".","Since","it","'s","used","on","the","left","side","of","the","join",",","the","items","in","the","``","start_node","''","are","sometimes","labeled","``","left","''","."]
docstr: Get nodes that can be joined to get 1 or more of the "needed_linkable_specs".
  
  The returned list is ordered by the number of "needed_linkable_specs" that it can satisfy.
docstr_tok: ["Get","nodes","that","can","be","joined","to","get","1","or","more","of","the","``","needed_linkable_specs","''",".","The","returned","list","is","ordered","by","the","number","of","``","needed_linkable_specs","''","that","it","can","satisfy","."]
code: def _find_joinable_candidate_nodes_that_can_satisfy_linkable_specs(
      self,
      start_node_instance_set: InstanceSet,
      needed_linkable_specs: List[LinkableInstanceSpec],
      join_type: SqlJoinType,
  ) -> List[JoinLinkableInstancesRecipe]:
      """Get nodes that can be joined to get 1 or more of the "needed_linkable_specs".
  
      The returned list is ordered by the number of "needed_linkable_specs" that it can satisfy.
      """
      candidates_for_join: List[JoinLinkableInstancesRecipe] = []
      start_node_spec_set = start_node_instance_set.spec_set
      for right_node in self._nodes_available_for_joins:
          data_set_in_right_node: SqlDataSet = (
              self._node_data_set_resolver.get_output_data_set(
                  right_node
              )
          )
          linkable_specs_in_right_node = (
              data_set_in_right_node.instance_set.spec_set.linkable_specs
          )
          entity_specs_in_right_node = (
              data_set_in_right_node.instance_set.spec_set.entity_specs
          )
  
          # For each unlinked entity in the data set, create a candidate for joining.
          # For a data set to be useful for satisfying a linkable spec, it needs to have the entity
          # and the linkable spec without the entity. This allows joining based on the entity, which will
          # then produce the linkable spec. See comments further below for more details.
  
          for entity_spec_in_right_node in entity_specs_in_right_node:
              # If an entity has links, what that means and whether it can be used is unclear at the moment,
              # so skip it.
              if len(entity_spec_in_right_node.entity_links) > 0:
                  continue
  
              entity_instance_in_right_node = None
              for (
                  instance
              ) in data_set_in_right_node.instance_set.entity_instances:
                  if instance.spec == entity_spec_in_right_node:
                      entity_instance_in_right_node = instance
                      break
  
              if entity_instance_in_right_node is None:
                  raise RuntimeError(
                      f"Could not find entity instance with name ({entity_spec_in_right_node})"
                  )
  
              assert (
                  len(entity_instance_in_right_node.defined_from) == 1
              ), f"Did not get exactly 1 defined_from in {entity_instance_in_right_node}"
  
              entity_in_right_node = self._semantic_model_lookup.get_entity_in_semantic_model(
                  entity_instance_in_right_node.defined_from[0]
              )
              if entity_in_right_node is None:
                  raise RuntimeError(
                      f"Invalid SemanticModelElementReference {entity_instance_in_right_node.defined_from[0]}"
                  )
  
              entity_instance_in_left_node = None
              for instance in start_node_instance_set.entity_instances:
                  if (
                      instance.spec.reference
                      == entity_spec_in_right_node.reference
                  ):
                      entity_instance_in_left_node = instance
                      break
  
              if entity_instance_in_left_node is None:
                  # The right node can have a superset of entities.
                  continue
  
              assert len(entity_instance_in_left_node.defined_from) == 1
              assert (
                  len(entity_instance_in_right_node.defined_from) == 1
              )
  
              if not self._join_evaluator.is_valid_semantic_model_join(
                  left_semantic_model_reference=entity_instance_in_left_node.defined_from[
                      0
                  ].semantic_model_reference,
                  right_semantic_model_reference=entity_instance_in_right_node.defined_from[
                      0
                  ].semantic_model_reference,
                  on_entity_reference=entity_spec_in_right_node.reference,
              ):
                  continue
  
              linkless_entity_spec_in_node = (
                  LinklessEntitySpec.from_element_name(
                      entity_spec_in_right_node.element_name
                  )
              )
  
              satisfiable_linkable_specs = []
              for needed_linkable_spec in needed_linkable_specs:
                  assert (
                      len(needed_linkable_spec.entity_links) != 0
                  ), f"Invalid needed linkable spec passed in {needed_linkable_spec}"
  
                  # If the entity in the data set matches the link, then it can be used for joins. For example,
                  # if the node has the entity "user_id", and dimension "country" then it can be used for
                  # satisfying "user_id__country".
                  #
                  # Multi-hop example:
                  # required_linkable_spec = "user_id__device_id__platform"
                  # entity_spec_in_data_set = "user_id"
                  #
                  # Then the data set must contain "device_id__platform", which is realized with
                  #
                  # required_linkable_spec.remove_first_entity_link()
                  #
                  # We might also need to check the entity type and see if it's the type of join we're allowing,
                  # but since we're doing all left joins now, it's been left out.
  
                  required_entity_matches_data_set_entity = (
                      LinklessEntitySpec.from_reference(
                          needed_linkable_spec.entity_links[0]
                      )
                      == linkless_entity_spec_in_node
                  )
                  needed_linkable_spec_in_node = (
                      needed_linkable_spec.without_first_entity_link
                      in linkable_specs_in_right_node
                  )
                  if (
                      required_entity_matches_data_set_entity
                      and needed_linkable_spec_in_node
                  ):
                      satisfiable_linkable_specs.append(
                          needed_linkable_spec
                      )
  
              # If this node can satisfy some linkable specs, it could be useful to join on, so add it to the
              # candidate list.
              if len(satisfiable_linkable_specs) > 0:
                  join_on_partition_dimensions = self._partition_resolver.resolve_partition_dimension_joins(
                      start_node_spec_set=start_node_spec_set,
                      node_to_join_spec_set=data_set_in_right_node.instance_set.spec_set,
                  )
                  join_on_partition_time_dimensions = self._partition_resolver.resolve_partition_time_dimension_joins(
                      start_node_spec_set=start_node_spec_set,
                      node_to_join_spec_set=data_set_in_right_node.instance_set.spec_set,
                  )
                  validity_window_join_description = CreateValidityWindowJoinDescription(
                      self._semantic_model_lookup
                  ).transform(
                      instance_set=data_set_in_right_node.instance_set
                  )
  
                  candidates_for_join.append(
                      JoinLinkableInstancesRecipe(
                          node_to_join=right_node,
                          join_on_entity=linkless_entity_spec_in_node,
                          satisfiable_linkable_specs=satisfiable_linkable_specs,
                          join_on_partition_dimensions=join_on_partition_dimensions,
                          join_on_partition_time_dimensions=join_on_partition_time_dimensions,
                          validity_window=validity_window_join_description,
                          join_type=join_type,
                      )
                  )
  
      # Return with the candidate set that can satisfy the most linkable specs at the front.
      return sorted(
          candidates_for_join,
          key=lambda x: len(x.satisfiable_linkable_specs),
          reverse=True,
      )
  
imports: from __future__ import annotations
  import itertools
  import logging
  from dataclasses import dataclass
  from typing import List, Optional, Sequence, Tuple
  from dbt_semantic_interfaces.pretty_print import pformat_big_objects
  from metricflow.dataflow.builder.node_data_set import DataflowPlanNodeOutputDataSetResolver
  from metricflow.dataflow.builder.partitions import PartitionJoinResolver
  from metricflow.dataflow.dataflow_plan import BaseOutput, FilterElementsNode, JoinDescription, PartitionDimensionJoinDescription, PartitionTimeDimensionJoinDescription, ValidityWindowJoinDescription
  from metricflow.dataset.sql_dataset import SqlDataSet
  from metricflow.instances import InstanceSet
  from metricflow.model.semantics.semantic_model_join_evaluator import SemanticModelJoinEvaluator
  from metricflow.plan_conversion.instance_converters import CreateValidityWindowJoinDescription
  from metricflow.protocols.semantics import SemanticModelAccessor
  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 NodeEvaluatorForLinkableInstances obj's underlying member method code with example usage: obj._find_joinable_candidate_nodes_that_can_satisfy_linkable_specs(start_node_instance_set, needed_linkable_specs, join_type) and returns: sorted