def _shuffle_items(items, bucket_key=None, disable=None, seed=None, session=None):
    """
    Shuffles a list of `items` in place.

    If `bucket_key` is None, items are shuffled across the entire list.

    `bucket_key` is an optional function called for each item in `items` to
    calculate the key of bucket in which the item falls.

    Bucket defines the boundaries across which items will not
    be shuffled.

    `disable` is a function that takes an item and returns a falsey value
    if this item is ok to be shuffled. It returns a truthy value otherwise and
    the truthy value is used as part of the item's key when determining the bucket
    it belongs to.
    """

    if seed is not None:
        random.seed(seed)

    # If `bucket_key` is falsey, shuffle is global.
    if not bucket_key and not disable:
        random.shuffle(items)
        return

    def get_full_bucket_key(item):
        assert bucket_key or disable
        if bucket_key and disable:
            return ItemKey(bucket=bucket_key(item, session), disabled=disable(item, session))
        elif disable:
            return ItemKey(disabled=disable(item, session))
        else:
            return ItemKey(bucket=bucket_key(item, session))

    # For a sequence of items A1, A2, B1, B2, C1, C2,
    # where key(A1) == key(A2) == key(C1) == key(C2),
    # items A1, A2, C1, and C2 will end up in the same bucket.
    buckets = OrderedDict()
    for item in items:
        full_bucket_key = get_full_bucket_key(item)
        if full_bucket_key not in buckets:
            buckets[full_bucket_key] = []
        buckets[full_bucket_key].append(item)

    # Shuffle inside a bucket

    bucket_keys = list(buckets.keys())

    for full_bucket_key in buckets.keys():
        if full_bucket_key.bucket == FAILED_FIRST_LAST_FAILED_BUCKET_KEY:
            # Do not shuffle the last failed bucket
            continue

        if not full_bucket_key.disabled:
            random.shuffle(buckets[full_bucket_key])

    # Shuffle buckets

    # Only the first bucket can be FAILED_FIRST_LAST_FAILED_BUCKET_KEY
    if bucket_keys and bucket_keys[0].bucket == FAILED_FIRST_LAST_FAILED_BUCKET_KEY:
        new_bucket_keys = list(buckets.keys())[1:]
        random.shuffle(new_bucket_keys)
        new_bucket_keys.insert(0, bucket_keys[0])
    else:
        new_bucket_keys = list(buckets.keys())
        random.shuffle(new_bucket_keys)

    items[:] = [item for bk in new_bucket_keys for item in buckets[bk]]
    return
Shuffles a list of `items` in place.

    If `bucket_key` is None, items are shuffled across the entire list.

    `bucket_key` is an optional function called for each item in `items` to
    calculate the key of bucket in which the item falls.

    Bucket defines the boundaries across which items will not
    be shuffled.

    `disable` is a function that takes an item and returns a falsey value
    if this item is ok to be shuffled. It returns a truthy value otherwise and
    the truthy value is used as part of the item's key when determining the bucket
    it belongs to.