This times the steps in a pipeline. Give it an iterable to test against
followed by the steps of the pipeline seperated in individual functions.

Example Usage:
```
    from random import choice, randint

    l = [randint(0,50) for i in range(100)]

    step1 = lambda iterable:(i for i in iterable if i%5==0)
    step2 = lambda iterable:(i for i in iterable if i%8==3)
    step3 = lambda iterable:sorted((1.0*i)/50 for i in iterable)
    step4 = lambda iterable:(float(float(float(float(i*3)))) for i in iterable)

    print('filter first')
    time_pipeline(l, step1, step2, step3, step4)
    print('process first')
    time_pipeline(l, step3, step4, step1, step2)
    print('filter, process, filter, process')
    time_pipeline(l, step1, step3, step2, step4)
``` 

Outputs:
    filter first
    step 1 | 2.0427s | step1 = lambda iterable:(i for i in iterable if i%5==0)
    step 2 | 2.0510s | step2 = lambda iterable:(i for i in iterable if i%8==3)
    step 3 | 2.4839s | step3 = lambda iterable:sorted((1.0*i)/50 for i in iterable)
    step 4 | 2.8446s | step4 = lambda iterable:(float(float(float(float(i*3)))) for i in iterable)
    process first
    step 1 | 7.5291s | step3 = lambda iterable:sorted((1.0*i)/50 for i in iterable)
    step 2 | 20.6732s | step4 = lambda iterable:(float(float(float(float(i*3)))) for i in iterable)
    step 3 | 16.8470s | step1 = lambda iterable:(i for i in iterable if i%5==0)
    step 4 | 16.8269s | step2 = lambda iterable:(i for i in iterable if i%8==3)
    filter, process, filter, process
    step 1 | 2.0528s | step1 = lambda iterable:(i for i in iterable if i%5==0)
    step 2 | 3.3039s | step3 = lambda iterable:sorted((1.0*i)/50 for i in iterable)
    step 3 | 3.1385s | step2 = lambda iterable:(i for i in iterable if i%8==3)
    step 4 | 3.1489s | step4 = lambda iterable:(float(float(float(float(i*3)))) for i in iterable)

def time_pipeline(iterable, *steps):
    '''
This times the steps in a pipeline. Give it an iterable to test against
followed by the steps of the pipeline seperated in individual functions.

Example Usage:
```
    from random import choice, randint

    l = [randint(0,50) for i in range(100)]

    step1 = lambda iterable:(i for i in iterable if i%5==0)
    step2 = lambda iterable:(i for i in iterable if i%8==3)
    step3 = lambda iterable:sorted((1.0*i)/50 for i in iterable)
    step4 = lambda iterable:(float(float(float(float(i*3)))) for i in iterable)

    print('filter first')
    time_pipeline(l, step1, step2, step3, step4)
    print('process first')
    time_pipeline(l, step3, step4, step1, step2)
    print('filter, process, filter, process')
    time_pipeline(l, step1, step3, step2, step4)
``` 

Outputs:
    filter first
    step 1 | 2.0427s | step1 = lambda iterable:(i for i in iterable if i%5==0)
    step 2 | 2.0510s | step2 = lambda iterable:(i for i in iterable if i%8==3)
    step 3 | 2.4839s | step3 = lambda iterable:sorted((1.0*i)/50 for i in iterable)
    step 4 | 2.8446s | step4 = lambda iterable:(float(float(float(float(i*3)))) for i in iterable)
    process first
    step 1 | 7.5291s | step3 = lambda iterable:sorted((1.0*i)/50 for i in iterable)
    step 2 | 20.6732s | step4 = lambda iterable:(float(float(float(float(i*3)))) for i in iterable)
    step 3 | 16.8470s | step1 = lambda iterable:(i for i in iterable if i%5==0)
    step 4 | 16.8269s | step2 = lambda iterable:(i for i in iterable if i%8==3)
    filter, process, filter, process
    step 1 | 2.0528s | step1 = lambda iterable:(i for i in iterable if i%5==0)
    step 2 | 3.3039s | step3 = lambda iterable:sorted((1.0*i)/50 for i in iterable)
    step 3 | 3.1385s | step2 = lambda iterable:(i for i in iterable if i%8==3)
    step 4 | 3.1489s | step4 = lambda iterable:(float(float(float(float(i*3)))) for i in iterable)
'''
    if callable(iterable):
        try:
            iter(iterable())
            callable_base = True
        except:
            raise TypeError('time_pipeline needs the first argument to be an iterable or a function that produces an iterable.')
    else:
        try:
            iter(iterable)
            callable_base = False
        except:
            raise TypeError('time_pipeline needs the first argument to be an iterable or a function that produces an iterable.')
    # if iterable is not a function, load the whole thing
    # into a list so it can be ran over multiple times
    if not callable_base:
        iterable = tuple(iterable)
    # these store timestamps for time calculations
    durations = []
    results = []
    for i,_ in enumerate(steps):
        current_tasks = steps[:i+1]
        #print('testing',current_tasks)
        duration = 0.0
        # run this test x number of times
        for t in range(100000):
            # build the generator
            test_generator = iter(iterable()) if callable_base else iter(iterable)
            # time its execution
            start = ts()
            for task in current_tasks:
                test_generator = task(test_generator)
            for i in current_tasks[-1](test_generator):
                pass
            duration += ts() - start
        durations.append(duration)
        if len(durations) == 1:
            results.append(durations[0])
            #print(durations[0],durations[0])
        else:
            results.append(durations[-1]-durations[-2])
            #print(durations[-1]-durations[-2],durations[-1])
    #print(results)
    #print(durations)
    assert sum(results) > 0
    resultsum = sum(results)
    ratios = [i/resultsum for i in results]
    #print(ratios)
    for i in range(len(ratios)):
        try:
            s = getsource(steps[i]).splitlines()[0].strip()
        except:
            s = repr(steps[i]).strip()
        print('step {} | {:2.4f}s | {}'.format(i+1, durations[i], s))