Add datetime helpers and move all time/date time datetime_handling folder
previous string_handling located datetime and timestamp files have been moved to the datetime handling folder Update readme file with more information about currently covered areas
This commit is contained in:
0
src/corelibs/datetime_handling/__init__.py
Normal file
0
src/corelibs/datetime_handling/__init__.py
Normal file
435
src/corelibs/datetime_handling/datetime_helpers.py
Normal file
435
src/corelibs/datetime_handling/datetime_helpers.py
Normal file
@@ -0,0 +1,435 @@
|
||||
"""
|
||||
Various string based date/time helpers
|
||||
"""
|
||||
|
||||
import time as time_t
|
||||
from datetime import datetime, time
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
from typing import Callable
|
||||
|
||||
DAYS_OF_WEEK_LONG_TO_SHORT: dict[str, str] = {
|
||||
'Monday': 'Mon',
|
||||
'Tuesday': 'Tue',
|
||||
'Wednesay': 'Wed',
|
||||
'Thursday': 'Thu',
|
||||
'Friday': 'Fri',
|
||||
'Saturday': 'Sat',
|
||||
'Sunday': 'Sun',
|
||||
}
|
||||
DAYS_OF_WEEK_ISO: dict[int, str] = {
|
||||
1: 'Mon', 2: 'Tue', 3: 'Wed', 4: 'Thu', 5: 'Fri', 6: 'Sat', 7: 'Sun'
|
||||
}
|
||||
DAYS_OF_WEEK_ISO_REVERSED: dict[str, int] = {value: key for key, value in DAYS_OF_WEEK_ISO.items()}
|
||||
|
||||
|
||||
def create_time(timestamp: float, timestamp_format: str = "%Y-%m-%d %H:%M:%S") -> str:
|
||||
"""
|
||||
just takes a timestamp and prints out humand readable format
|
||||
|
||||
Arguments:
|
||||
timestamp {float} -- _description_
|
||||
|
||||
Keyword Arguments:
|
||||
timestamp_format {_type_} -- _description_ (default: {"%Y-%m-%d %H:%M:%S"})
|
||||
|
||||
Returns:
|
||||
str -- _description_
|
||||
"""
|
||||
return time_t.strftime(timestamp_format, time_t.localtime(timestamp))
|
||||
|
||||
|
||||
def get_system_timezone():
|
||||
"""Get system timezone using datetime's automatic detection"""
|
||||
# Get current time with system timezone
|
||||
local_time = datetime.now().astimezone()
|
||||
|
||||
# Extract timezone info
|
||||
system_tz = local_time.tzinfo
|
||||
timezone_name = str(system_tz)
|
||||
|
||||
return system_tz, timezone_name
|
||||
|
||||
|
||||
def parse_timezone_data(timezone_tz: str = '') -> ZoneInfo:
|
||||
"""
|
||||
parses a string to get the ZoneInfo
|
||||
If not set or not valid gets local time,
|
||||
if that is not possible get UTC
|
||||
|
||||
Keyword Arguments:
|
||||
timezone_tz {str} -- _description_ (default: {''})
|
||||
|
||||
Returns:
|
||||
ZoneInfo -- _description_
|
||||
"""
|
||||
try:
|
||||
return ZoneInfo(timezone_tz)
|
||||
except (ZoneInfoNotFoundError, ValueError, TypeError):
|
||||
# use default
|
||||
time_tz, time_tz_str = get_system_timezone()
|
||||
if time_tz is None:
|
||||
return ZoneInfo('UTC')
|
||||
# TODO build proper TZ lookup
|
||||
tz_mapping = {
|
||||
'JST': 'Asia/Tokyo',
|
||||
'KST': 'Asia/Seoul',
|
||||
'IST': 'Asia/Kolkata',
|
||||
'CST': 'Asia/Shanghai', # Default to China for CST
|
||||
'AEST': 'Australia/Sydney',
|
||||
'AWST': 'Australia/Perth',
|
||||
'EST': 'America/New_York',
|
||||
'EDT': 'America/New_York',
|
||||
'CDT': 'America/Chicago',
|
||||
'MST': 'America/Denver',
|
||||
'MDT': 'America/Denver',
|
||||
'PST': 'America/Los_Angeles',
|
||||
'PDT': 'America/Los_Angeles',
|
||||
'GMT': 'UTC',
|
||||
'UTC': 'UTC',
|
||||
'CET': 'Europe/Berlin',
|
||||
'CEST': 'Europe/Berlin',
|
||||
'BST': 'Europe/London',
|
||||
}
|
||||
try:
|
||||
return ZoneInfo(tz_mapping[time_tz_str])
|
||||
except (ZoneInfoNotFoundError, IndexError) as e:
|
||||
raise ValueError(f"No mapping for {time_tz_str}: {e}") from e
|
||||
|
||||
|
||||
def get_datetime_iso8601(timezone_tz: str | ZoneInfo = '', sep: str = 'T', timespec: str = 'microseconds') -> str:
|
||||
"""
|
||||
set a datetime in the iso8601 format with microseconds
|
||||
|
||||
Returns:
|
||||
str -- _description_
|
||||
"""
|
||||
# parse if this is a string
|
||||
if isinstance(timezone_tz, str):
|
||||
timezone_tz = parse_timezone_data(timezone_tz)
|
||||
return datetime.now(timezone_tz).isoformat(sep=sep, timespec=timespec)
|
||||
|
||||
|
||||
def validate_date(date: str, not_before: datetime | None = None, not_after: datetime | None = None) -> bool:
|
||||
"""
|
||||
check if Y-m-d or Y/m/d are parsable and valid
|
||||
|
||||
Arguments:
|
||||
date {str} -- _description_
|
||||
|
||||
Returns:
|
||||
bool -- _description_
|
||||
"""
|
||||
formats = ['%Y-%m-%d', '%Y/%m/%d']
|
||||
for __format in formats:
|
||||
try:
|
||||
__date = datetime.strptime(date, __format).date()
|
||||
if not_before is not None and __date < not_before.date():
|
||||
return False
|
||||
if not_after is not None and __date > not_after.date():
|
||||
return False
|
||||
return True
|
||||
except ValueError:
|
||||
continue
|
||||
return False
|
||||
|
||||
|
||||
def parse_flexible_date(
|
||||
date_str: str,
|
||||
timezone_tz: str | ZoneInfo | None = None,
|
||||
shift_time_zone: bool = True
|
||||
) -> datetime | None:
|
||||
"""
|
||||
Parse date string in multiple formats
|
||||
will add time zone info if not None
|
||||
on default it will change the TZ and time to the new time zone
|
||||
if no TZ info is set in date_str, then localtime is assumed
|
||||
|
||||
Arguments:
|
||||
date_str {str} -- _description_
|
||||
|
||||
Keyword Arguments:
|
||||
timezone_tz {str | ZoneInfo | None} -- _description_ (default: {None})
|
||||
shift_time_zone {bool} -- _description_ (default: {True})
|
||||
|
||||
Returns:
|
||||
datetime | None -- _description_
|
||||
"""
|
||||
|
||||
date_str = date_str.strip()
|
||||
|
||||
# Try different parsing methods
|
||||
parsers: list[Callable[[str], datetime]] = [
|
||||
# ISO 8601 format
|
||||
lambda x: datetime.fromisoformat(x), # pylint: disable=W0108
|
||||
# Simple date format
|
||||
lambda x: datetime.strptime(x, "%Y-%m-%d"),
|
||||
# Alternative ISO formats (fallback)
|
||||
lambda x: datetime.strptime(x, "%Y-%m-%dT%H:%M:%S"),
|
||||
lambda x: datetime.strptime(x, "%Y-%m-%dT%H:%M:%S.%f"),
|
||||
]
|
||||
|
||||
if timezone_tz is not None:
|
||||
if isinstance(timezone_tz, str):
|
||||
timezone_tz = parse_timezone_data(timezone_tz)
|
||||
|
||||
date_new = None
|
||||
for parser in parsers:
|
||||
try:
|
||||
date_new = parser(date_str)
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if date_new is not None:
|
||||
if timezone_tz is not None:
|
||||
# shift time zone (default), this will change the date
|
||||
# if the date has no +HH:MM it will take the local time zone as base
|
||||
if shift_time_zone:
|
||||
return date_new.astimezone(timezone_tz)
|
||||
# just add the time zone
|
||||
return date_new.replace(tzinfo=timezone_tz)
|
||||
return date_new
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def compare_dates(date1_str: str, date2_str: str) -> None | bool:
|
||||
"""
|
||||
compare two dates, if the first one is newer than the second one return True
|
||||
If the dates are equal then false will be returned
|
||||
on error return None
|
||||
|
||||
Arguments:
|
||||
date1_str {str} -- _description_
|
||||
date2_str {str} -- _description_
|
||||
|
||||
Returns:
|
||||
None | bool -- _description_
|
||||
"""
|
||||
|
||||
try:
|
||||
# Parse both dates
|
||||
date1 = parse_flexible_date(date1_str)
|
||||
date2 = parse_flexible_date(date2_str)
|
||||
|
||||
# Check if parsing was successful
|
||||
if date1 is None or date2 is None:
|
||||
return None
|
||||
|
||||
# Compare dates
|
||||
return date1.date() > date2.date()
|
||||
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def find_newest_datetime_in_list(date_list: list[str]) -> None | str:
|
||||
"""
|
||||
Find the newest date from a list of ISO 8601 formatted date strings.
|
||||
Handles potential parsing errors gracefully.
|
||||
|
||||
Args:
|
||||
date_list (list): List of date strings in format '2025-08-06T16:17:39.747+09:00'
|
||||
|
||||
Returns:
|
||||
str: The date string with the newest/latest date, or None if list is empty or all dates are invalid
|
||||
"""
|
||||
if not date_list:
|
||||
return None
|
||||
|
||||
valid_dates: list[tuple[str, datetime]] = []
|
||||
|
||||
for date_str in date_list:
|
||||
try:
|
||||
# Parse the date string and store both original string and parsed datetime
|
||||
parsed_date = parse_flexible_date(date_str)
|
||||
if parsed_date is None:
|
||||
continue
|
||||
valid_dates.append((date_str, parsed_date))
|
||||
except ValueError:
|
||||
# Skip invalid date strings
|
||||
continue
|
||||
|
||||
if not valid_dates:
|
||||
return None
|
||||
|
||||
# Find the date string with the maximum datetime value
|
||||
newest_date_str: str = max(valid_dates, key=lambda x: x[1])[0]
|
||||
|
||||
return newest_date_str
|
||||
|
||||
|
||||
def parse_day_of_week_range(dow_days: str) -> list[tuple[int, str]]:
|
||||
"""
|
||||
Parse a day of week list/range string and return a list of tuples with day index and name.
|
||||
Allowed are short (eg Mon) or long names (eg Monday).
|
||||
|
||||
Arguments:
|
||||
dow_days {str} -- A comma-separated list of days or ranges (e.g., "Mon,Wed-Fri")
|
||||
|
||||
Raises:
|
||||
ValueError: If the input format is invalid or if duplicate days are found.
|
||||
|
||||
Returns:
|
||||
list[tuple[int, str]] -- A list of tuples containing the day index and name.
|
||||
"""
|
||||
# we have Sun twice because it can be 0 or 7
|
||||
# Mon is 1 and Sun is 7, which is ISO standard
|
||||
dow_day = dow_days.split(",")
|
||||
dow_day = [day.strip() for day in dow_day if day.strip()]
|
||||
__out_dow_days: list[tuple[int, str]] = []
|
||||
for __dow_day in dow_day:
|
||||
# if we have a "-" in there fill
|
||||
if "-" in __dow_day:
|
||||
__dow_range = __dow_day.split("-")
|
||||
__dow_range = [day.strip().capitalize() for day in __dow_range if day.strip()]
|
||||
try:
|
||||
start_day = DAYS_OF_WEEK_ISO_REVERSED[__dow_range[0]]
|
||||
end_day = DAYS_OF_WEEK_ISO_REVERSED[__dow_range[1]]
|
||||
except KeyError:
|
||||
# try long time
|
||||
try:
|
||||
start_day = DAYS_OF_WEEK_ISO_REVERSED[DAYS_OF_WEEK_LONG_TO_SHORT[__dow_range[0]]]
|
||||
end_day = DAYS_OF_WEEK_ISO_REVERSED[DAYS_OF_WEEK_LONG_TO_SHORT[__dow_range[1]]]
|
||||
except KeyError as e:
|
||||
raise ValueError(f"Invalid day of week entry found: {__dow_day}: {e}") from e
|
||||
# Check if this spans across the weekend (e.g., Fri-Mon)
|
||||
if start_day > end_day:
|
||||
# Handle weekend-spanning range: start_day to 7, then 1 to end_day
|
||||
__out_dow_days.extend(
|
||||
[
|
||||
(i, DAYS_OF_WEEK_ISO[i])
|
||||
for i in range(start_day, 8) # start_day to Sunday (7)
|
||||
]
|
||||
)
|
||||
__out_dow_days.extend(
|
||||
[
|
||||
(i, DAYS_OF_WEEK_ISO[i])
|
||||
for i in range(1, end_day + 1) # Monday (1) to end_day
|
||||
]
|
||||
)
|
||||
else:
|
||||
# Normal range: start_day to end_day
|
||||
__out_dow_days.extend(
|
||||
[
|
||||
(i, DAYS_OF_WEEK_ISO[i])
|
||||
for i in range(start_day, end_day + 1)
|
||||
]
|
||||
)
|
||||
else:
|
||||
try:
|
||||
__out_dow_days.append((DAYS_OF_WEEK_ISO_REVERSED[__dow_day], __dow_day))
|
||||
except KeyError as e:
|
||||
raise ValueError(f"Invalid day of week entry found: {__dow_day}: {e}") from e
|
||||
# if there are duplicates, alert
|
||||
if len(__out_dow_days) != len(set(__out_dow_days)):
|
||||
raise ValueError(f"Duplicate day of week entries found: {__out_dow_days}")
|
||||
|
||||
return __out_dow_days
|
||||
|
||||
|
||||
def parse_time_range(time_str: str, time_format: str = "%H:%M") -> tuple[time, time]:
|
||||
"""
|
||||
Parse a time range string in the format "HH:MM-HH:MM" and return a tuple of two time objects.
|
||||
|
||||
Arguments:
|
||||
time_str {str} -- The time range string to parse.
|
||||
|
||||
Raises:
|
||||
ValueError: Invalid time block set
|
||||
ValueError: Invalid time format
|
||||
ValueError: Start time must be before end time
|
||||
|
||||
Returns:
|
||||
tuple[time, time] -- start time, end time: leading zeros formattd
|
||||
"""
|
||||
__time_str = time_str.strip()
|
||||
# split by "-"
|
||||
__time_split = __time_str.split("-")
|
||||
if len(__time_split) != 2:
|
||||
raise ValueError(f"Invalid time block: {__time_str}")
|
||||
try:
|
||||
__time_start = datetime.strptime(__time_split[0], time_format).time()
|
||||
__time_end = datetime.strptime(__time_split[1], time_format).time()
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Invalid time block format [{__time_str}]: {e}") from e
|
||||
if __time_start >= __time_end:
|
||||
raise ValueError(f"Invalid time block set, start time after end time or equal: {__time_str}")
|
||||
|
||||
return __time_start, __time_end
|
||||
|
||||
|
||||
def times_overlap_or_connect(time1: tuple[time, time], time2: tuple[time, time], allow_touching: bool = False) -> bool:
|
||||
"""
|
||||
Check if two time ranges overlap or connect
|
||||
|
||||
Args:
|
||||
time1 (tuple): (start_time, end_time) for first range
|
||||
time2 (tuple): (start_time, end_time) for second range
|
||||
allow_touching (bool): If True, touching ranges (e.g., 8:00-10:00 and 10:00-12:00) are allowed
|
||||
|
||||
Returns:
|
||||
bool: True if ranges overlap or connect (based on allow_touching)
|
||||
"""
|
||||
start1, end1 = time1
|
||||
start2, end2 = time2
|
||||
|
||||
if allow_touching:
|
||||
# Only check for actual overlap (touching is OK)
|
||||
return start1 < end2 and start2 < end1
|
||||
# Check for overlap OR touching
|
||||
return start1 <= end2 and start2 <= end1
|
||||
|
||||
|
||||
def is_time_in_range(current_time: str, start_time: str, end_time: str) -> bool:
|
||||
"""
|
||||
Check if current_time is within start_time and end_time (inclusive)
|
||||
Time format: "HH:MM" (24-hour format)
|
||||
|
||||
Arguments:
|
||||
current_time {str} -- _description_
|
||||
start_time {str} -- _description_
|
||||
end_time {str} -- _description_
|
||||
|
||||
Returns:
|
||||
bool -- _description_
|
||||
"""
|
||||
# Convert string times to time objects
|
||||
current = datetime.strptime(current_time, "%H:%M:%S").time()
|
||||
start = datetime.strptime(start_time, "%H:%M:%S").time()
|
||||
end = datetime.strptime(end_time, "%H:%M:%S").time()
|
||||
|
||||
# Handle case where range crosses midnight (e.g., 22:00 to 06:00)
|
||||
if start <= end:
|
||||
# Normal case: start time is before end time
|
||||
return start <= current <= end
|
||||
# Crosses midnight: e.g., 22:00 to 06:00
|
||||
return current >= start or current <= end
|
||||
|
||||
|
||||
def reorder_weekdays_from_today(base_day: str) -> dict[int, str]:
|
||||
"""
|
||||
Reorder the days of the week starting from the specified base_day.
|
||||
|
||||
Arguments:
|
||||
base_day {str} -- The day to start the week from (e.g., "Mon").
|
||||
|
||||
Returns:
|
||||
dict[int, str] -- A dictionary mapping day numbers to day names.
|
||||
"""
|
||||
try:
|
||||
today_num = DAYS_OF_WEEK_ISO_REVERSED[base_day]
|
||||
except KeyError:
|
||||
try:
|
||||
today_num = DAYS_OF_WEEK_ISO_REVERSED[DAYS_OF_WEEK_LONG_TO_SHORT[base_day]]
|
||||
except KeyError as e:
|
||||
raise ValueError(f"Invalid day name provided: {base_day}: {e}") from e
|
||||
# Convert to list of tuples
|
||||
items = list(DAYS_OF_WEEK_ISO.items())
|
||||
# Reorder: from today onwards + from beginning to yesterday
|
||||
reordered_items = items[today_num - 1:] + items[:today_num - 1]
|
||||
|
||||
# Convert back to dictionary
|
||||
return dict(reordered_items)
|
||||
|
||||
# __END__
|
||||
@@ -1,10 +1,9 @@
|
||||
"""
|
||||
Current timestamp strings and time zones
|
||||
Convert timestamp strings with time units into seconds and vice versa.
|
||||
"""
|
||||
|
||||
from math import floor
|
||||
import re
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
from corelibs.var_handling.var_helpers import is_float
|
||||
|
||||
|
||||
@@ -16,30 +15,6 @@ class TimeUnitError(Exception):
|
||||
"""Custom exception for time parsing errors."""
|
||||
|
||||
|
||||
class TimestampStrings:
|
||||
"""
|
||||
set default time stamps
|
||||
"""
|
||||
|
||||
TIME_ZONE: str = 'Asia/Tokyo'
|
||||
|
||||
def __init__(self, time_zone: str | ZoneInfo | None = None):
|
||||
self.timestamp_now = datetime.now()
|
||||
# set time zone as string
|
||||
time_zone = time_zone if time_zone is not None else self.TIME_ZONE
|
||||
self.time_zone = str(time_zone) if not isinstance(time_zone, str) else time_zone
|
||||
# set ZoneInfo type
|
||||
try:
|
||||
self.time_zone_zi = ZoneInfo(self.time_zone)
|
||||
except ZoneInfoNotFoundError as e:
|
||||
raise ValueError(f'Zone could not be loaded [{self.time_zone}]: {e}') from e
|
||||
self.timestamp_now_tz = datetime.now(self.time_zone_zi)
|
||||
self.today = self.timestamp_now.strftime('%Y-%m-%d')
|
||||
self.timestamp = self.timestamp_now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
self.timestamp_tz = self.timestamp_now_tz.strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||
self.timestamp_file = self.timestamp_now.strftime("%Y-%m-%d_%H%M%S")
|
||||
|
||||
|
||||
def convert_to_seconds(time_string: str | int | float) -> int:
|
||||
"""
|
||||
Conver a string with time units into a seconds string
|
||||
@@ -124,7 +99,10 @@ def convert_to_seconds(time_string: str | int | float) -> int:
|
||||
def seconds_to_string(seconds: str | int | float, show_microseconds: bool = False) -> str:
|
||||
"""
|
||||
Convert seconds to compact human readable format (e.g., "1d 2h 3m 4.567s")
|
||||
Zero values are omitted.
|
||||
milliseconds if requested are added as fractional part of seconds.
|
||||
Supports negative values with "-" prefix
|
||||
if not int or float, will return as is
|
||||
|
||||
Args:
|
||||
seconds (float): Time in seconds (can be negative)
|
||||
@@ -172,4 +150,51 @@ def seconds_to_string(seconds: str | int | float, show_microseconds: bool = Fals
|
||||
result = " ".join(parts)
|
||||
return f"-{result}" if negative else result
|
||||
|
||||
|
||||
def convert_timestamp(timestamp: float | int | str, show_microseconds: bool = True) -> str:
|
||||
"""
|
||||
format timestamp into human readable format. This function will add 0 values between set values
|
||||
for example if we have 1d 1s it would output 1d 0h 0m 1s
|
||||
Milliseconds will be shown if set, and added with ms at the end
|
||||
Negative values will be prefixed with "-"
|
||||
if not int or float, will return as is
|
||||
|
||||
Arguments:
|
||||
timestamp {float} -- _description_
|
||||
|
||||
Keyword Arguments:
|
||||
show_micro {bool} -- _description_ (default: {True})
|
||||
|
||||
Returns:
|
||||
str -- _description_
|
||||
"""
|
||||
if not isinstance(timestamp, (int, float)):
|
||||
return timestamp
|
||||
# cut of the ms, but first round them up to four
|
||||
__timestamp_ms_split = str(round(timestamp, 4)).split(".")
|
||||
timestamp = int(__timestamp_ms_split[0])
|
||||
negative = timestamp < 0
|
||||
timestamp = abs(timestamp)
|
||||
try:
|
||||
ms = int(__timestamp_ms_split[1])
|
||||
except IndexError:
|
||||
ms = 0
|
||||
timegroups = (86400, 3600, 60, 1)
|
||||
output: list[int] = []
|
||||
for i in timegroups:
|
||||
output.append(int(floor(timestamp / i)))
|
||||
timestamp = timestamp % i
|
||||
# output has days|hours|min|sec ms
|
||||
time_string = ""
|
||||
if output[0]:
|
||||
time_string = f"{output[0]}d "
|
||||
if output[0] or output[1]:
|
||||
time_string += f"{output[1]}h "
|
||||
if output[0] or output[1] or output[2]:
|
||||
time_string += f"{output[2]}m "
|
||||
time_string += f"{output[3]}s"
|
||||
if show_microseconds:
|
||||
time_string += f" {ms}ms" if ms else " 0ms"
|
||||
return f"-{time_string}" if negative else time_string
|
||||
|
||||
# __END__
|
||||
32
src/corelibs/datetime_handling/timestamp_strings.py
Normal file
32
src/corelibs/datetime_handling/timestamp_strings.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
Current timestamp strings and time zones
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
|
||||
class TimestampStrings:
|
||||
"""
|
||||
set default time stamps
|
||||
"""
|
||||
|
||||
TIME_ZONE: str = 'Asia/Tokyo'
|
||||
|
||||
def __init__(self, time_zone: str | ZoneInfo | None = None):
|
||||
self.timestamp_now = datetime.now()
|
||||
# set time zone as string
|
||||
time_zone = time_zone if time_zone is not None else self.TIME_ZONE
|
||||
self.time_zone = str(time_zone) if not isinstance(time_zone, str) else time_zone
|
||||
# set ZoneInfo type
|
||||
try:
|
||||
self.time_zone_zi = ZoneInfo(self.time_zone)
|
||||
except ZoneInfoNotFoundError as e:
|
||||
raise ValueError(f'Zone could not be loaded [{self.time_zone}]: {e}') from e
|
||||
self.timestamp_now_tz = datetime.now(self.time_zone_zi)
|
||||
self.today = self.timestamp_now.strftime('%Y-%m-%d')
|
||||
self.timestamp = self.timestamp_now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
self.timestamp_tz = self.timestamp_now_tz.strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||
self.timestamp_file = self.timestamp_now.strftime("%Y-%m-%d_%H%M%S")
|
||||
|
||||
# __END__
|
||||
@@ -32,7 +32,7 @@ show_position(file pos optional)
|
||||
import time
|
||||
from typing import Literal
|
||||
from math import floor
|
||||
from corelibs.string_handling.datetime_helpers import convert_timestamp
|
||||
from corelibs.datetime_handling.datetime_helpers import convert_timestamp
|
||||
from corelibs.string_handling.byte_helpers import format_bytes
|
||||
|
||||
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
"""
|
||||
Various string based date/time helpers
|
||||
"""
|
||||
|
||||
from math import floor
|
||||
import time as time_t
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
|
||||
def convert_timestamp(timestamp: float | int, show_micro: bool = True) -> str:
|
||||
"""
|
||||
format timestamp into human readable format
|
||||
|
||||
Arguments:
|
||||
timestamp {float} -- _description_
|
||||
|
||||
Keyword Arguments:
|
||||
show_micro {bool} -- _description_ (default: {True})
|
||||
|
||||
Returns:
|
||||
str -- _description_
|
||||
"""
|
||||
# cut of the ms, but first round them up to four
|
||||
__timestamp_ms_split = str(round(timestamp, 4)).split(".")
|
||||
timestamp = int(__timestamp_ms_split[0])
|
||||
try:
|
||||
ms = int(__timestamp_ms_split[1])
|
||||
except IndexError:
|
||||
ms = 0
|
||||
timegroups = (86400, 3600, 60, 1)
|
||||
output: list[int] = []
|
||||
for i in timegroups:
|
||||
output.append(int(floor(timestamp / i)))
|
||||
timestamp = timestamp % i
|
||||
# output has days|hours|min|sec ms
|
||||
time_string = ""
|
||||
if output[0]:
|
||||
time_string = f"{output[0]}d"
|
||||
if output[0] or output[1]:
|
||||
time_string += f"{output[1]}h "
|
||||
if output[0] or output[1] or output[2]:
|
||||
time_string += f"{output[2]}m "
|
||||
time_string += f"{output[3]}s"
|
||||
if show_micro:
|
||||
time_string += f" {ms}ms" if ms else " 0ms"
|
||||
return time_string
|
||||
|
||||
|
||||
def create_time(timestamp: float, timestamp_format: str = "%Y-%m-%d %H:%M:%S") -> str:
|
||||
"""
|
||||
just takes a timestamp and prints out humand readable format
|
||||
|
||||
Arguments:
|
||||
timestamp {float} -- _description_
|
||||
|
||||
Keyword Arguments:
|
||||
timestamp_format {_type_} -- _description_ (default: {"%Y-%m-%d %H:%M:%S"})
|
||||
|
||||
Returns:
|
||||
str -- _description_
|
||||
"""
|
||||
return time_t.strftime(timestamp_format, time_t.localtime(timestamp))
|
||||
|
||||
|
||||
def get_system_timezone():
|
||||
"""Get system timezone using datetime's automatic detection"""
|
||||
# Get current time with system timezone
|
||||
local_time = datetime.now().astimezone()
|
||||
|
||||
# Extract timezone info
|
||||
system_tz = local_time.tzinfo
|
||||
timezone_name = str(system_tz)
|
||||
|
||||
return system_tz, timezone_name
|
||||
|
||||
|
||||
def parse_timezone_data(timezone_tz: str = '') -> ZoneInfo:
|
||||
"""
|
||||
parses a string to get the ZoneInfo
|
||||
If not set or not valid gets local time,
|
||||
if that is not possible get UTC
|
||||
|
||||
Keyword Arguments:
|
||||
timezone_tz {str} -- _description_ (default: {''})
|
||||
|
||||
Returns:
|
||||
ZoneInfo -- _description_
|
||||
"""
|
||||
try:
|
||||
return ZoneInfo(timezone_tz)
|
||||
except (ZoneInfoNotFoundError, ValueError, TypeError):
|
||||
# use default
|
||||
time_tz, time_tz_str = get_system_timezone()
|
||||
if time_tz is None:
|
||||
return ZoneInfo('UTC')
|
||||
# TODO build proper TZ lookup
|
||||
tz_mapping = {
|
||||
'JST': 'Asia/Tokyo',
|
||||
'KST': 'Asia/Seoul',
|
||||
'IST': 'Asia/Kolkata',
|
||||
'CST': 'Asia/Shanghai', # Default to China for CST
|
||||
'AEST': 'Australia/Sydney',
|
||||
'AWST': 'Australia/Perth',
|
||||
'EST': 'America/New_York',
|
||||
'EDT': 'America/New_York',
|
||||
'CDT': 'America/Chicago',
|
||||
'MST': 'America/Denver',
|
||||
'MDT': 'America/Denver',
|
||||
'PST': 'America/Los_Angeles',
|
||||
'PDT': 'America/Los_Angeles',
|
||||
'GMT': 'UTC',
|
||||
'UTC': 'UTC',
|
||||
'CET': 'Europe/Berlin',
|
||||
'CEST': 'Europe/Berlin',
|
||||
'BST': 'Europe/London',
|
||||
}
|
||||
try:
|
||||
return ZoneInfo(tz_mapping[time_tz_str])
|
||||
except (ZoneInfoNotFoundError, IndexError) as e:
|
||||
raise ValueError(f"No mapping for {time_tz_str}: {e}") from e
|
||||
|
||||
|
||||
def get_datetime_iso8601(timezone_tz: str | ZoneInfo = '', sep: str = 'T', timespec: str = 'microseconds') -> str:
|
||||
"""
|
||||
set a datetime in the iso8601 format with microseconds
|
||||
|
||||
Returns:
|
||||
str -- _description_
|
||||
"""
|
||||
# parse if this is a string
|
||||
if isinstance(timezone_tz, str):
|
||||
timezone_tz = parse_timezone_data(timezone_tz)
|
||||
return datetime.now(timezone_tz).isoformat(sep=sep, timespec=timespec)
|
||||
|
||||
# __END__
|
||||
Reference in New Issue
Block a user