Ejemplo n.º 1
0
def get_barcode_forecast(data_frame: DataFrame, now: Arrow,
                         for_date: Arrow) -> Series:
    if not data_frame.empty:
        last_data_date = arrow.get(data_frame.index.max())

        beg_date = now.shift(months=-1)
        end_date = for_date

        real = create_df_with_zeroes(data_frame, beg_date.shift(weeks=-1),
                                     end_date)
        old = create_df_with_zeroes(data_frame, beg_date.shift(weeks=-1),
                                    end_date, lambda a: a.shift(years=-1))
        real_smoothed = smooth_df(real, beg_date, last_data_date)
        old_smoothed = smooth_df(old, beg_date, end_date)

        if not real_smoothed.empty:
            _, diff = compare_df(real_smoothed, old_smoothed,
                                 now.shift(days=1), end_date)
            result_forecast = shift_df(old_smoothed, diff, now.shift(days=1),
                                       end_date)
            return result_forecast['quantity'][now.date():for_date.date()]
        else:
            raise Exception('Empty data frame')
    else:
        raise Exception('Empty data frame')
Ejemplo n.º 2
0
def make_time_list(terminus_times: list, current_time: arrow.Arrow,
                   data_pool: str):
    """

    :param terminus_times:
    :type terminus_times:
    :param current_time:
    :type current_time:
    :param data_pool:
    :type data_pool:
    :return:
    :rtype:
    """
    time_list = []
    previous_hour = int(current_time.shift(hours=-1).format('HH'))
    current_hour = int(current_time.format('HH'))
    next_hour = int(current_time.shift(hours=1).format('HH'))
    prev_hr = find_hr(terminus_times, previous_hour)
    curr_hr = find_hr(terminus_times, current_hour)
    next_hr = find_hr(terminus_times, next_hour)
    for hour_set in [prev_hr, curr_hr, next_hr]:
        if hour_set:
            hour = hour_set.get('hour')
            minute_set = hour_set.get(data_pool)
            for minutes in minute_set:
                time_list.append(make_time(hour, minutes))
    return time_list
Ejemplo n.º 3
0
def get_weekday_after(date: arrow.Arrow, iso_weekday: int) -> arrow.Arrow:
    """
    Returns the first day with the given weekday that is equal to or greater than
    the given date.
    """
    new_date = date.shift(weekday=iso_weekday - 1)
    return new_date
Ejemplo n.º 4
0
    def DailyCallback(self, schedule_time: arrow.Arrow, fn: Callable, *args,
                      **kwargs) -> Job:
        """Schedules fn to be run once a day at schedule_time.

    The actual scheduled time is perturbed randomly +/-30s unless the kwarg
    '_jitter' is set to False.

    Args:
      schedule_time: An Arrow object specifying when to run fn.
      fn: The function to be run.
      *args: Arguments to pass to fn.
      **kwargs: Keyworded arguments to pass to fn. Special kwargs listed below:
          _jitter - {int} How many seconds to perturb scheduling time by, in
                    both directions. Defaults to 30s.

    Returns:
      APScheduler Job.
    """
        if self._local_tz:
            schedule_time = schedule_time.to(self._local_tz)
        jitter = kwargs.get('_jitter', 30)
        if jitter:
            jitter_secs = random.randint(-jitter, jitter)
            schedule_time = schedule_time.shift(seconds=jitter_secs)
        kwargs.pop('_jitter', None)

        # APScheduler 2.1.2 doesn't understand timezones.
        return self._scheduler.add_interval_job(fn,
                                                args=args,
                                                kwargs=kwargs,
                                                start_date=schedule_time.naive,
                                                days=1)
Ejemplo n.º 5
0
def apply_weekday_offset(start_time: arrow.Arrow,
                         week_start: str) -> arrow.Arrow:
    """
    Apply the offset required to move the start date `start_time` of a week
    starting on Monday to that of a week starting on `week_start`.
    """
    weekdays = dict(
        zip(
            [
                "monday",
                "tuesday",
                "wednesday",
                "thursday",
                "friday",
                "saturday",
                "sunday",
            ],
            range(0, 7),
        ))

    new_start = week_start.lower()
    if new_start not in weekdays:
        return start_time
    now = datetime.datetime.now()
    offset = weekdays[new_start] - 7 * (weekdays[new_start] > now.weekday())
    # -> Array.shift(days=)
    return start_time.shift(days=offset)
def get_date_range_with_end_date(
        statistical_interval: Optional[str],
        end_date: arrow.Arrow) -> Tuple[arrow.Arrow, arrow.Arrow]:
    # end_date = end_date.shift(days=1)
    if statistical_interval:
        if statistical_interval == "daily":
            # start = end_date.shift(days=-1)
            return end_date.floor('day'), end_date.ceil('day')
        elif statistical_interval == "monthly":
            start = end_date.shift(months=-1)
            return start.floor('day'), end_date.ceil('day')
        elif statistical_interval == "weekly":
            start = end_date.shift(weeks=-1)
            return start.floor('day'), end_date.ceil('day')
    else:
        # start = end_date.shift(days=-1)
        return end_date.floor('day'), end_date.ceil('day')
Ejemplo n.º 7
0
def exam_dates(begin: Tuple[int, int]) -> Iterable[Tuple[int, int]]:
    date = Arrow(2020, begin[0], begin[1], tzinfo=TIMEZONE)
    while True:
        yield date.date().month, date.date().day
        while True:
            date = date.shift(days=1)
            if date.isoweekday() < 6:
                break
Ejemplo n.º 8
0
def combine_date_time(date: arrow.Arrow, time: arrow.Arrow) -> arrow.Arrow:
    """
    Returns an Arrow object with the date of the first argument and the time of the second.
    """
    return date.shift(hours=time.hour,
                      minutes=time.minute,
                      seconds=time.second,
                      microseconds=time.microsecond)
Ejemplo n.º 9
0
def get_mean_forecast(data_frame: DataFrame, now: Arrow,
                      for_date: Arrow) -> Series:
    tomorrow = now.shift(days=1)
    beg = now.shift(days=-6)
    arr = []
    for day in Arrow.range('day', beg, now):
        try:
            value = data_frame.loc[day.date()].values[0]
        except KeyError:
            value = 0.0
        arr.append([day.date(), value])
    init_df = create_df_indexed_by_date(create_df(arr))
    df = create_df_with_zeroes(init_df, beg, for_date)
    for day in Arrow.range('day', tomorrow, for_date):
        yesterday = day.shift(days=-1)
        past_week = df[yesterday.shift(days=-6).date():yesterday.date()]
        df.loc[day.date()] = past_week.mean()
    return df['quantity'][tomorrow.date():for_date.date()]
Ejemplo n.º 10
0
def check_datadog(objective: models.Objective, query_datetime: arrow.Arrow) -> float:
    response = api.Metric.query(
        start=query_datetime.shift(days=-30).timestamp,
        end=query_datetime.timestamp,
        query=objective.indicator_query,
    )

    series = response["series"][0]
    point = series["pointlist"][0]
    _, point_value = point
    return round(Decimal(point_value), 4)
Ejemplo n.º 11
0
def run_captain(
    distance: float,
    ship: OurShip,
    days_spent: int,
    date: Arrow,
    initial_destination: str,
) -> str:
    """
    Simulate the entire lifetime of a captain and their ship, returning a log of their endeavours
    """
    log = ""
    destination = initial_destination

    while not ship.destroyed:
        date_already_logged = False

        travel_log, date_already_logged = do_travel(distance, ship, days_spent,
                                                    date)
        log += travel_log

        # We have arrived!
        if ship.destroyed:
            break

        if not date_already_logged:
            log += f"\n{get_date(date)}"

        location_log = do_location(
            ship,
            destination,
            days_spent,
            data.place_names,
        )
        (
            new_destination,
            new_distance,
            destination_log,
        ) = navigation.get_new_heading(
            destination,
            data.place_coords,
            ship.visited,
        )
        log += location_log
        log += destination_log

        # Re-initialise variables
        date = date.shift(days=+1)
        days_spent = 0
        destination = new_destination
        distance = new_distance
        date_already_logged = False

    return log
Ejemplo n.º 12
0
def get_category_forecast(barcode_data_frame: DataFrame,
                          category_data_frame: DataFrame, now: Arrow,
                          for_date: Arrow) -> Series:
    tomorrow = now.shift(days=1)
    past_month = now.shift(months=-1)

    barcode_df = create_df_with_zeroes(barcode_data_frame, past_month, now)
    barcode_smoothed = smooth_df(barcode_df, past_month, now)
    category_df = create_df_with_zeroes(category_data_frame, past_month,
                                        for_date, lambda a: a.shift(years=-1))
    category_smoothed = smooth_df(category_df, past_month, for_date)

    barcode_series = barcode_smoothed['quantity'][past_month.date():now.date()]
    category_series = category_smoothed['quantity'][past_month.date():now.date(
    )]
    percent, _ = compare_df(barcode_series.to_frame(),
                            category_series.to_frame(), past_month, now)
    forecast = category_smoothed['quantity'][tomorrow.date():for_date.date()]
    forecast_normalized = increase_df(forecast.to_frame(), percent, tomorrow,
                                      for_date)
    return forecast_normalized['quantity'][now.date():for_date.date()]
Ejemplo n.º 13
0
    async def clear_raid(self, guild_id, raid_cooldown_end: arrow.Arrow):
        """
        cleares the current raid ( resets redis stuff )
        sets the cleared at for the current active raid in postgres ( used for the stat upload,
        since that is bound to a specific raid )
        """
        logger.debug("clear_raid: raid_cooldown_end: %s", raid_cooldown_end)
        raid_config = await self.raid_dao.get_raid_configuration(guild_id)
        spawn_timestamp = raid_config.get(RAID_SPAWN, 0)
        cooldown_timestamp = raid_config.get(RAID_COOLDOWN, 0)

        now = arrow.utcnow()
        spawn = arrow.get(spawn_timestamp)

        if not spawn_timestamp and not cooldown_timestamp:
            raise NoRaidActive()

        if cooldown_timestamp:
            raise RaidAlreadyCleared()

        if now < spawn:
            raise RaidUnspawned()

        delta_now_cd = raid_cooldown_end - now
        logger.debug("clear_raid: time until raid cooldown ends from now %s",
                     delta_now_cd)

        # ~2 secs for delays and stuff
        raid_cooldown_start = raid_cooldown_end.shift(minutes=-59, seconds=-59)
        time_needed_to_clear = raid_cooldown_start - spawn

        logger.debug(
            "clear_raid: raid cooldown started at %s, time needed to clear raid: %s",
            raid_cooldown_start,
            time_needed_to_clear,
        )

        if spawn > raid_cooldown_start:
            logger.warning(
                "clear_raid: something went wrong :o spawn %s is after raid cooldown start %s",
                spawn,
                raid_cooldown_start,
            )
            raise ValueError("Raid cooldown must be 60m after spawn")

        await self.raid_postgres_dao.complete_last_raid_stat_entry(
            guild_id, raid_cooldown_start)

        await self._clear_current_raid_data(guild_id)
        await self.raid_dao.set_raid_cooldown(guild_id,
                                              raid_cooldown_end.timestamp)

        return time_needed_to_clear
Ejemplo n.º 14
0
def get_metrics_for_signal(
    cluster: str,
    pool: str,
    scheduler: str,
    app: str,
    metrics_client: ClustermanMetricsBotoClient,
    required_metrics: List[MetricsConfigDict],
    end_time: arrow.Arrow,
) -> MetricsValuesDict:
    """ Get the metrics required for a signal """

    metrics: MetricsValuesDict = defaultdict(list)
    for metric_dict in required_metrics:
        if metric_dict['type'] not in (SYSTEM_METRICS, APP_METRICS):
            raise MetricsError(
                f"Metrics of type {metric_dict['type']} cannot be queried by signals."
            )

        # Need to add the cluster/pool to get the right system metrics
        # TODO (CLUSTERMAN-126) this should probably be cluster/pool/app eventually
        # TODO (CLUSTERMAN-446) if a mesos pool and a k8s pool share the same app_name,
        #      APP_METRICS will be used for both
        if metric_dict['type'] == SYSTEM_METRICS:
            dims_list = [get_cluster_dimensions(cluster, pool, scheduler)]
            if scheduler == 'mesos':  # handle old (non-scheduler-aware) metrics
                dims_list.insert(0,
                                 get_cluster_dimensions(cluster, pool, None))
        else:
            dims_list = [{}]

        # We only support regex expressions for APP_METRICS
        if 'regex' not in metric_dict:
            metric_dict['regex'] = False

        start_time = end_time.shift(minutes=-metric_dict['minute_range'])
        for dims in dims_list:
            query_results = metrics_client.get_metric_values(
                metric_dict['name'],
                metric_dict['type'],
                start_time.timestamp,
                end_time.timestamp,
                is_regex=metric_dict['regex'],
                extra_dimensions=dims,
                app_identifier=app,
            )
            for metric_name, timeseries in query_results.items():
                metrics[metric_name].extend(timeseries)
                # safeguard; the metrics _should_ already be sorted since we inserted the old
                # (non-scheduler-aware) metrics before the new metrics above, so this should be fast
                metrics[metric_name].sort()
    return metrics
Ejemplo n.º 15
0
def __generate_search_url_by_day(query: str, date: Arrow):
    """
    Returns a string with a url to ask twitter for a query in a day
    :param query:str twitter advanced query string
    :param date: date to query
    :return: url for date
    """

    search_url = '%s since:%s until:%s' % (query, date.format('YYYY-MM-DD'),
                                           date.shift(
                                               days=1).format('YYYY-MM-DD'))
    search_url = 'https://mobile.twitter.com/search?q=' + urllib.parse.quote_plus(
        search_url)
    logger.debug(f"Generated url: {search_url}")
    return search_url
Ejemplo n.º 16
0
 def export_monthly(self, target_month: arrow.Arrow):
     """
     导入一个月的数据
     :param target_month: 要导入的月份
     :return:
     """
     batch_size = self.batch_size
     start_i = 0
     epoch = 0
     df = pandas.DataFrame()
     self._tmp_show_index = 0
     total_num = 0
     for batch_i in range((self.all_vins_len // batch_size) + 1):
         # 选取新的vin号查询
         end_i = (batch_i + 1) * batch_size
         self._tmp_show_index = end_i
         if end_i > self.all_vins_len:
             end_i = self.all_vins_len
         select_vins = self.all_vins[start_i:end_i]
         # 月查询
         dd, err_list = self.export(
             select_vins, [(target_month, target_month.shift(months=1))])
         df = df.append(dd)
         # 错误vin号划分为日查询
         if len(err_list) > 0:
             df = df.append(self.export_daily2month(err_list, target_month))
         # 后续处理
         df_len = len(df)
         start_i = end_i
         if df_len >= self.max_per_batch or end_i == self.all_vins_len:
             epoch += 1
             total_num += df_len
             file_path = f"{self.data_dir}{target_month.format('YYYY-MM')}/"
             if not os.path.exists(file_path):
                 os.makedirs(file_path)
             df.to_parquet(f"{file_path}{epoch}.snappy.parquet",
                           index=False)
             df = pandas.DataFrame()
             self._show_process(target_month, end_i, df_len)
             logger.info(
                 f'completed:{target_month}_{epoch},len={df_len},,process={round(self._tmp_show_index / self.all_vins_len * 100, 4)}%'
             )
             self.record_batch_ok(target_month.format('YYYY-MM'),
                                  select_vins)
     logger.info(f'completed:{target_month},total_num={total_num}')
     return total_num
Ejemplo n.º 17
0
 def addTime(self, cur: arrow.Arrow, fore_unit: int) -> arrow.Arrow:
     if fore_unit == 0:
         cur = cur.shift(years=1)
     elif fore_unit == 1:
         cur = cur.shift(months=1)
     elif fore_unit == 2:
         cur = cur.shift(days=1)
     elif fore_unit == 3:
         cur = cur.shift(hours=1)
     elif fore_unit == 4:
         cur = cur.shift(minutes=1)
     elif fore_unit == 5:
         cur = cur.shift(seconds=1)
     return cur
Ejemplo n.º 18
0
 def export_hourly2day(self, target_day: arrow.Arrow):
     """
     将一天拆成小时查询并合并
     :param vins: 需要查询的vin
     :param target_day: 需要查询的精确到天的时间
     :return: 查询的vin的目标日的数据
     """
     day_hours = list(
         target_day.range('hour', target_day, target_day.shift(days=1)))
     target_time_bucket = []
     for i in range(len(day_hours) - 1):
         target_time_bucket.append((day_hours[i], day_hours[i + 1]))
     # 查询数据
     df = pandas.DataFrame()
     for t_t, d_t in target_time_bucket:
         dd, err_list = self.export(t_t, d_t)
         df = df.append(dd)
         if len(err_list) > 0:
             self.record_err(t_t.format('YYY-MM-DD_HH'))
     return df
Ejemplo n.º 19
0
 def export_daily2month(self, vins: list, target_month: arrow.Arrow):
     """
     将目标vin号的一个月拆分为单天查询后组合返回
     :param vins: 需要查询的vin
     :param target_month: 需要查询的精确到月的时间
     :return: 查询的vin的目标月的数据
     """
     # 拼装一个月的单日时间范围
     month_days = list(
         target_month.range('day', target_month,
                            target_month.shift(months=1)))
     target_time_bucket = []
     for i in range(len(month_days) - 1):
         target_time_bucket.append((month_days[i], month_days[i + 1]))
     # 查询数据
     df = pandas.DataFrame()
     for t_t, d_t in target_time_bucket:
         dd, err_list = self.export(vins, [(t_t, d_t)])
         df = df.append(dd)
         if len(err_list) > 0:
             df = df.append(self.export_hourly2day(err_list, t_t))
     return df
Ejemplo n.º 20
0
def check_pingdom(objective: models.Objective, query_datetime: arrow.Arrow) -> float:
    api_url = (
        f"https://api.pingdom.com/api/2.1/summary.average/{objective.indicator_query}"
    )

    response = requests.get(
        api_url,
        auth=(settings.PINGDOM_EMAIL, settings.PINGDOM_PASS),
        headers={"App-Key": settings.PINGDOM_APP_KEY},
        params={
            "from": query_datetime.shift(days=-30).timestamp,
            "to": query_datetime.timestamp,
            "includeuptime": "true",
        },
    )

    response.raise_for_status()

    status = response.json()["summary"]["status"]
    total_time = sum(status.values())

    return round(Decimal(status["totalup"] / total_time), 4)
Ejemplo n.º 21
0
def do_travel(distance: float, ship: OurShip, days_spent: int,
              date: Arrow) -> Tuple[str, bool]:
    """
    Performs the travelling part of our journey, will mutate `date` as a side effect

    Returns a log entry and a boolean denoting whether the current date has already been logged
    """
    log = ""
    date_already_logged = False

    while distance > 0:
        day_log_entry = "<span>"

        sunk, provision_log = handle_provisions(ship)
        day_log_entry += provision_log
        if sunk:
            log += day_log_entry
            break

        sunk, encounter_log = do_encounter(ship)
        day_log_entry += encounter_log
        if sunk:
            log += day_log_entry
            break

        wind = navigation.check_wind(ship.captain)
        distance_travelled = 125 * wind[0]
        days_spent += 1
        current_date = date.shift(days=+1)
        distance -= distance_travelled

        # Check if anything interesting happened and add it to the main log if so
        if day_log_entry != "<span>":
            log += f"\n{get_date(current_date)}"
            log += f"{day_log_entry}</span>"
            date_already_logged = True

    return log, date_already_logged
Ejemplo n.º 22
0
def create_time_entry(date: arrow.Arrow, start_hour: int, start_minute: int,
                      end_hour: int, end_minute: int, hourly_rate: float):
    if end_hour == 0 and end_minute == 0:
        end_hour = 24

    if end_hour < start_hour:  # Create two separate time entries if the recorded time is winds back due to midnight
        print("TWO ENTRIES FOR MIDNIGHT")
        print(date)
        print(str(start_hour) + ":" + str(start_minute))
        print(str(end_hour) + ":" + str(end_minute))
        return [
            create_time_entry(date, start_hour, start_minute, 24, 0,
                              hourly_rate)[0],
            create_time_entry(date.shift(days=1), 0, 0, end_hour, end_minute,
                              hourly_rate)[0]
        ]

    start = start_hour + (start_minute / 60.0)
    end = end_hour + (end_minute / 60.0)

    #print(str(start_hour) + ":" + str(start_minute) + " -> " + str(end_hour) + ":" + str(end_minute))

    return [TimeEntry(date, start, end, hourly_rate)]
Ejemplo n.º 23
0
    def initialize_room(self, room: Room, now: Arrow) -> Room:
        """
        部屋の情報を初期化する。次のバックログの見積もりを始める際に実施する。
        :param now:
        :param room: 部屋
        :return:
        """
        item = {
            "room_id": room.room_id,
            "opened": False,
            "ttl": now.shift(days=1).int_timestamp,
        }
        for member in room.members:
            item[f"mem_{member.member_id}"] = {
                "nickname": member.nickname,
                "point": None
            }

        self.table.put_item(Item=item)

        new_room = self.query_room(room.room_id)
        if new_room:
            return new_room
        raise Exception
Ejemplo n.º 24
0
def open_time(control_dist_km: float, brevet_dist_km: float,
              brevet_start_time: Arrow):
    """
    Args:
       control_dist_km:  number, control distance in kilometers
       brevet_dist_km: number, nominal distance of the brevet
          in kilometers, which must be one of 200, 300, 400, 600,
          or 1000 (the only official ACP brevet distances)
       brevet_start_time:  A date object (arrow)
    Returns:
       A date object indicating the control open time.
       This will be in the same time zone as the brevet start time.
    """
    # note: times are rounded to the nearest minute
    # special case: if control_dist_km == 0, open_time is brevet_start_time
    if control_dist_km == 0:
        open_time = brevet_start_time
    # for distances below 200 - normal case
    elif control_dist_km <= 200:
        (hours, minutes) = h_m_at_speed(control_dist_km, TOP_SPEEDS['to200'])
        open_time = brevet_start_time.shift(hours=hours,
                                            minutes=round(minutes))
    # special case: if brevet_dist == 200, controle distances can be up to 20% longer
    # but the controle times remain fixed at their 200 values
    elif (brevet_dist_km
          == 200) and (control_dist_km <= round(200 + 200 * .2)):
        open_time = brevet_start_time.shift(hours=5, minutes=53)
    # special case: if brevet_dist == 300km, controle distances can be up to 20% longer
    # but the controle times remain fixed at their 300km values
    elif (brevet_dist_km == 300) and (control_dist_km <= round(300 + 300 * .2)
                                      ) and (control_dist_km >= 300):
        open_time = brevet_start_time.shift(hours=9)
    # for distances below 400
    elif control_dist_km <= 400:
        first_200: Tuple[int, float] = h_m_at_speed(200, TOP_SPEEDS['to200'])
        remaining_distance = control_dist_km - 200
        second_200: Tuple[int, float] = h_m_at_speed(remaining_distance,
                                                     TOP_SPEEDS['to400'])
        (total_hours,
         total_minutes) = (first_200[0] + second_200[0]), (first_200[1] +
                                                           second_200[1])
        # summing minutes may result in more than 60, so we need to carry over into hour
        if total_minutes >= 60:
            (total_hours,
             total_minutes) = carry_m_to_h(total_hours, total_minutes)
        open_time = brevet_start_time.shift(hours=total_hours,
                                            minutes=round(total_minutes))
    # special case: if brevet_dist == 400km, controle distances can be up to 20% longer
    # but the controle times remain fixed at their 400km values
    elif (brevet_dist_km
          == 400) and (control_dist_km <= round(400 + 400 * .2)):
        open_time = brevet_start_time.shift(hours=12, minutes=8)
    # for distances below 600
    elif control_dist_km <= 600:
        first_200: Tuple[int, float] = h_m_at_speed(200, TOP_SPEEDS['to200'])
        second_200: Tuple[int, float] = h_m_at_speed(200, TOP_SPEEDS['to400'])
        remaining_distance = control_dist_km - 400
        third_200: Tuple[int, float] = h_m_at_speed(remaining_distance,
                                                    TOP_SPEEDS['to600'])
        (total_hours,
         total_minutes) = (first_200[0] + second_200[0]) + third_200[0], (
             first_200[1] + second_200[1] + third_200[1])
        # summing minutes may result in more than 60, so we need to carry over into hour
        if total_minutes >= 60:
            (total_hours,
             total_minutes) = carry_m_to_h(total_hours, total_minutes)
        open_time = brevet_start_time.shift(hours=total_hours,
                                            minutes=round(total_minutes))
    # special case: if brevet_dist == 600, controle distances can be up to 20% longer
    # but the controle times remain fixed at their 600km values
    elif (brevet_dist_km
          == 600) and (control_dist_km <= round(600 + 600 * .2)):
        open_time = brevet_start_time.shift(hours=18, minutes=48)
    # a 1000km brevet may have a final controle up to 20% greater than 1000
    elif control_dist_km < round(1000 + 1000 * .2):
        if control_dist_km > 1000:
            control_dist_km = 1000
        first_200: Tuple[int, float] = h_m_at_speed(200, TOP_SPEEDS['to200'])
        second_200: Tuple[int, float] = h_m_at_speed(200, TOP_SPEEDS['to400'])
        third_200: Tuple[int, float] = h_m_at_speed(200, TOP_SPEEDS['to600'])
        remaining_distance = control_dist_km - 600
        final_400: Tuple[int, float] = h_m_at_speed(remaining_distance,
                                                    TOP_SPEEDS['to1000'])
        (total_hours, total_minutes
         ) = (first_200[0] + second_200[0]) + third_200[0] + final_400[0], (
             first_200[1] + second_200[1] + third_200[1] + final_400[1])
        # summing minutes may result in more than 60, so we need to carry over into hour
        if total_minutes >= 60:
            (total_hours,
             total_minutes) = carry_m_to_h(total_hours, total_minutes)
        open_time = brevet_start_time.shift(hours=total_hours,
                                            minutes=round(total_minutes))

    # should never end up here
    else:
        # indicate error for now by returning datetime arrow with all 0's
        open_time = arrow.get('0000-00-00 00:00:00', 'YYYY-MM-DD HH:mm:ss')
    #   open_time = None
    return open_time