def test_patch_timeout_bigger_than_0( self, service: Service, timeout: timedelta ) -> None: body = {'timeout': timeout.total_seconds()} self._send_patch_request(service, body) self.assertEqual( floor(timeout.total_seconds()), service.timeout.total_seconds() )
def test_that_setting_valid_timeout_changes_it( self, timeout: timedelta ): self.service.timeout = timeout self.assertEqual( timeout.total_seconds(), self.service.timeout.total_seconds() )
def fetch_rows(self, column_names: Tuple, interval: timedelta) -> List[Tuple]: str_columns: str = '' column_names = ("Timestamp",) + column_names for str_column in column_names: if not str_column: continue if str_columns: str_columns += ', ' str_columns += str_column str_query: str = 'SELECT {} ' \ 'FROM `{}.{}` ' \ 'WHERE Timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL {} SECOND)'. \ format(str_columns, self.bot.bq.dataset_id, self.telemetry_table_id, int(interval.total_seconds())) print('MonitoringTelegramBot: About to execute query: "{}"'.format(str_query)) job: bigquery.job.QueryJob = self.bot.bq.client.query(str_query, location=self.bot.bq.location) result: List[Tuple] = [] for row in job.result(): columns: Tuple = () for str_column in column_names: if not str_column: columns += (None,) else: columns += (row.get(str_column),) result.append(columns) result.sort(key=lambda x: x[0]) # for r in result: # print(r) return result
def validate_timeout(self, timeout: timedelta): if timeout.total_seconds() < 0: raise ValidationError( 'Attempted to set a negative timeout duration' ) else: return True
def modulo_timedelta(dt: datetime, td: timedelta) -> datetime: """ Takes a datetime to perform modulo on and a timedelta. :returns: dt % td """ today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) return timedelta(seconds=((dt - today).total_seconds() % td.total_seconds()))
def get_cool_off_iso8601(delta: timedelta) -> str: """ Return datetime.timedelta translated to ISO 8601 formatted duration for use in e.g. cool offs. """ seconds = delta.total_seconds() minutes, seconds = divmod(seconds, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) days_str = f'{days:.0f}D' if days else '' time_str = ''.join( f'{value:.0f}{designator}' for value, designator in [ [hours, 'H'], [minutes, 'M'], [seconds, 'S'], ] if value ) if time_str: return f'P{days_str}T{time_str}' return f'P{days_str}'
def schedule_next(self, delay: datetime.timedelta): self._next_expected = datetime.datetime.now() + delay if self.scheduler is None: delay_secs = delay.total_seconds() default_run_worker(delay_secs, self) else: self.scheduler.add(delay, self)
def mark_as_failed(self, identifier: str, requeue_delay: timedelta=timedelta(0)): self._queue.mark_finished(identifier) self._queue.mark_dirty(identifier, requeue_delay) logging.debug('%s has been marked as failed', identifier) # Broadcast the change after the requeue delay # FIXME? Timer's interval may not be 100% accurate and may also # not correspond with the database server; this could go out of # synch... Add a tolerance?? Timer(requeue_delay.total_seconds(), self._broadcast).start()
def from_python(cls, value: timedelta): if value is None: return Null.from_python() data_type = cls.build_c_type() data_object = data_type() data_object.type_code = int.from_bytes( cls.type_code, byteorder=PROTOCOL_BYTE_ORDER ) data_object.value = int(value.total_seconds() * 1000) return bytes(data_object)
def __init__(self, payload_factory:Callable[..., Iterable], latency:timedelta): super().__init__() self._discharge_latency = latency.total_seconds() self._lock = Lock() self._payload = payload_factory() # Start the watcher self._watcher_thread = Thread(target=self._watcher, daemon=True) self._watching = True self._watcher_thread.start()
def batch_raw_query(prometheus_endpoint: ParseResult, start_timestamp: int, end_timestamp: int, step: datetime.timedelta, query: str, maxpts=11000) -> Iterable[bytes]: """Retrieve metrics from a Prometheus database""" sstep = '{}s'.format(int(step.total_seconds())) url = urljoin(prometheus_endpoint.geturl(), 'api/v1/query_range') def sub(sub_start, sub_end): """sub""" payload = [('start', sub_start), ('end', sub_end), ('step', sstep), ('query', query)] req = requests.get(url, params=payload) return req.content delta = end_timestamp - start_timestamp batch_size = min(delta // int(step.total_seconds()), maxpts) # type: int for limits in _create_batches(start_timestamp, end_timestamp, batch_size): sub_start, sub_end = limits yield sub(sub_start, sub_end)
def mark_as_failed(self, identifier: str, requeue_delay: timedelta=timedelta(0)): if identifier not in self._known_data: raise ValueError("Not known: %s" % identifier) with self._lists_lock: self._assert_is_being_processed(identifier) self._processing.remove(identifier) self._failed.append(identifier) if requeue_delay is not None: if requeue_delay.total_seconds() == 0: self._reprocess(identifier) else: end_time = self._get_time() + requeue_delay.total_seconds() def on_delay_end(): if timer in self._timers[end_time]: self._timers[end_time].remove(timer) self._reprocess(identifier) timer = Timer(requeue_delay.total_seconds(), on_delay_end) self._timers[end_time].append(timer) timer.start() else: self._on_complete(identifier)
def time_ago(interval: timedelta) -> str: ago_string = '' s = interval.total_seconds() if (s >= 31536000): return "{0:-4.1f} years".format(s/31536000) elif (s >= 2628000): return "{0:-4.1f} months".format(s/2628000) elif (s >= 604800): return "{0:-4.1f} weeks".format(s/604800) elif (s >= 86400): return "{0:-4.1f} days".format(s/86400) elif (s >= 3600): return "{0:-4.1f} hours".format(s/3600) elif (s >= 60): return "{0:-4.1f} minutes".format(s/60) else: return "{0:-4.1f} seconds".format(s)
def format_timestamp(time: datetime.timedelta) -> str: """ Convert timedelta to hh:mm:ss.mmm https://matroska.org/technical/specs/subtitles/srt.html :param time: Timedelta :return: Formatted time string """ days, seconds = divmod(time.total_seconds(), 24 * 60 * 60) hours, seconds = divmod(seconds, 60 * 60) minutes, seconds = divmod(seconds, 60) milliseconds = int((seconds - int(seconds)) * 1000) # Floor seconds and merge days to hours seconds = int(seconds) hours += days * 24 return f'{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d},{milliseconds:03d}'
def td_format(td_object: timedelta): seconds = int(td_object.total_seconds()) periods = [ ("year", 60 * 60 * 24 * 365), ("month", 60 * 60 * 24 * 30), ("day", 60 * 60 * 24), ("hour", 60 * 60), ("minute", 60), ("second", 1), ] strings = [] for period_name, period_seconds in periods: if seconds > period_seconds: period_value, seconds = divmod(seconds, period_seconds) has_s = "s" if period_value > 1 else "" strings.append("%s %s%s" % (period_value, period_name, has_s)) return ", ".join(strings)
def duration_display(duration: datetime.timedelta) -> str: secs = duration.total_seconds() if secs == 0: return "no time at all" hours, secs = divmod(secs, 3600) minutes, secs = divmod(secs, 60) result = [] if hours == 1: result.append("1 hour") elif hours > 1: result.append("%d hours" % hours) if minutes == 1: result.append("1 minute") elif minutes > 1: result.append("%d minutes" % minutes) if secs == 1: result.append("1 second") elif secs > 1: result.append("%d seconds" % secs) return lang.join(result)
def __init__(self, couchdb_url:str, couchdb_name:str, buffer_capacity:int = 1000, buffer_latency:timedelta = timedelta(milliseconds=50), **kwargs): """ Constructor: Initialise the database interfaces @param couchdb_url CouchDB URL @param couchdb_name Database name @param buffer_capacity Buffer capacity @param buffer_latency Buffer latency """ super().__init__() self._sofa = Sofabed(couchdb_url, couchdb_name, buffer_capacity, buffer_latency, **kwargs) self._queue = _Bert(self._sofa) self._metadata = _Ernie(self._sofa) self._queue_lock = CountingLock() self._pending_cache = deque() self._latency = buffer_latency.total_seconds()
def iso8601(delta: timedelta) -> str: """ Return datetime.timedelta translated to ISO 8601 formatted duration. """ seconds = delta.total_seconds() minutes, seconds = divmod(seconds, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) date = '{:.0f}D'.format(days) if days else '' time_values = hours, minutes, seconds time_designators = 'H', 'M', 'S' time = ''.join([ ('{:.0f}'.format(value) + designator) for value, designator in zip(time_values, time_designators) if value] ) return 'P' + date + ('T' + time if time else '')
def __call__(self, td: datetime.timedelta) -> float: if isinstance(td, (float, int)): return self._numtype(td) return self._numtype(td.total_seconds())
def delta_to_json(delta: timedelta) -> str: parts = str(delta.total_seconds()).split(".") if len(parts) > 1: while len(parts[1]) not in [3, 6, 9]: parts[1] = f"{parts[1]}0" return f"{'.'.join(parts)}s"
async def mute(self, actor: discord.Member, member: discord.Member, delta: timedelta, reason: str, modify_db: bool = True) -> None: """Mutes a ``member`` for ``delta``""" guild_config = await self.db.get_guild_config(member.guild.id) mute_role = discord.utils.get(member.guild.roles, id=int(guild_config.mute_role or 0)) if not mute_role: # mute role mute_role = discord.utils.get(member.guild.roles, name='Muted') if not mute_role: # existing mute role not found, let's create one mute_role = await member.guild.create_role( name='Muted', color=discord.Color(0x818689), reason='Attempted to mute user but role did not exist') for tc in member.guild.text_channels: try: await tc.set_permissions( mute_role, send_messages=False, reason= 'Attempted to mute user but role did not exist') except discord.Forbidden: pass for vc in member.guild.voice_channels: try: await vc.set_permissions( mute_role, speak=False, reason= 'Attempted to mute user but role did not exist') except discord.Forbidden: pass await self.db.update_guild_config( member.guild.id, {'$set': { 'mute_role': str(mute_role.id) }}) await member.add_roles(mute_role) # mute complete, log it log_channel: discord.TextChannel = self.get_channel( tryint(guild_config.modlog.member_mute)) if log_channel: current_time = datetime.utcnow() offset = guild_config.time_offset current_time += timedelta(hours=offset) current_time_fmt = current_time.strftime('%H:%M:%S') await log_channel.send( f"`{current_time_fmt}` {actor} has muted {member} ({member.id}), reason: {reason} for {format_timedelta(delta)}" ) if delta: duration = delta.total_seconds() # log complete, save to DB if duration is not None: duration += time() if modify_db: await self.db.update_guild_config( member.guild.id, { '$push': { 'mutes': { 'member': str(member.id), 'time': duration } } }) self.loop.create_task( self.unmute(member.guild.id, member.id, duration))
def test_that_setting_valid_timeout_changes_it(self, timeout: timedelta): self.service.timeout = timeout self.assertEqual(timeout.total_seconds(), self.service.timeout.total_seconds())
def _format_timedelta(delta: timedelta): total_seconds = delta.total_seconds() hours, remainder = divmod(total_seconds, 3600) minutes, seconds = divmod(remainder, 60) return f"{int(hours)}:{int(minutes):02}:{int(seconds):02}"
def format_timestamp(timestamp: timedelta): return ("%02d:%02d:%06.3f" % ( timestamp.total_seconds() // 3600, timestamp.total_seconds() // 60 % 60, timestamp.total_seconds() % 60, )).replace(".", ",")
def fromTimeDelta(cls, td: timedelta): return cls(seconds=td.total_seconds())
def format_timestamp(timestamp: timedelta): return ( '%02d:%02d:%02.3f' % (timestamp.total_seconds() // 3600, timestamp.total_seconds() // 60 % 60, timestamp.total_seconds() % 60)).replace('.', ',')
def downtime_services(self, service_re: str, reason: Reason, *, duration: timedelta = timedelta(hours=4)) -> None: """Downtime services on the Icinga server for the given time with a message. If there are multiple target_hosts, the set of matching services may vary from host to host (e.g. because a hostname, DB section, or other unique fact is included in the service name) and downtime_services will downtime each service on the correct target_host. If some hosts happen to have no matching services, they will be safely skipped. But if *no* hosts have matching services, IcingaError is raised (because the regex is probably wrong). Arguments: service_re (str): the regular expression matching service names to downtime. reason (spicerack.administrative.Reason): the reason to set for the downtime on the Icinga server. duration (datetime.timedelta, optional): the length of the downtime period. Raises: re.error: if service_re is an invalid regular expression. IcingaError: if no services on any target host match the regular expression. """ duration_seconds = int(duration.total_seconds()) if duration_seconds < MIN_DOWNTIME_SECONDS: raise IcingaError( f"Downtime duration must be at least 1 minute, got: {duration}" ) try: # This also validates the regular expression syntax and ensures all hosts are known to Icinga. status = self.get_status(service_re) except IcingaStatusNotFoundError as e: raise IcingaError(f"{e} - no hosts have been downtimed.") from e unique_services = set() for host_status in status.values(): for service in host_status.services: unique_services.add(service["name"]) unique_service_count = len(unique_services) matched_host_count = sum(1 if host_status.services else 0 for host_status in status.values()) if not unique_services: raise IcingaError( f'No services on {self._target_hosts} matched "{service_re}"') logger.info( 'Scheduling downtime on Icinga server %s for services "%s" for host%s: %s ' "(matched %d unique service name%s on %d host%s)", self._icinga_host, service_re, "" if len(self._target_hosts) == 1 else "s", self._target_hosts, unique_service_count, "" if unique_service_count == 1 else "s", matched_host_count, "" if matched_host_count == 1 else "s", ) start_time = str(int(time.time())) end_time = str(int(time.time() + duration_seconds)) # This doesn't use self.run_icinga_command because if the service names are different, we'll set different # downtimes (and therefore run different Icinga commands) for each target_host. commands = [] for hostname, host_status in status.items(): for service in host_status.services: logger.debug('Downtiming "%s" on %s', service["name"], hostname) commands.append( self._get_command_string( "SCHEDULE_SVC_DOWNTIME", hostname, service["name"], start_time, end_time, "1", # Start at the start_time and end at the end_time. "0", # Not triggered by another downtime. str(duration_seconds), reason.owner, reason.reason, )) self._icinga_host.run_sync(*commands, print_output=False, print_progress_bars=False)
def _serialize_timedelta(object_to_serialize: timedelta) -> float: return object_to_serialize.total_seconds()
def total_seconds(timedelta: mod_datetime.timedelta) -> float: """ Some versions of python don't have the timedelta.total_seconds() method. """ if timedelta is None: return None return timedelta.total_seconds()
def get_h_m_s(td: timedelta): m, s = divmod(td.total_seconds(), 60) h, m = divmod(m, 60) return h, m, s
def minutes(td: timedelta): return td.total_seconds() // 60
def alignDateToInterval(dt: datetime, interval: timedelta) -> datetime: intervalCount = datetimeToIntervalCount(dt, interval) return datetime.utcfromtimestamp(intervalCount * int(interval.total_seconds()))
def get_metabolism_factor(self, step: timedelta) -> float: hl_step = self.half_life.total_seconds() / step.total_seconds() factor = 2**(-1.0 / hl_step) return factor
def convert_timedelta_to_minutes(value: timedelta): seconds = value.total_seconds() minutes = seconds // 60 return minutes
def elapsed_time_value_and_unit(td: datetime.timedelta) -> (str, str): s = '%f' % td.total_seconds() return (s, 's')
def hours_minutes_seconds(duration: datetime.timedelta) -> Tuple[int, int, int]: seconds = duration.total_seconds() return int(seconds // 3600), int((seconds % 3600) // 60), int((seconds % 3600) % 60)
def _my_run_every(self, callback: Callable[[dict], Any], start: datetime, interval: timedelta, **kwargs): self.run_every(callback, start=start, interval=interval.total_seconds(), **kwargs)
def _(s: timedelta) -> float: return s.total_seconds()
def trunc_ts(ts: datetime, step: timedelta): base = datetime.min.replace(year=2000) step_s = step.total_seconds() seconds = (ts - base).total_seconds() seconds = int(seconds / step_s) * step_s return (base + timedelta(seconds=seconds, milliseconds=500)).replace(microsecond=0)
def timeout(self, new_timeout: timedelta) -> None: if not new_timeout.total_seconds() > 0: raise ValueError( 'The timeout must be a time longer than 0 seconds') self.db_model.timeout = new_timeout.total_seconds()
def t_to_x(self, t: timedelta) -> int: width = self.rect_f.width() x = round(t.total_seconds() / self.total_t.total_seconds() * width) return x
def get_metric_range_data( self, metric_name: str, label_config: dict = None, start_time: datetime = (datetime.now() - timedelta(minutes=10)), end_time: datetime = datetime.now(), chunk_size: timedelta = None, store_locally: bool = False, params: dict = None, ): r""" Get the current metric value for the specified metric and label configuration. :param metric_name: (str) The name of the metric. :param label_config: (dict) A dictionary specifying metric labels and their values. :param start_time: (datetime) A datetime object that specifies the metric range start time. :param end_time: (datetime) A datetime object that specifies the metric range end time. :param chunk_size: (timedelta) Duration of metric data downloaded in one request. For example, setting it to timedelta(hours=3) will download 3 hours worth of data in each request made to the prometheus host :param store_locally: (bool) If set to True, will store data locally at, `"./metrics/hostname/metric_date/name_time.json.bz2"` :param params: (dict) Optional dictionary containing GET parameters to be sent along with the API request, such as "time" :return: (list) A list of metric data for the specified metric in the given time range :raises: (RequestException) Raises an exception in case of a connection error (PrometheusApiClientException) Raises in case of non 200 response status code """ params = params or {} data = [] _LOGGER.debug("start_time: %s", start_time) _LOGGER.debug("end_time: %s", end_time) _LOGGER.debug("chunk_size: %s", chunk_size) if not (isinstance(start_time, datetime) and isinstance(end_time, datetime)): raise TypeError("start_time and end_time can only be of type datetime.datetime") if not chunk_size: chunk_size = end_time - start_time if not isinstance(chunk_size, timedelta): raise TypeError("chunk_size can only be of type datetime.timedelta") start = round(start_time.timestamp()) end = round(end_time.timestamp()) if (end_time - start_time).total_seconds() < chunk_size.total_seconds(): sys.exit("specified chunk_size is too big") chunk_seconds = round(chunk_size.total_seconds()) if label_config: label_list = [str(key + "=" + "'" + label_config[key] + "'") for key in label_config] query = metric_name + "{" + ",".join(label_list) + "}" else: query = metric_name _LOGGER.debug("Prometheus Query: %s", query) while start < end: if start + chunk_seconds > end: chunk_seconds = end - start # using the query API to get raw data response = self._session.get( "{0}/api/v1/query".format(self.url), params={ **{ "query": query + "[" + str(chunk_seconds) + "s" + "]", "time": start + chunk_seconds, }, **params, }, verify=self.ssl_verification, headers=self.headers, ) if response.status_code == 200: data += response.json()["data"]["result"] else: raise PrometheusApiClientException( "HTTP Status Code {} ({!r})".format(response.status_code, response.content) ) if store_locally: # store it locally self._store_metric_values_local( metric_name, json.dumps(response.json()["data"]["result"]), start + chunk_seconds, ) start += chunk_seconds return data
def timedelta_to_utc_str(delta: timedelta) -> str: """converts timedelta to the string like 'UTC+delta'""" hours, seconds = divmod(delta.total_seconds(), 3600) minutes, _ = divmod(seconds, 60) return f"UTC{int(hours):+}" + (f":{int(minutes)}" if minutes != 0 else "")
def __get_value(self, td: timedelta): return td.total_seconds()
def formatTimeDelta(td: timedelta) -> str: total = td.total_seconds() hours, remainder = divmod(total, 3600) minutes, seconds = divmod(remainder, 60) return '%d:%02d:%02d' % (hours, minutes, seconds)
def elapsed_time(self, new_value: timedelta): self.response_time = new_value.total_seconds() * 1000
def async_set_delayed_turn_off(self, time_period: timedelta): """Set delay off. The unit is different per device.""" yield from self._try_command( "Setting the delay off failed.", self._light.delay_off, time_period.total_seconds())
def timedelta_to_miliseconds(timedelta: datetime.timedelta) -> int: """Convert ``timedelta`` object to miliseconds.""" return int(timedelta.total_seconds() * 1000)
def TimeToTicks(value): """Converts a Time object to ticks.""" timeStruct = TimeDelta(hours = value.hour, minutes = value.minute, seconds = value.second, microseconds = value.microsecond) timeDec = decimal.Decimal(str(timeStruct.total_seconds())) return (int((timeDec + time.timezone) * 10**abs(timeDec.as_tuple()[2])), abs(timeDec.as_tuple()[2]))
def validate_timeout(self, timeout: timedelta): if timeout.total_seconds() < 0: raise ValidationError( 'Attempted to set a negative timeout duration') else: return True
def generate(self, departureTime:datetime.time, delay: datetime.timedelta, category, number, sequence): """ :param departureTime: :param category: :param number: :param sequence: list of station IDs :return: """ orders, goodness = self._generate_stations(sequence) aux = [] if self.display_hour is not None: flap_id = next((flap.flap_id for flap in self.display_hour.flaps.values() if flap.ids[0] == departureTime.hour), None) if flap_id is not None: aux.append(self.display_hour.flaps[flap_id]) if self.display_minu is not None: flap_id = next((flap.flap_id for flap in self.display_minu.flaps.values() if flap.ids[0] == departureTime.minute), None) if flap_id is not None: aux.append(self.display_minu.flaps[flap_id]) if self.display_dela is not None: minutes = delay.total_seconds() / 60 if minutes > 0: best_flap = None best_diff = None for flap in self.display_dela.flaps.values(): if type(flap.ids[0]) is not int: continue diff = abs(minutes - int(flap.ids[0])) if diff <= 30 and (best_flap is None or best_diff >= diff): best_flap = flap best_diff = diff if diff == 0: # perfect match break if best_flap is not None: aux.append(best_flap) #TODO: move to _generate_stations() and _goodness() if self.display_type is not None: best_flap = None best_rating = 0 for flap in self.display_type.flaps.values(): rating = 0 requirements = True if category == flap.ids[0]: rating += 1 else: requirements = False if number == str(flap.ids[2]): rating += 4 elif flap.ids[2] is None: rating += 1 else: requirements = False if flap.ids[4] is not None and int(flap.ids[4]) in sequence: rating += 1 if flap.ids[5] is not None and int(flap.ids[5]) in sequence: rating += 1 if requirements and (best_flap is None or rating > best_rating): best_flap = flap best_rating = rating if best_flap is not None: aux.append(best_flap) print("Type rating: {} {}".format(best_rating, best_flap)) return list(orders)+aux, goodness
def get_hours_minutes_seconds(self, timedelta: datetime.timedelta): total_s = timedelta.total_seconds() hours = int(total_s // 3600) minutes = int((total_s % 3600) // 60) seconds = int((total_s % 3600) % 60) return hours, minutes, seconds
def timedelta_encoder(timedelta: datetime.timedelta): return str(timedelta.total_seconds())
def timeout(self, new_timeout: timedelta) -> None: if not new_timeout.total_seconds() > 0: raise ValueError( 'The timeout must be a time longer than 0 seconds' ) self.db_model.timeout = new_timeout.total_seconds()
async def async_set_delayed_turn_off(self, time_period: timedelta): """Set delayed turn off.""" await self._try_command( "Setting the turn off delay failed.", self._light.delay_off, round(time_period.total_seconds() / 60))
def datetimeToIntervalCount(dt: datetime, interval: timedelta) -> int: dateSeconds = int(datetimeToTimestamp(dt)) intervalSeconds = int(interval.total_seconds()) return dateSeconds // intervalSeconds
def __init__(self, max_size:int, latency:timedelta): self._max_size = max_size if max_size > 0 else 1 self._latency = latency.total_seconds() super().__init__(list, latency / 2) self.last_updated = monotonic()
def fmtDelta(t: delta) -> str: ts = t.total_seconds() tm, ss = divmod(abs(ts), 60) hh, mm = divmod(tm, 60) return f"{'-' if ts < 0 else ' '}{hh:02.0f}:{mm:02.0f}:{ss:02.0f}"
def _count_timedelta(delta: _datetime.timedelta, step: int, seconds_in_interval: int) -> int: """Helper function for iterate. Finds the number of intervals in the timedelta.""" return int(delta.total_seconds() / (seconds_in_interval * step))
def fmt_delta(delta: timedelta): secs = delta.total_seconds() hrs = secs // 60 ** 2 mins = secs // 60 % 60 return '({:02.0f}:{:02.0f})'.format(hrs, mins)