docs: def query(self):
          """
          Generates a query using the ingredients supplied by the recipe.
  
          :return: A SQLAlchemy query
          """
          if len(self._cauldron.ingredients()) == 0:
              raise BadRecipe('No ingredients have been added to this recipe')
          if not self.dirty and self._query:
              return self._query
  
          # Step 1: Gather up global filters and user filters and
          # apply them as if they had been added to recipe().filters(...)
  
          for extension in self.recipe_extensions:
              extension.add_ingredients()
  
          # Step 2: Build the query (now that it has all the filters
          # and apply any blend recipes
  
          # Get the parts of the query from the cauldron
          # We don't need to regather order_bys
          recipe_parts = self._cauldron.brew_query_parts()
          recipe_parts['order_bys'] = self._prepare_order_bys()
  
          for extension in self.recipe_extensions:
              recipe_parts = extension.modify_recipe_parts(recipe_parts)
  
          # Start building the query
          query = self._session.query(*recipe_parts['columns'])
          if self._select_from is not None:
              query = query.select_from(self._select_from)
          recipe_parts['query'] = query \
              .group_by(*recipe_parts['group_bys']) \
              .order_by(*recipe_parts['order_bys']) \
              .filter(*recipe_parts['filters'])
  
          if recipe_parts['havings']:
              for having in recipe_parts['havings']:
                  recipe_parts['query'] = recipe_parts['query'].having(having)
  
          for extension in self.recipe_extensions:
              recipe_parts = extension.modify_prequery_parts(recipe_parts)
  
          if self._select_from is None and len(
              recipe_parts['query'].selectable.froms
          ) != 1:
              raise BadRecipe(
                  'Recipes must use ingredients that all come from '
                  'the same table. \nDetails on this recipe:\n{'
                  '}'.format(str(self._cauldron))
              )
  
          for extension in self.recipe_extensions:
              recipe_parts = extension.modify_postquery_parts(recipe_parts)
  
          recipe_parts = run_hooks(
              recipe_parts, 'modify_query', self.dynamic_extensions
          )
  
          # Apply limit on the outermost query
          # This happens after building the comparison recipe
          if self._limit and self._limit > 0:
              recipe_parts['query'] = recipe_parts['query'].limit(self._limit)
  
          if self._offset and self._offset > 0:
              recipe_parts['query'] = recipe_parts['query'].offset(self._offset)
  
          # Step 5:  Clear the dirty flag,
          # Patch the query if there's a comparison query
          # cache results
  
          self._query = recipe_parts['query']
          self.dirty = False
          return self._query