def test_add_workout(self): ws1 = [0, 2, 5] rt1 = FirstRaceType(name='Marathon', distance=FirstDistance.from_string('42.195 km')) rd1 = date(year=2017, month=7, day=29) r1 = FirstRace(name='SFM', race_type=rt1, race_date=rd1) rn1 = FirstRunner(name='DBD') p1 = FirstPlan(name='My first marathon training plan', weekly_schedule=ws1, race=r1, runner=rn1) t_warmup = FirstTime.from_string('0:15:00') p_warmup = FirstPace.from_string('0:10:00 min per mile') s_warmup = FirstStepBody(name='Warm up', pace=p_warmup, time=t_warmup) s_repeat = FirstStepRepeat(name='repeat X 8', repeat=8) d_interval = FirstDistance.from_string('400 m') p_fast = FirstPace.from_string('0:08:00 min per mile') s_fast = FirstStepBody(name='Fast', pace=p_fast, distance=d_interval) s_repeat.add_step(s_fast) s_slow = FirstStepBody(name='Rest', pace=p_warmup, distance=d_interval) s_repeat.add_step(s_slow) t_cooldown = FirstTime.from_string('0:10:00') s_cooldown = FirstStepBody(name='Cool down', pace=p_warmup, time=t_cooldown) wo = FirstWorkout(name='Week 1 Key-run 1', workout_date=date(year=2017, month=6, day=24)) wo.add_step(step=s_warmup) wo.add_step(step=s_repeat) wo.add_step(step=s_cooldown) try: # first workout p1.add_workout(workout=wo) cmp_string = ('Training Plan:\nName - "My first marathon training plan"\n' + 'Workout days: Mon, Wed, Sat\nRace:\n' + ' Name - "SFM" of type Marathon - 42.195 km\nRunner:\n Name - "DBD"\nWorkouts:\n' + ' "Week 1 Key-run 1"\n Sat 2017-06-24\n scheduled\n' + 'Total 1 workouts\n') self.assertEqual(cmp_string, str(p1)) file_name = 'cmp_plan2.tcx' with open('{}/{}'.format(Config.TEST_RESOURCE_DIR, file_name), 'r') as from_file: cmp_string = from_file.read() self.assertEqual(cmp_string, p1.tcx()) file_name = 'cmp_plan2.json' with open('{}/{}'.format(Config.TEST_RESOURCE_DIR, file_name), 'r') as from_file: cmp_json = json.load(from_file) self.assertEqual(cmp_json, p1.to_json()) file_name = 'cmp_plan2_km.json' with open('{}/{}'.format(Config.TEST_RESOURCE_DIR, file_name), 'r') as from_file: cmp_json = json.load(from_file) self.assertEqual(cmp_json, p1.to_json(output_unit='km')) file_name = 'cmp_plan2.html' with open('{}/{}'.format(Config.TEST_RESOURCE_DIR, file_name), 'r') as from_file: cmp_html = from_file.read() self.assertEqual(cmp_html, p1.to_html()) file_name = 'cmp_plan2_km.html' with open('{}/{}'.format(Config.TEST_RESOURCE_DIR, file_name), 'r') as from_file: cmp_html = from_file.read() self.assertEqual(cmp_html, p1.to_html(output_unit='km')) except TypeError as ex: self.fail(str(ex))
def test_from_string(self): try: distance = FirstDistance.from_string(string='1 mile') self.assertEqual('1.0 mile', str(distance)) except ValueError as ex: self.fail(ex) try: _ = FirstDistance.from_string('onlyonetoken') self.fail('from_string is expected to fail with only one token') except ValueError as ex: self.assertEqual('2 tokens are expected, number and unit, but got "onlyonetoken"', str(ex)) try: _ = FirstDistance.from_string('34t papa') self.fail('from_string is expected to fail if the first token is not a number') except ValueError as ex: message = 'first token is expected to be a number but could not convert string to float: \'34t\'' self.assertEqual(message, str(ex)) try: _ = FirstDistance.from_string('-35.2 papa') self.fail('FirstDistance is expected to fail if the number is negative') except ValueError as ex: self.assertEqual('-35.2 is not a positive number', str(ex)) try: _ = FirstDistance.from_string('42.2 papa') self.fail('FirstDistance is expected to fail for invalid unit') except ValueError as ex: self.assertEqual('"papa" is not a valid unit', str(ex))
def to_distance(self, time, unit): """ How far you run given duration with this pace :param time: the duration :param unit: the desired unit of the result :return: the distance value for this unit :rtype: float """ factor = time.total_seconds()/self.time.total_seconds() result_distance = FirstDistance(distance=factor, unit=self.length_unit) return result_distance.convert_to(unit=unit)
def from_time_distance(cls, time: FirstTime, distance: FirstDistance, unit: str = None): """ Constructor: Initiate FirstPace from time/distance :param time: :type time: FirstTime :param distance: :type distance: FirstDistance :param unit: length unit :type unit: str :return: instance to FirstPace :rtype: FirstPace """ if unit is None: unit = distance.unit factor = distance.convert_to( unit=unit) # 400m with unit = mile will become ~0.25 seconds = time.total_seconds( ) / factor # 2 minutes for 400m will give ~(2*60)/0.25 return cls(minutes=int(seconds // 60), seconds=round(seconds % 60), length_unit=unit)
def test_race_type(self): try: race_type = FirstRaceType( name='marathon', distance=FirstDistance.from_string('26.219 mile')) self.assertEqual('marathon - 26.219 mile', str(race_type)) self.assertEqual( { 'distance': { 'distance': 26.219, 'unit': 'mile' }, 'name': 'marathon' }, race_type.to_json()) self.assertEqual( { 'distance': { 'distance': 42.195390336, 'unit': 'km' }, 'name': 'marathon' }, race_type.to_json(output_unit='km')) self.assertEqual('26.219 mile', race_type.distance.to_html()) self.assertEqual('42.195 km', race_type.distance.to_html(output_unit='km')) except ValueError as ex: self.fail(str(ex))
def test_json_n_html(self): try: distance = FirstDistance.from_string(string='1 mile') self.assertEqual({'distance': 1.0, 'unit': 'mile'}, distance.to_json()) self.assertEqual({'distance': 1.609344, 'unit': 'km'}, distance.to_json(output_unit='km')) self.assertEqual('1.000 mile', distance.to_html()) self.assertEqual('1.609 km', distance.to_html(output_unit='km')) except ValueError as ex: self.fail(ex)
def test_from_string(self): try: d1 = FirstDistance.from_string('1 mile') self.assertEqual('1.0 mile', str(d1)) except ValueError as ex: self.fail(ex) try: d2 = FirstDistance.from_string('onlyonetoken') self.fail('from_string is expected to fail with only one token') except ValueError as ex: self.assertEqual( 'FirstDistance.from_string - from_string() ' + 'expects 2 tokens, number and unit, but got "onlyonetoken"', str(ex)) try: d3 = FirstDistance.from_string('34t papa') self.fail( 'from_string is expected to fail if the first token is not a number' ) except ValueError as ex: message = ( 'FirstDistance.from_string - ' + 'expects the first token to be a number but invalid literal for float(): 34t' ) self.assertEqual(message, str(ex)) try: d4 = FirstDistance.from_string('-35.2 papa') self.fail( 'FirstDistance is expected to fail if the number is negative') except ValueError as ex: self.assertEqual( 'FirstDistance.__init__ - -35.2 is not a positive number', str(ex)) try: d5 = FirstDistance.from_string('42.2 papa') self.fail('FirstDistance is expected to fail for invalid unit') except ValueError as ex: self.assertEqual( 'FirstDistance.__init__ - "papa" is not a valid unit', str(ex))
def __init__(self, name, distance, unit): """ Constructor :param name: like Marathon :type name: str :param distance: distance value :type distance: float :param unit: length unit :type unit: str :return: instance of FirstRaceType :rtype: FirstRaceType """ where_am_i = 'FirstRace.__init__' if not FirstDistance.is_valid_unit(unit): raise ValueError(where_am_i + ' - "%1s" is not a valid length unit' % unit) self.name = name self.distance = FirstDistance(distance=distance, unit=unit)
def to_html(self, output_unit: Union[str, None] = None) -> str: if output_unit and output_unit != self.length_unit: dist = FirstDistance(1.0, output_unit) seconds_time = self.to_time(dist, 'second') output_pace = FirstPace(seconds=round(seconds_time), length_unit=output_unit) return '{} min per {}'.format(str(output_pace.time), output_unit) else: return '{} min per {}'.format(str(self.time), self.length_unit)
def from_instructions(cls, instructions: str, data: FirstData, time_index: int, rp: FirstPace): """ Create a step from an instruction string :param instructions: instruction string :type instructions: str :param data: First database :type data: FirstData :param time_index: the index in the paces table :type time_index: int :param rp: race pace :type rp: FirstPace :return: the step :rtype: FirstStep """ segment_name = instructions.split('@')[0] increment = None pace = None duration = None try: segment = data.segment_by_name(segment_name) distance = segment.distance except KeyError: distance = FirstDistance.from_string(segment_name) segment_name = instructions.split('@')[1] pace_list = segment_name.split('+') segment_name = pace_list[0] if len(pace_list) > 1: increment = int(pace_list[1]) else: increment = None if segment_name == 'RP': # special case for race-pace segment = None pace = FirstPace.copy(rp) else: segment = data.segment_by_name(segment_name) if segment is not None: if segment.ref_pace_name is not None: segment_name = segment.ref_pace_name segment_index = data.segment_index_by_name( segment_name) + 1 # +1 since the first column is the ref time pace = data.segments_paces[time_index][segment_index] duration = segment.duration if increment is not None: pace.increment(increment) return cls(name=instructions, pace=pace, time=duration, distance=distance)
def __get_segments_json(self, json_segments: Dict) -> None: self.reference_race = json_segments['reference_race'] index = 0 for segment_type in json_segments['segment_types']: name = segment_type['name'] type_str = segment_type['type'] ref_pace = segment_type['ref_pace_name'] if type_str == 'DISTANCE': distance = FirstDistance.from_string( string=segment_type['distance']) self.segments.append(FirstSegment(name=name, distance=distance)) elif type_str == 'TIME': duration = FirstTime.from_string(string=segment_type['time']) self.segments.append( FirstSegment(name=name, duration=duration, ref_pace_name=ref_pace)) else: # PACE self.segments.append( FirstSegment(name=name, ref_pace_name=ref_pace)) self.segments_lookup[name] = index index += 1 pace_unit = json_segments['pace_unit'] distance_unit = pace_unit.split()[-1] for line in json_segments['paces']: items = line.split() paces_list = [FirstTime.from_string(items[0])] index = 0 for pace_str in items[1:]: time_str = '0:{}'.format(pace_str) ref_segment = self.segments[index] if ref_segment.get_type() == 'distance': cur_time = FirstTime.from_string(time_str) cur_distance = ref_segment.distance paces_list.append( FirstPace.from_time_distance(time=cur_time, distance=cur_distance, unit=distance_unit)) elif ref_segment.get_type() == 'pace': paces_list.append( FirstPace.from_string('{} {}'.format( time_str, pace_unit))) else: raise ValueError( 'Duration segments have already a reference pace') index += 1 self.segments_paces.append(paces_list)
def test_to_string(self): ws1 = [0, 2, 5] ws2 = [1, 3, 6] try: # name only p1 = FirstPlan(name='My first marathon training plan', weekly_schedule=ws1) self.assertEqual('Training Plan:\nName - "My first marathon training plan"\nWorkout days: Mon, Wed, Sat\n', str(p1)) file_name = 'cmp_plan1.tcx' with open('{}/{}'.format(Config.TEST_RESOURCE_DIR, file_name), 'r') as from_file: cmp_string = from_file.read() self.assertEqual(cmp_string, p1.tcx()) cmp_json = {"name": "My first marathon training plan", "weekly_schedule": ["mon", "wed", "sat"], "workouts": []} self.assertEqual(cmp_json, p1.to_json()) cmp_html = ('<!DOCTYPE html>\n' + '<html>\n' + ' <head>\n' + ' </head>\n' + ' <body>\n' + ' <h1>Training Plan: My first marathon training plan</h1>\n' + ' <div>\n' + ' <h2>\n' + ' Schedule:\n' + ' </h2>\n' + ' </div>\n' + ' </body>\n' + '</html>') self.assertEqual(cmp_html, p1.to_html()) except TypeError as tex: self.fail(str(tex)) except ValueError as vex: self.fail(str(vex)) rt1 = FirstRaceType(name='Marathon', distance=FirstDistance.from_string('42.195 km')) rd1 = date(year=2017, month=7, day=29) r1 = FirstRace(name='SFM', race_type=rt1, race_date=rd1) rn1 = FirstRunner(name='DBD') try: # all p2 = FirstPlan(name='My first marathon training plan', weekly_schedule=ws2, race=r1, runner=rn1) cmp_string = ('Training Plan:\nName - "My first marathon training plan"\nWorkout days: Tue, Thu, Sun\n' + 'Race:\n Name - "SFM" of type Marathon - 42.195 km\nRunner:\n Name - "DBD"\n') self.assertEqual(cmp_string, str(p2)) except TypeError as tex: self.fail(str(tex)) except ValueError as vex: self.fail(str(vex))
def meters_per_second_delta(self, delta_in_seconds: int) -> float: """ Convert to speed in m/s for tcx with delta for tolerance :param delta_in_seconds: :type delta_in_seconds: int :return: calculated speed in m/s :rtype: float """ seconds = self.time.total_seconds() + delta_in_seconds meters = FirstDistance(distance=1.0, unit=self.length_unit).convert_to(unit='m') return meters / seconds
def __init__(self, name, age=None, gender=None, email=None, length_unit='mile'): """ Constructor :param name: :type name: str :param age: years only :type age: int :param gender: for now anything :type gender: str :param email: Should be a valid email address :type email: str :param length_unit: preferred and valid length unit :type length_unit: str """ where_am_i = 'FirstRunner.__init__' if not isinstance(name, basestring): raise TypeError(where_am_i + ' - name must be a string') if age is not None: if not isinstance(age, int): raise TypeError(where_am_i + ' - age must be an integer') if age <= 0: raise ValueError(where_am_i + ' - age must be positive') if gender is not None: if not isinstance(gender, basestring): raise TypeError(where_am_i + ' - gender must be a string') # for now no limit on gender but if the plan has gender related instructions then we might post a warning # when a gender is not recognized by the plan if not isinstance(length_unit, basestring): raise TypeError(where_am_i + ' - length_unit is expected to be a string') if not FirstDistance.is_valid_unit(length_unit): raise ValueError(where_am_i + ' - length unit not recognized') if FirstUtils.is_internet_on(): from validate_email import validate_email if email is not None and not validate_email(email=email): raise ValueError(where_am_i + ' - invalid email address') self.name = name self.age = age self.gender = gender self.email = email self.length_unit = length_unit
def to_time(self, distance: FirstDistance, unit: str) -> float: """ How much time will take to run a given distance with this pace :param distance: the distance :type distance: FirstDistance :param unit: the desired unit of the result :type unit: str :return: the time value for this unit :rtype: float """ factor = distance.convert_to(unit=self.length_unit) seconds = self.time.total_seconds() * factor result_time = FirstTime(seconds=round(seconds)) return result_time.convert_to(unit=unit)
def meters_per_second_delta(self, delta_in_seconds): """ Convert to speed in m/s for tcx with delta for tolerance :param delta_in_seconds: :type delta_in_seconds: int :return: """ where_am_i = 'FirstPace.meters_per_second_delta' if not isinstance(delta_in_seconds, int): raise ValueError(where_am_i + ' - delta_in_seconds must be an integer') seconds = self.time.total_seconds() + delta_in_seconds meters = FirstDistance(distance=1.0, unit=self.length_unit).convert_to(unit='m') return meters / seconds
def to_json(self, output_unit: Union[str, None] = None) -> Dict: if output_unit and output_unit != self.length_unit: dist = FirstDistance(1.0, output_unit) seconds_time = self.to_time(dist, 'second') output_pace = FirstPace(seconds=round(seconds_time), length_unit=output_unit) return { 'pace': str(output_pace), 'length_unit': output_unit, 'time': output_pace.time.to_json() } else: return { 'pace': str(self), 'length_unit': self.length_unit, 'time': self.time.to_json() }
def __init__(self, minutes=0, seconds=0, length_unit='mile'): """ Constructor :param minutes: :type minutes: int :param seconds: :type seconds: int :param length_unit: :type length_unit: str :return: instance of FirstPace :rtype: FirstPace """ where_am_i = 'FirstPace.__init__' if not FirstDistance.is_valid_unit(length_unit): raise ValueError(where_am_i + ' - "%1s" is not a valid length unit' % length_unit) self.time = FirstTime(minutes=minutes, seconds=seconds) self.length_unit = length_unit
def __init__(self, json_path: str): """ Constructor :return: instance of FirstData :rtype: FirstData """ self.race_types = [] self.race_times = [] self.segments = [] self.segments_lookup = {} self.reference_race = None self.segments_paces = [] self.plan_instructions = [] if json_path is not None: with open(json_path, 'r') as fd: data_dict = json.load(fd) self.name = data_dict['name'] self.note = data_dict['note'] self.race_types = [ FirstRaceType(name=race['name'], distance=FirstDistance.from_string( race['distance'])) for race in data_dict['races'] ] self.race_times = [[ FirstTime.from_string(string=time) for time in times ] for times in data_dict['equivalent_times']] self.__get_segments_json(json_segments=data_dict['segments']) self.plan_instructions = [ PlanInstructions( name=plan['name'], race_name=plan['race_name'], instructions=[line for line in plan['instructions']]) for plan in data_dict['workout_instructions'] ] else: raise ValueError('json_path should point to an existing file')
def from_string(cls, str_input): """ Constructor: Instantiate FirstPace from a string input :param str_input: format - '0:MM:SS per unit' :type str_input: str :return: instance of FirstPace :rtype: FirstPace """ where_am_i = 'FirstPace.from_string' tokens = str_input.split() p_time = FirstTime.from_string(tokens[0]) # pass the exception on length_unit = tokens[-1] if not FirstDistance.is_valid_unit(unit=tokens[-1]): raise ValueError(where_am_i + ' - "%1s" is not a valid length unit' % length_unit) return cls(minutes=p_time.seconds//60, seconds=p_time.seconds % 60, length_unit=length_unit)
def __init__(self, minutes: int = 0, seconds: int = 0, length_unit: str = 'mile'): """ Constructor :param minutes: :type minutes: int :param seconds: :type seconds: int :param length_unit: :type length_unit: str :return: instance of FirstPace :rtype: FirstPace """ if not FirstDistance.is_valid_unit(unit=length_unit): raise ValueError( '"{}" is not a valid length unit'.format(length_unit)) self.time = FirstTime(minutes=minutes, seconds=seconds) self.length_unit = length_unit
def test_convert_to(self): try: d1 = FirstDistance(distance=5, unit='km') except ValueError as ex: self.fail(ex) try: result = d1.convert_to(unit='mm') self.fail('convert_to is expected to fail with unknown unit') except ValueError as ex: self.assertEqual( 'FirstDistance.convert_to - mm is not a valid unit', str(ex)) try: result = d1.convert_to(unit='km') self.assertEqual(5, result) except ValueError as ex: self.fail(ex) try: result = d1.convert_to(unit='m') self.assertEqual(5000, result) except ValueError as ex: self.fail(ex) try: result = d1.convert_to(unit='ft') self.assertEqual('16404.20', '{:.2f}'.format(result)) except ValueError as ex: self.fail(ex) try: result = d1.convert_to(unit='mile') self.assertEqual('3.11', '{:.2f}'.format(result)) except ValueError as ex: self.fail(ex)
def test_to_string(self): try: d1 = FirstDistance(distance=300, unit='mm') self.fail( 'FirstDistance is expected to fail for wrong length unit') except ValueError as ex: self.assertEqual( 'FirstDistance.__init__ - "mm" is not a valid unit', str(ex)) try: d1 = FirstDistance(distance=-300, unit='m') self.fail('FirstDistance is expected to fail for negative length') except ValueError as ex: self.assertEqual( 'FirstDistance.__init__ - -300 is not a positive number', str(ex)) try: d1 = FirstDistance(distance=300, unit='m') self.assertEqual('300 m', str(d1)) except ValueError as ex: self.fail(ex) try: d1 = FirstDistance(distance=5, unit='km') self.assertEqual('5 km', str(d1)) except ValueError as ex: self.fail(ex) try: d1 = FirstDistance(distance=26.2, unit='mile') self.assertEqual('26.2 mile', str(d1)) except ValueError as ex: self.fail(ex) try: d1 = FirstDistance(distance=5280, unit='ft') self.assertEqual('5280 ft', str(d1)) except ValueError as ex: self.fail(ex)
def test_race(self): rt1 = FirstRaceType(name='5K', distance=FirstDistance.from_string('5.0 km')) rd1 = date(year=2017, month=7, day=29) tt1 = FirstTime.from_string(string='0:25:30') tt2 = FirstTime.from_string(string='0:24:34') try: # positive race = FirstRace(race_type=rt1, race_date=rd1, name='Martial Cottle Park 5K', target_time=tt1) cmp_string = ('Martial Cottle Park 5K of type ' + str(rt1) + '\n' + 'On ' + str(rd1) + '\n' + 'Target time - ' + str(tt1) + '\n' + 'Status - scheduled\n') self.assertEqual(cmp_string, str(race)) cmp_json = { 'Name': 'Martial Cottle Park 5K', 'race_date': '2017-07-29', 'race_type': { 'distance': { 'distance': 5.0, 'unit': 'km' }, 'name': '5K' }, 'status': 'scheduled', 'target_time': { 'seconds': 1530, 'time': '0:25:30' } } self.assertEqual(cmp_json, race.to_json()) cmp_json = { 'Name': 'Martial Cottle Park 5K', 'race_date': '2017-07-29', 'race_type': { 'distance': { 'distance': 3.10686, 'unit': 'mile' }, 'name': '5K' }, 'status': 'scheduled', 'target_time': { 'seconds': 1530, 'time': '0:25:30' } } FirstUtils.assert_deep_almost_equal( self, cmp_json, race.to_json(output_unit='mile'), 5) cmp_html = ( '<div>\n' + ' <h2>Race:</h2>\n' + ' <table style="border-spacing: 15px 0">\n' + ' <tbody>\n' + ' <tr>\n' + ' <td>Name:</td>\n' + ' <td><b>Martial Cottle Park 5K</b></td>\n' + ' </tr>\n' + ' <tr>\n' + ' <td>Type:</td>\n' + ' <td><b>5K</b></td>\n' + ' </tr>\n' + ' <tr>\n' + ' <td>Distance:</td>\n' + ' <td><b>5.000 km</b></td>\n' + ' </tr>\n' + ' <tr>\n' + ' <td>Date:</td>\n' + ' <td><b>2017-07-29</b></td>\n' + ' </tr>\n' + ' <tr>\n' + ' <td>Target time:</td>\n' + ' <td><b>0:25:30</b></td>\n' + ' </tr>\n' + ' <tr>\n' + ' <td>Status:</td>\n' + ' <td><b>scheduled</b></td>\n' + ' </tr>\n' + ' </tbody>\n' + ' </table>\n' + '</div>') self.assertEqual(cmp_html, race.to_html().indented_str()) cmp_html = ( '<div>\n' + ' <h2>Race:</h2>\n' + ' <table style="border-spacing: 15px 0">\n' + ' <tbody>\n' + ' <tr>\n' + ' <td>Name:</td>\n' + ' <td><b>Martial Cottle Park 5K</b></td>\n' + ' </tr>\n' + ' <tr>\n' + ' <td>Type:</td>\n' + ' <td><b>5K</b></td>\n' + ' </tr>\n' + ' <tr>\n' + ' <td>Distance:</td>\n' + ' <td><b>3.107 mile</b></td>\n' + ' </tr>\n' + ' <tr>\n' + ' <td>Date:</td>\n' + ' <td><b>2017-07-29</b></td>\n' + ' </tr>\n' + ' <tr>\n' + ' <td>Target time:</td>\n' + ' <td><b>0:25:30</b></td>\n' + ' </tr>\n' + ' <tr>\n' + ' <td>Status:</td>\n' + ' <td><b>scheduled</b></td>\n' + ' </tr>\n' + ' </tbody>\n' + ' </table>\n' + '</div>') self.assertEqual(cmp_html, race.to_html(output_unit='mile').indented_str()) except TypeError as tex: self.fail(str(tex)) except ValueError as vex: self.fail(str(vex)) try: # add actual time race.set_status(status='done') race.set_actual_time(a_time=tt2) cmp_string = ('Martial Cottle Park 5K of type ' + str(rt1) + '\n' + 'On ' + str(rd1) + '\n' + 'Target time - ' + str(tt1) + '\n' + 'Status - done\n' + 'Actual time - ' + str(tt2) + '\n') self.assertEqual(cmp_string, str(race)) cmp_json = { 'Name': 'Martial Cottle Park 5K', 'actual_time': '0:24:34', 'race_date': '2017-07-29', 'race_type': { 'distance': { 'distance': 5.0, 'unit': 'km' }, 'name': '5K' }, 'status': 'done', 'target_time': { 'seconds': 1530, 'time': '0:25:30' } } self.assertEqual(cmp_json, race.to_json()) cmp_json = { 'Name': 'Martial Cottle Park 5K', 'actual_time': '0:24:34', 'race_date': '2017-07-29', 'race_type': { 'distance': { 'distance': 3.10686, 'unit': 'mile' }, 'name': '5K' }, 'status': 'done', 'target_time': { 'seconds': 1530, 'time': '0:25:30' } } FirstUtils.assert_deep_almost_equal( self, cmp_json, race.to_json(output_unit='mile'), 5) except TypeError as tex: self.fail(str(tex)) except ValueError as vex: self.fail(str(vex)) try: # remove target time race.set_target_time() cmp_string = ('Martial Cottle Park 5K of type ' + str(rt1) + '\n' + 'On ' + str(rd1) + '\n' + 'Status - done\n' + 'Actual time - ' + str(tt2) + '\n') self.assertEqual(cmp_string, str(race)) except TypeError as tex: self.fail(str(tex)) except ValueError as vex: self.fail(str(vex)) try: # negative race.set_status('lulu') self.fail('Should not get here with a bad status') except ValueError as ex: self.assertEqual("Status not in ['scheduled', 'done', 'skipped']", str(ex))
def test_add_workout(self): ws1 = [0, 2, 5] rt1 = FirstRaceType(name='Marathon', distance=42.195, unit='km') rd1 = date(year=2017, month=7, day=29) r1 = FirstRace(name='SFM', race_type=rt1, race_date=rd1) rn1 = FirstRunner(name='DBD') p1 = FirstPlan(name='My first marathon training plan', weekly_schedule=ws1, race=r1, runner=rn1) t_warmup = FirstTime.from_string('0:15:00') p_warmup = FirstPace.from_string('0:10:00 min per mile') s_warmup = FirstStepBody(name='Warm up', pace=p_warmup, time=t_warmup) s_repeat = FirstStepRepeat(name='repeat X 8', repeat=8) d_interval = FirstDistance.from_string('400 m') p_fast = FirstPace.from_string('0:08:00 min per mile') s_fast = FirstStepBody(name='Fast', pace=p_fast, distance=d_interval) s_repeat.add_step(s_fast) s_slow = FirstStepBody(name='Rest', pace=p_warmup, distance=d_interval) s_repeat.add_step(s_slow) t_cooldown = FirstTime.from_string('0:10:00') s_cooldown = FirstStepBody(name='Cool down', pace=p_warmup, time=t_cooldown) wo = FirstWorkout(name='Week 1 Key-run 1', workout_date=date(year=2017, month=6, day=24)) wo.add_step(step=s_warmup) wo.add_step(step=s_repeat) wo.add_step(step=s_cooldown) try: # first workout p1.add_workout(workout=wo) cmp_string = ( 'Training Plan:\nName - "My first marathon training plan"\n' + 'Workout days: Mon, Wed, Sat\nRace:\n' + ' Name - "SFM" of type Marathon - 42.195 km\nRunner:\n Name - "DBD"\nWorkouts:\n' + ' "Week 1 Key-run 1"\n Sat 2017-06-24\n scheduled\n' + 'Total 1 workouts\n') self.assertEqual(cmp_string, str(p1)) file_name = expanduser( '~/PycharmProjects/first/database/cmp_plan2.tcx') # to_file = open(file_name, 'w') # to_file.write(p1.tcx()) # to_file.close() from_file = open(file_name) cmp_string = from_file.read() from_file.close() self.assertEqual(cmp_string, p1.tcx()) except TypeError as ex: self.fail(str(ex)) try: # bad workout p1.add_workout(workout='workout') self.fail('Should not get here with bad workout') except TypeError as ex: self.assertEqual( 'FirstPlan.add_workout - workout must be an instance of FirstWorkout', str(ex))
def test_repeat(self): FirstStepBase.reset_global_id() p1 = FirstPace.from_string('0:10:00 min per mile') d1 = FirstDistance.from_string('3 mile') t1 = FirstTime.from_string('0:15:00') try: name = '3 X (3 mile @ 10 min per mile + 15 minutes @ 19 min per mile)' s1 = FirstStepRepeat(name=name, repeat=3) self.assertEqual( 'Step: "' + name + '" id = 0\ntype - repeat repeat - 3\n', str(s1)) self.assertAlmostEquals(0.0, s1.total(unit='mile'), 5) self.assertAlmostEquals(0.0, s1.total(what='time', unit='minute')) tcx_string = ( '<Step xsi:type="Repeat_t">\n' + ' <StepId>0</StepId>\n' + ' <Name>3 X (3 mile @ 10 min per mile + 15 minutes @ 19 min per mile)</Name>\n' + ' <Repetitions>3</Repetitions>\n' + '</Step>\n') self.assertEqual(tcx_string, s1.tcx()) except TypeError as tex: self.fail(str(tex)) except ValueError as vex: self.fail(str(vex)) try: s2 = FirstStepBody(name='3 mile @ 10 min per mile', pace=p1, distance=d1) s3 = FirstStepBody(name='15 minutes @ 19 min per mile', pace=p1, time=t1) s1.add_step(s2) s1.add_step(s3) short = 'Step: "' + name + '" id = 0\ntype - repeat repeat - 3\n' self.assertEqual(short, str(s1)) detail = 'Step: "3 X (3 mile @ 10 min per mile + 15 minutes @ 19 min per mile)"\n' +\ ' Step: "3 mile @ 10 min per mile"\n' +\ ' 3.0 mile at 0:10:00 min per mile\n' +\ ' Step: "15 minutes @ 19 min per mile"\n' +\ ' 0:15:00 at 0:10:00 min per mile\n' self.assertEqual(detail, s1.details()) self.assertAlmostEquals(13.5, s1.total(unit='mile'), 5) self.assertAlmostEquals(135.0, s1.total(what='time', unit='minute')) tcx_string = ( ' <Step xsi:type="Repeat_t">\n' + ' <StepId>0</StepId>\n' + ' <Name>3 X (3 mile @ 10 min per mile + 15 minutes @ 19 min per mile)</Name>\n' + ' <Repetitions>3</Repetitions>\n' + ' <Child xsi:type="Step_t">\n' + ' <StepId>1</StepId>\n' + ' <Name>3 mile @ 10 min per mile</Name>\n' + ' <Duration xsi:type="Distance_t">\n' + ' <Meters>4828</Meters>\n' + ' </Duration>\n' + ' <Intensity>Active</Intensity>\n' + ' <Target xsi:type="Speed_t">\n' + ' <SpeedZone xsi:type="CustomSpeedZone_t">\n' + ' <LowInMetersPerSecond>2.6382689</LowInMetersPerSecond>\n' + ' <HighInMetersPerSecond>2.7277017</HighInMetersPerSecond>\n' + ' </SpeedZone>\n' + ' </Target>\n' + ' </Child>\n' + ' <Child xsi:type="Step_t">\n' + ' <StepId>2</StepId>\n' + ' <Name>15 minutes @ 19 min per mile</Name>\n' + ' <Duration xsi:type="Time_t">\n' + ' <Seconds>900</Seconds>\n' + ' </Duration>\n' + ' <Intensity>Active</Intensity>\n' + ' <Target xsi:type="Speed_t">\n' + ' <SpeedZone xsi:type="CustomSpeedZone_t">\n' + ' <LowInMetersPerSecond>2.6382689</LowInMetersPerSecond>\n' + ' <HighInMetersPerSecond>2.7277017</HighInMetersPerSecond>\n' + ' </SpeedZone>\n' + ' </Target>\n' + ' </Child>\n' ' </Step>\n') self.assertEqual(tcx_string, s1.tcx(indent=' ', delta_seconds=10)) except TypeError as tex: self.fail(str(tex)) except ValueError as vex: self.fail(str(vex)) try: # bad repeat type dummy = FirstStepRepeat(name='bla', repeat='3') self.fail('Should not get here with bad repeat type') except TypeError as ex: self.assertEqual( 'FirstStepRepeat.__init__ - repeat must be an integer', str(ex)) try: # negative repeat dummy = FirstStepRepeat(name='bla', repeat=-3) self.fail('Should not get here with negative repeat value') except ValueError as ex: self.assertEqual( 'FirstStepRepeat.__init__ - repeat must be greater than 0', str(ex)) try: # bad child step type s1.add_step('bad step') self.fail('Should not get here with bad step type') except TypeError as ex: self.assertEqual( 'FirstStepRepeat.add_step - step must be an instance of FirstStepBase', str(ex))
def test_body(self): FirstStepBase.reset_global_id() p1 = FirstPace.from_string('0:10:00 min per mile') d1 = FirstDistance.from_string('3 mile') t1 = FirstTime.from_string('0:15:00') try: # distance step s1 = FirstStepBody(name='3 miles @ 10 minutes per mile', pace=p1, distance=d1) cmp_string = 'Step: "3 miles @ 10 minutes per mile" id = 0\n' + \ 'type - body pace - 0:10:00 min per mile\nDistance - 3.0 mile\n' self.assertEqual(cmp_string, str(s1)) self.assertAlmostEquals(3.0, s1.total(unit='mile'), 5) self.assertAlmostEquals(4.828032, s1.total(unit='km'), 5) self.assertAlmostEquals(30.0, s1.total(what='time', unit='minute'), 5) self.assertAlmostEquals(0.5, s1.total(what='time', unit='hour'), 5) tcx_string = ( '<Step xsi:type="Step_t">\n' + ' <StepId>0</StepId>\n' + ' <Name>3 miles @ 10 minutes per mile</Name>\n' + ' <Duration xsi:type="Distance_t">\n' + ' <Meters>4828</Meters>\n' + ' </Duration>\n' + ' <Intensity>Active</Intensity>\n' + ' <Target xsi:type="Speed_t">\n' + ' <SpeedZone xsi:type="CustomSpeedZone_t">\n' + ' <LowInMetersPerSecond>2.6600727</LowInMetersPerSecond>\n' + ' <HighInMetersPerSecond>2.7047798</HighInMetersPerSecond>\n' + ' </SpeedZone>\n' + ' </Target>\n' + '</Step>\n') self.assertEqual(tcx_string, s1.tcx()) # no indent tcx_string = ( ' <Step xsi:type="Step_t">\n' + ' <StepId>0</StepId>\n' + ' <Name>3 miles @ 10 minutes per mile</Name>\n' + ' <Duration xsi:type="Distance_t">\n' + ' <Meters>4828</Meters>\n' + ' </Duration>\n' + ' <Intensity>Active</Intensity>\n' + ' <Target xsi:type="Speed_t">\n' + ' <SpeedZone xsi:type="CustomSpeedZone_t">\n' + ' <LowInMetersPerSecond>2.6688955</LowInMetersPerSecond>\n' + ' <HighInMetersPerSecond>2.6957186</HighInMetersPerSecond>\n' + ' </SpeedZone>\n' + ' </Target>\n' + ' </Step>\n') self.assertEqual(tcx_string, s1.tcx(indent=' ', delta_seconds=3)) # with indent except TypeError as tex: self.fail(str(tex)) except ValueError as vex: self.fail(str(vex)) try: # time step s1 = FirstStepBody(name='15 minutes @ 10 minutes per mile', pace=p1, time=t1) cmp_string = 'Step: "15 minutes @ 10 minutes per mile" id = 1\n' + \ 'type - body pace - 0:10:00 min per mile\nTime - 0:15:00\n' self.assertEqual(cmp_string, str(s1)) self.assertAlmostEquals(15.0, s1.total(what='time', unit='minute'), 5) self.assertAlmostEquals(7920.0, s1.total(unit='ft')) tcx_string = ( '<Step xsi:type="Step_t">\n' + ' <StepId>1</StepId>\n' + ' <Name>15 minutes @ 10 minutes per mile</Name>\n' + ' <Duration xsi:type="Time_t">\n' + ' <Seconds>900</Seconds>\n' + ' </Duration>\n' + ' <Intensity>Active</Intensity>\n' + ' <Target xsi:type="Speed_t">\n' + ' <SpeedZone xsi:type="CustomSpeedZone_t">\n' + ' <LowInMetersPerSecond>2.6600727</LowInMetersPerSecond>\n' + ' <HighInMetersPerSecond>2.7047798</HighInMetersPerSecond>\n' + ' </SpeedZone>\n' + ' </Target>\n' + '</Step>\n') self.assertEqual(tcx_string, s1.tcx()) except TypeError as tex: self.fail(str(tex)) except ValueError as vex: self.fail(str(vex)) try: # bad name type dummy = FirstStepBody(name=123, pace=p1, time=t1) self.fail('Should not get here with bad name') except TypeError as ex: self.assertEqual('FirstStepBase.__init__ - name must be a string', str(ex)) try: # bad pace dummy = FirstStepBody(name='dummy', pace='bad pace type', time=t1) self.fail('Should not get here with bad pace') except TypeError as ex: self.assertEqual( 'FirstStepBody.__init__ - pace must be an instance of FirstPace', str(ex)) try: # no distance and no time dummy = FirstStepBody(name='dummy', pace=p1) self.fail('Should not get here with neither distance nor duration') except ValueError as ex: self.assertEqual( 'FirstStepBody.__init__ - Either distance or time must have a value', str(ex)) try: # bad distance dummy = FirstStepBody(name='dummy', pace=p1, distance=123.45) self.fail('Should not get here with bad distance') except TypeError as ex: self.assertEqual( 'FirstStepBody.__init__ - distance must be an instance of FirstDistance', str(ex)) try: # bad time dummy = FirstStepBody(name='dummy', pace=p1, time=987.65) self.fail('Should not get here with bad time') except TypeError as ex: self.assertEqual( 'FirstStepBody.__init__ - time must be an instance of FirstTime', str(ex)) try: # both distance and time dummy = FirstStepBody(name='dummy', pace=p1, distance=d1, time=t1) self.fail('Should not get here with both distance and time') except ValueError as ex: self.assertEqual( 'FirstStepBody.__init__ - cannot set both distance and duration in the same step', str(ex))
def __parse_segments(self, segment_def, ref_race, pace_unit, segment_paces): where_am_i = 'FirstData.__parse_segments' index = 0 for segment in segment_def: name = segment[0].text segment_type = segment[1].text distance = None duration = None ref_pace_name = None if segment_type == 'DISTANCE': distance = FirstDistance(distance=float(segment[2][0].text), unit=segment[2][1].text) elif segment_type == 'TIME': duration = FirstTime(seconds=int(segment[2][0].text), minutes=int(segment[2][1].text), hours=int(segment[2][2].text)) ref_pace_name = segment[3].text else: # PACE ref_pace_name = segment[2].text self.segments.append( FirstSegment(name=name, distance=distance, duration=duration, ref_pace_name=ref_pace_name)) self.segments_lookup[name] = index index += 1 self.reference_race = ref_race.text dist_unit = pace_unit.text.split()[-1] for line in segment_paces: paces_list = [] first = True index = 0 for value in line.text.split(): if first: paces_list.append(FirstTime.from_string(value)) first = False else: # why incrementing index makes ref_segment.get_type undefined? time_string = '0:' + value ref_segment = self.segments[index] if ref_segment.get_type() == 'distance': cur_time = FirstTime.from_string(time_string) cur_dist = ref_segment.distance paces_list.append( FirstPace.from_time_distance(time=cur_time, distance=cur_dist, unit=dist_unit)) elif ref_segment.get_type() == 'pace': paces_list.append( FirstPace.from_string(time_string + ' ' + pace_unit.text)) else: raise ValueError( where_am_i + ' - Duration segments have already a reference pace' ) index = index + 1 self.segments_paces.append(paces_list)
def test_body(self): FirstStepBase.reset_global_id() pace = FirstPace.from_string(str_input='0:10:00 min per mile') distance = FirstDistance.from_string(string='3 mile') time = FirstTime.from_string(string='0:15:00') try: # distance step step_b = FirstStepBody(name='3 miles @ 10 minutes per mile', pace=pace, distance=distance) cmp_string = 'Step: "3 miles @ 10 minutes per mile" id = 0\n' + \ 'type - body pace - 0:10:00 min per mile\nDistance - 3.0 mile\n' self.assertEqual(cmp_string, str(step_b)) self.assertAlmostEqual(3.0, step_b.total(unit='mile'), 5) self.assertAlmostEqual(4.828032, step_b.total(unit='km'), 5) self.assertAlmostEqual(30.0, step_b.total(what='time', unit='minute'), 5) self.assertAlmostEqual(0.5, step_b.total(what='time', unit='hour'), 5) tcx_string = ( '<Step xsi:type="Step_t">\n' + ' <StepId>0</StepId>\n' + ' <Name>3 miles @ 10 minutes per mile</Name>\n' + ' <Duration xsi:type="Distance_t">\n' + ' <Meters>4828</Meters>\n' + ' </Duration>\n' + ' <Intensity>Active</Intensity>\n' + ' <Target xsi:type="Speed_t">\n' + ' <SpeedZone xsi:type="CustomSpeedZone_t">\n' + ' <LowInMetersPerSecond>2.6600727</LowInMetersPerSecond>\n' + ' <HighInMetersPerSecond>2.7047798</HighInMetersPerSecond>\n' + ' </SpeedZone>\n' + ' </Target>\n' + '</Step>') self.assertEqual(tcx_string, step_b.tcx().indented_str()) # no indent tcx_string = ( '<Step xsi:type="Step_t">\n' + ' <StepId>0</StepId>\n' + ' <Name>3 miles @ 10 minutes per mile</Name>\n' + ' <Duration xsi:type="Distance_t">\n' + ' <Meters>4828</Meters>\n' + ' </Duration>\n' + ' <Intensity>Active</Intensity>\n' + ' <Target xsi:type="Speed_t">\n' + ' <SpeedZone xsi:type="CustomSpeedZone_t">\n' + ' <LowInMetersPerSecond>2.6688955</LowInMetersPerSecond>\n' + ' <HighInMetersPerSecond>2.6957186</HighInMetersPerSecond>\n' + ' </SpeedZone>\n' + ' </Target>\n' + '</Step>') self.assertEqual(tcx_string, step_b.tcx(delta_seconds=3).indented_str()) cmp_json = { 'distance': { 'distance': 3.0, 'unit': 'mile' }, 'name': '3 miles @ 10 minutes per mile', 'pace': { 'length_unit': 'mile', 'pace': '0:10:00 min per mile', 'time': { 'seconds': 600, 'time': '0:10:00' } } } self.assertEqual(cmp_json, step_b.to_json()) cmp_json = { 'distance': { 'distance': 4.828032, 'unit': 'km' }, 'name': '3 miles @ 10 minutes per mile', 'pace': { 'length_unit': 'km', 'pace': '0:06:13 min per km', 'time': { 'seconds': 373, 'time': '0:06:13' } } } self.assertEqual(cmp_json, step_b.to_json(output_unit='km')) cmp_html = ( '<div style="margin-left: 20px">\n' + ' <p>\n' + ' 3 miles @ 10 minutes per mile - 3.000 mile at 0:10:00 min per mile\n' + ' </p>\n' + '</div>') self.assertEqual(cmp_html, step_b.to_html().indented_str()) cmp_html = ( '<div style="margin-left: 20px">\n' + ' <p>\n' + ' 3 miles @ 10 minutes per mile - 4.828 km at 0:06:13 min per km\n' + ' </p>\n' + '</div>') self.assertEqual(cmp_html, step_b.to_html(output_unit='km').indented_str()) except TypeError as tex: self.fail(str(tex)) except ValueError as vex: self.fail(str(vex)) try: # time step step_b = FirstStepBody(name='15 minutes @ 10 minutes per mile', pace=pace, time=time) cmp_string = 'Step: "15 minutes @ 10 minutes per mile" id = 1\n' + \ 'type - body pace - 0:10:00 min per mile\nTime - 0:15:00\n' self.assertEqual(cmp_string, str(step_b)) self.assertAlmostEqual(15.0, step_b.total(what='time', unit='minute'), 5) self.assertAlmostEqual(7920.0, step_b.total(unit='ft')) tcx_string = ( '<Step xsi:type="Step_t">\n' + ' <StepId>1</StepId>\n' + ' <Name>15 minutes @ 10 minutes per mile</Name>\n' + ' <Duration xsi:type="Time_t">\n' + ' <Seconds>900</Seconds>\n' + ' </Duration>\n' + ' <Intensity>Active</Intensity>\n' + ' <Target xsi:type="Speed_t">\n' + ' <SpeedZone xsi:type="CustomSpeedZone_t">\n' + ' <LowInMetersPerSecond>2.6600727</LowInMetersPerSecond>\n' + ' <HighInMetersPerSecond>2.7047798</HighInMetersPerSecond>\n' + ' </SpeedZone>\n' + ' </Target>\n' + '</Step>') self.assertEqual(tcx_string, step_b.tcx().indented_str()) cmp_json = { 'name': '15 minutes @ 10 minutes per mile', 'pace': { 'length_unit': 'mile', 'pace': '0:10:00 min per mile', 'time': { 'seconds': 600, 'time': '0:10:00' } }, 'time': { 'seconds': 900, 'time': '0:15:00' } } self.assertEqual(cmp_json, step_b.to_json()) cmp_json = { 'name': '15 minutes @ 10 minutes per mile', 'pace': { 'length_unit': 'km', 'pace': '0:06:13 min per km', 'time': { 'seconds': 373, 'time': '0:06:13' } }, 'time': { 'seconds': 900, 'time': '0:15:00' } } self.assertEqual(cmp_json, step_b.to_json(output_unit='km')) cmp_html = ( '<div style="margin-left: 20px">\n' + ' <p>\n' + ' 15 minutes @ 10 minutes per mile - 0:15:00 at 0:10:00 min per mile\n' + ' </p>\n' + '</div>') self.assertEqual(cmp_html, step_b.to_html().indented_str()) cmp_html = ( '<div style="margin-left: 20px">\n' + ' <p>\n' + ' 15 minutes @ 10 minutes per mile - 0:15:00 at 0:06:13 min per km\n' + ' </p>\n' + '</div>') self.assertEqual(cmp_html, step_b.to_html(output_unit='km').indented_str()) except TypeError as tex: self.fail(str(tex)) except ValueError as vex: self.fail(str(vex)) try: # no distance and no time _ = FirstStepBody(name='dummy', pace=pace) self.fail('Should not get here with neither distance nor duration') except ValueError as ex: self.assertEqual('Either distance or time must have a value', str(ex)) try: # both distance and time _ = FirstStepBody(name='dummy', pace=pace, distance=distance, time=time) self.fail('Should not get here with both distance and time') except ValueError as ex: self.assertEqual( 'Cannot set both distance and duration in the same step', str(ex))
def test_repeat(self): FirstStepBase.reset_global_id() pace = FirstPace.from_string(str_input='0:10:00 min per mile') distance = FirstDistance.from_string(string='3 mile') time = FirstTime.from_string(string='0:15:00') try: name = '3 X (3 mile @ 10 min per mile + 15 minutes @ 19 min per mile)' step_r = FirstStepRepeat(name=name, repeat=3) self.assertEqual( 'Step: "' + name + '" id = 0\ntype - repeat repeat - 3\n', str(step_r)) self.assertAlmostEqual(0.0, step_r.total(unit='mile'), 5) self.assertAlmostEqual(0.0, step_r.total(what='time', unit='minute')) tcx_string = ( '<Step xsi:type="Repeat_t">\n' + ' <StepId>0</StepId>\n' + ' <Name>3 X (3 mile @ 10 min per mile + 15 minutes @ 19 min per mile)</Name>\n' + ' <Repetitions>3</Repetitions>\n' + '</Step>') self.assertEqual(tcx_string, step_r.tcx().indented_str()) except TypeError as tex: self.fail(str(tex)) except ValueError as vex: self.fail(str(vex)) try: step_b1 = FirstStepBody(name='3 mile @ 10 min per mile', pace=pace, distance=distance) step_b2 = FirstStepBody(name='15 minutes @ 19 min per mile', pace=pace, time=time) step_r.add_step(step_b1) step_r.add_step(step_b2) short = 'Step: "' + name + '" id = 0\ntype - repeat repeat - 3\n' self.assertEqual(short, str(step_r)) detail = 'Step: "3 X (3 mile @ 10 min per mile + 15 minutes @ 19 min per mile)"\n' + \ ' Step: "3 mile @ 10 min per mile"\n' + \ ' 3.0 mile at 0:10:00 min per mile\n' + \ ' Step: "15 minutes @ 19 min per mile"\n' + \ ' 0:15:00 at 0:10:00 min per mile\n' self.assertEqual(detail, step_r.details()) self.assertAlmostEqual(13.5, step_r.total(unit='mile'), 5) self.assertAlmostEqual(135.0, step_r.total(what='time', unit='minute')) tcx_string = ( '<Step xsi:type="Repeat_t">\n' + ' <StepId>0</StepId>\n' + ' <Name>3 X (3 mile @ 10 min per mile + 15 minutes @ 19 min per mile)</Name>\n' + ' <Repetitions>3</Repetitions>\n' + ' <Child xsi:type="Step_t">\n' + ' <StepId>1</StepId>\n' + ' <Name>3 mile @ 10 min per mile</Name>\n' + ' <Duration xsi:type="Distance_t">\n' + ' <Meters>4828</Meters>\n' + ' </Duration>\n' + ' <Intensity>Active</Intensity>\n' + ' <Target xsi:type="Speed_t">\n' + ' <SpeedZone xsi:type="CustomSpeedZone_t">\n' + ' <LowInMetersPerSecond>2.6382689</LowInMetersPerSecond>\n' + ' <HighInMetersPerSecond>2.7277017</HighInMetersPerSecond>\n' + ' </SpeedZone>\n' + ' </Target>\n' + ' </Child>\n' + ' <Child xsi:type="Step_t">\n' + ' <StepId>2</StepId>\n' + ' <Name>15 minutes @ 19 min per mile</Name>\n' + ' <Duration xsi:type="Time_t">\n' + ' <Seconds>900</Seconds>\n' + ' </Duration>\n' + ' <Intensity>Active</Intensity>\n' + ' <Target xsi:type="Speed_t">\n' + ' <SpeedZone xsi:type="CustomSpeedZone_t">\n' + ' <LowInMetersPerSecond>2.6382689</LowInMetersPerSecond>\n' + ' <HighInMetersPerSecond>2.7277017</HighInMetersPerSecond>\n' + ' </SpeedZone>\n' + ' </Target>\n' + ' </Child>\n' '</Step>') self.assertEqual(tcx_string, step_r.tcx(delta_seconds=10).indented_str()) cmp_json = { 'name': '3 X (3 mile @ 10 min per mile + 15 minutes @ 19 min per mile)', 'repeat': 3, 'steps': [{ 'distance': { 'distance': 3.0, 'unit': 'mile' }, 'name': '3 mile @ 10 min per mile', 'pace': { 'length_unit': 'mile', 'pace': '0:10:00 min per mile', 'time': { 'seconds': 600, 'time': '0:10:00' } } }, { 'name': '15 minutes @ 19 min per mile', 'pace': { 'length_unit': 'mile', 'pace': '0:10:00 min per mile', 'time': { 'seconds': 600, 'time': '0:10:00' } }, 'time': { 'seconds': 900, 'time': '0:15:00' } }] } self.assertEqual(cmp_json, step_r.to_json()) cmp_json = { 'name': '3 X (3 mile @ 10 min per mile + 15 minutes @ 19 min per mile)', 'repeat': 3, 'steps': [{ 'distance': { 'distance': 4.828032, 'unit': 'km' }, 'name': '3 mile @ 10 min per mile', 'pace': { 'length_unit': 'km', 'pace': '0:06:13 min per km', 'time': { 'seconds': 373, 'time': '0:06:13' } } }, { 'name': '15 minutes @ 19 min per mile', 'pace': { 'length_unit': 'km', 'pace': '0:06:13 min per km', 'time': { 'seconds': 373, 'time': '0:06:13' } }, 'time': { 'seconds': 900, 'time': '0:15:00' } }] } self.assertEqual(cmp_json, step_r.to_json(output_unit='km')) cmp_html = ( '<div style="margin-left: 20px">\n' + ' <p>\n' + ' Repeat 3 times:\n' + ' </p>\n' + ' <div style="margin-left: 20px">\n' + ' <p>\n' + ' 3 mile @ 10 min per mile - 3.000 mile at 0:10:00 min per mile\n' + ' </p>\n' + ' </div>\n' + ' <div style="margin-left: 20px">\n' + ' <p>\n' + ' 15 minutes @ 19 min per mile - 0:15:00 at 0:10:00 min per mile\n' + ' </p>\n' + ' </div>\n' + '</div>') self.assertEqual(cmp_html, step_r.to_html().indented_str()) cmp_html = ( '<div style="margin-left: 20px">\n' + ' <p>\n' + ' Repeat 3 times:\n' + ' </p>\n' + ' <div style="margin-left: 20px">\n' + ' <p>\n' + ' 3 mile @ 10 min per mile - 4.828 km at 0:06:13 min per km\n' + ' </p>\n' + ' </div>\n' + ' <div style="margin-left: 20px">\n' + ' <p>\n' + ' 15 minutes @ 19 min per mile - 0:15:00 at 0:06:13 min per km\n' + ' </p>\n' + ' </div>\n' + '</div>') self.assertEqual(cmp_html, step_r.to_html(output_unit='km').indented_str()) except TypeError as tex: self.fail(str(tex)) except ValueError as vex: self.fail(str(vex)) try: # negative repeat _ = FirstStepRepeat(name='bla', repeat=-3) self.fail('Should not get here with negative repeat value') except ValueError as ex: self.assertEqual('repeat must be greater than 0', str(ex))