Beispiel #1
0
  def produced_types_for_subject(self, subject, output_product_types):
    """Filters the given list of output products to those that are actually possible to produce.

    This method additionally validates that there are no "partially consumed" input products.
    A partially consumed input product is a product where no planner successfully consumes the
    product, but at least one planner would consume it given some other missing input.

    Note that this does not validate dependency subjects of the input subject, so it is necessary
    to validate every call to `def promise` against these requirements.
    """
    input_products = list(Products.for_subject(subject))

    producible_output_types = list()
    fully_consumed = set()
    partially_consumed_candidates = defaultdict(lambda: defaultdict(set))
    for output_product_type in output_product_types:
      if self._apply_product_requirements(output_product_type,
                                          input_products,
                                          fully_consumed,
                                          partially_consumed_candidates):
        producible_output_types.append(output_product_type)

    # If any partially consumed candidate was not fully consumed by some planner, it's an error.
    partially_consumed = {product: partials
                          for product, partials in partially_consumed_candidates.items()
                          if product not in fully_consumed}
    if partially_consumed:
      raise PartiallyConsumedInputsError(output_product_type, subject, partially_consumed)

    return producible_output_types
Beispiel #2
0
  def for_product_type_and_subject(self, product_type, subject, configuration=None):
    """Return the set of task planners that can produce the given product type for the subject.

    TODO: memoize.

    :param type product_type: The product type the returned planners are capable of producing.
    :param subject: The subject that the product will be produced for.
    :param configuration: An optional configuration to require that a planner consumes, or None.
    :rtype: set of :class:`TaskPlanner`
    """
    input_products = list(Products.for_subject(subject))

    partially_consumed_candidates = defaultdict(lambda: defaultdict(set))
    for planner, ored_clauses in self._product_requirements[product_type].items():
      fully_consumed = set()
      if not self._apply_product_requirement_clauses(input_products,
                                                     planner,
                                                     ored_clauses,
                                                     fully_consumed,
                                                     partially_consumed_candidates):
        continue
      # Only yield planners that were recursively able to consume the configuration.
      # TODO: This is matching on type only, while selectors are usually implemented
      # as by-name. Convert config selectors to configuration mergers.
      if not configuration or type(configuration) in fully_consumed:
        yield planner
Beispiel #3
0
  def produced_types_for_subject(self, subject, output_product_types):
    """Filters the given list of output products to those that are actually possible to produce.

    This method additionally validates that there are no "partially consumed" input products.
    A partially consumed input product is a product where no planner successfully consumes the
    product, but at least one planner would consume it given some other missing input.

    Note that this does not validate dependency subjects of the input subject, so it is necessary
    to validate every call to `def promise` against these requirements.
    """
    input_products = list(Products.for_subject(subject))

    producible_output_types = list()
    fully_consumed = set()
    partially_consumed_candidates = defaultdict(lambda: defaultdict(set))
    for output_product_type in output_product_types:
      if self._apply_product_requirements(output_product_type,
                                          input_products,
                                          fully_consumed,
                                          partially_consumed_candidates):
        producible_output_types.append(output_product_type)

    # If any partially consumed candidate was not fully consumed by some planner, it's an error.
    partially_consumed = {product: partials
                          for product, partials in partially_consumed_candidates.items()
                          if product not in fully_consumed}
    if partially_consumed:
      raise PartiallyConsumedInputsError(output_product_type, subject, partially_consumed)

    return producible_output_types
Beispiel #4
0
  def for_product_type_and_subject(self, product_type, subject, configuration=None):
    """Return the set of task planners that can produce the given product type for the subject.

    TODO: memoize.

    :param type product_type: The product type the returned planners are capable of producing.
    :param subject: The subject that the product will be produced for.
    :param configuration: An optional configuration to require that a planner consumes, or None.
    :rtype: set of :class:`TaskPlanner`
    """
    input_products = list(Products.for_subject(subject))

    partially_consumed_candidates = defaultdict(lambda: defaultdict(set))
    for planner, ored_clauses in self._product_requirements[product_type].items():
      fully_consumed = set()
      if not self._apply_product_requirement_clauses(input_products,
                                                     planner,
                                                     ored_clauses,
                                                     fully_consumed,
                                                     partially_consumed_candidates):
        continue
      # Only yield planners that were recursively able to consume the configuration.
      # TODO: This is matching on type only, while selectors are usually implemented
      # as by-name. Convert config selectors to configuration mergers.
      if not configuration or type(configuration) in fully_consumed:
        yield planner
Beispiel #5
0
  def promise(self, subject, product_type, configuration=None):
    if isinstance(subject, Address):
      subject = self._graph.resolve(subject)

    promise = Promise(product_type, subject, configuration=configuration)
    plan = self._product_mapper.promised(promise)
    if plan is not None:
      return promise

    plans = []
    # For all planners that can produce this product with the given configuration, request it.
    for planner in self._planners.for_product_type_and_subject(product_type,
                                                               subject,
                                                               configuration=configuration):
      plan = planner.plan(self, product_type, subject, configuration=configuration)
      if plan:
        plans.append((planner, plan))
    # Additionally, if the product is directly available as a "native" product on the subject,
    # include a Plan that lifts it off the subject.
    for native_product_type in Products.for_subject(subject):
      if native_product_type == product_type:
        plans.append((self.NoPlanner, Plan(func_or_task_type=lift_native_product,
                                       subjects=(subject,),
                                       subject=subject,
                                       product_type=product_type)))

    # TODO: It should be legal to have multiple plans, and they should be merged.
    if len(plans) > 1:
      planners = [planner for planner, plan in plans]
      raise ConflictingProducersError(product_type, subject, planners)
    elif not plans:
      raise NoProducersError(product_type, subject)

    planner, plan = plans[0]
    try:
      primary_promise = self._product_mapper.register_promises(product_type, plan,
                                                               primary_subject=subject,
                                                               configuration=configuration)
      self._plans_by_product_type_by_planner[planner][product_type].add(plan)
      return primary_promise
    except ProductMapper.InvalidRegistrationError:
      raise SchedulingError('The plan produced for {subject!r} by {planner!r} does not cover '
                            '{subject!r}:\n\t{plan!r}'.format(subject=subject,
                                                              planner=type(planner).__name__,
                                                              plan=plan))
Beispiel #6
0
  def promise(self, subject, product_type, configuration=None):
    if isinstance(subject, Address):
      subject = self._graph.resolve(subject)

    promise = Promise(product_type, subject, configuration=configuration)
    plan = self._product_mapper.promised(promise)
    if plan is not None:
      return promise

    plans = []
    # For all planners that can produce this product with the given configuration, request it.
    for planner in self._planners.for_product_type_and_subject(product_type,
                                                               subject,
                                                               configuration=configuration):
      plan = planner.plan(self, product_type, subject, configuration=configuration)
      if plan:
        plans.append((planner, plan))
    # Additionally, if the product is directly available as a "native" product on the subject,
    # include a Plan that lifts it off the subject.
    for native_product_type in Products.for_subject(subject):
      if native_product_type == product_type:
        plans.append((self.NoPlanner, Plan(func_or_task_type=lift_native_product,
                                       subjects=(subject,),
                                       subject=subject,
                                       product_type=product_type)))

    # TODO: It should be legal to have multiple plans, and they should be merged.
    if len(plans) > 1:
      planners = [planner for planner, plan in plans]
      raise ConflictingProducersError(product_type, subject, planners)
    elif not plans:
      raise NoProducersError(product_type, subject)

    planner, plan = plans[0]
    try:
      primary_promise = self._product_mapper.register_promises(product_type, plan,
                                                               primary_subject=subject,
                                                               configuration=configuration)
      self._plans_by_product_type_by_planner[planner][product_type].add(plan)
      return primary_promise
    except ProductMapper.InvalidRegistrationError:
      raise SchedulingError('The plan produced for {subject!r} by {planner!r} does not cover '
                            '{subject!r}:\n\t{plan!r}'.format(subject=subject,
                                                              planner=type(planner).__name__,
                                                              plan=plan))