Compare commits

...

18 Commits

Author SHA1 Message Date
Clemens Schwaighofer
c16b086467 v0.39.1: Skip Log format update if it did not change 2025-11-19 17:44:44 +09:00
Clemens Schwaighofer
0ad8883809 v0.39.0: Add Log LEVEL flag for console format 2025-11-19 17:37:00 +09:00
Clemens Schwaighofer
51e9b1ce7c Add "LEVEL" option to console log format
So we can set output to onle the message without any information (NONE),
only level (BARE), time and level (MINIMAL), time, file, line and level (CONDENSED) or
(ALL) full information.
2025-11-19 17:35:27 +09:00
Clemens Schwaighofer
0d3104f60a v0.38.0: Log console format update 2025-11-19 15:45:49 +09:00
Clemens Schwaighofer
d29f827fc9 Add a function to Log system to update the console formatter dynamically. 2025-11-19 15:17:25 +09:00
Clemens Schwaighofer
282fe1f7c0 v0.37.0: Log add from lookup for strings in Console config, move var helpers, datetime, enum to stand alone libs 2025-11-19 13:48:29 +09:00
Clemens Schwaighofer
afce5043e4 Cleanup other functions to use extern corelibs
Remove tests for parts that have moved to stand alone libraries
2025-11-19 13:46:34 +09:00
Clemens Schwaighofer
5996bb1fc0 Add Log ConsoleFormatSettings.from_string static method to get settings by name with default option
To help set from config or command line with fallback
2025-11-19 13:45:26 +09:00
Clemens Schwaighofer
06a17d7c30 Switch datetime handling, var handling to corelibs libraries
Use external corelib libraries for datetime handling and var handling enum base.
2025-11-19 13:13:32 +09:00
Clemens Schwaighofer
af7633183c v0.36.0: Log console format settings with bitwise mask 2025-11-19 11:31:50 +09:00
Clemens Schwaighofer
1280b2f855 Log switch to bitwise flag settings for console format type
Has the following settings
TIME, TIME_SECONDS, TIME_MILLISECONDS, TIME_MICROSECONDS: enable time output in different formats
TIME and TIME_MILLISECONDS are equivalent, if multiple are set the smallest precision wins
TIMEZONE: add time zone to time output
NAME: log group name
FILE: short file name
FUNCTION: function name
LINENO: line number

There is a class with quick grouped settings
ConsoleFormatSettings
ALL: all options enabled, time is in milliseconds
CONDENSED: time without time zone, file and line number
MINIMAL: only time without time zone
BARE: only the message, no other info
2025-11-19 11:25:49 +09:00
Clemens Schwaighofer
2e0b1f5951 v0.35.2: Sync miss for Log file format change 2025-11-18 15:55:49 +09:00
Clemens Schwaighofer
548d7491b8 Merge branch 'development' 2025-11-18 15:55:05 +09:00
Clemens Schwaighofer
ad99115544 v0.35.1: Log move pid into path name block, remove double filename 2025-11-18 15:51:29 +09:00
Clemens Schwaighofer
52919cbc49 Log: move process id to front of pathname in log format
The previous filename:pid has been removed, the filename is part of the pathname.
No need for double filename info and wasting space in the log line.
2025-11-18 15:49:46 +09:00
Clemens Schwaighofer
7f2dc13c31 v0.35.0: Logging update with output format settings for console logging 2025-11-18 15:37:13 +09:00
Clemens Schwaighofer
592652cff1 Update logging with console output format changes
"console_format_type" with "normal", "condensed", "minimal" options
This sets the format of the console output, controlling the amount of detail shown.
normal show log title, file, function and line number
condensed show file and line number only
minimal shows only timestamp, log level and message
Default is normal

"console_iso_precision" with "seconds", "milliseconds", "microseconds" options
This sets the precision of the ISO timestamp in console logs.
Default is milliseconds

The timestamp output is now ISO8601 formatatted with time zone.
2025-11-18 15:31:16 +09:00
Clemens Schwaighofer
6a1724695e Fix pyproject settings by removing explicit=true 2025-11-11 18:05:07 +09:00
28 changed files with 895 additions and 2905 deletions

View File

@@ -4,6 +4,7 @@
> This is pre-production, location of methods and names of paths can change
>
> This will be split up into modules per file and this will be just a collection holder
> See [Deprecated](#deprecated) below
This is a pip package that can be installed into any project and covers the following parts
@@ -42,6 +43,15 @@ This is a pip package that can be installed into any project and covers the foll
- csv_handling/csv_interface: The CSV DictWriter interface is just in a very basic way implemented
- script_handling/script_helpers: No idea if there is need for this, tests are written but not finished
## Deprecated
All content in this module will move to stand alone libraries, as of now the following entries have moved and will throw deprecated warnings if used
- var_handling.enum_base: corelibs-enum-base
- var_handling.var_helpers: corelibs-var
- datetime_handling: corelibs-datetime
- string_handling.text_colors: corelibs-text-colors
## UV setup
uv must be [installed](https://docs.astral.sh/uv/getting-started/installation/)

View File

@@ -1,17 +1,25 @@
# MARK: Project info
[project]
name = "corelibs"
version = "0.34.0"
version = "0.39.1"
description = "Collection of utils for Python scripts"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"corelibs-datetime>=1.0.1",
"corelibs-enum-base>=1.0.0",
"corelibs-var>=1.0.0",
"cryptography>=46.0.3",
"jmespath>=1.0.1",
"jsonpath-ng>=1.7.0",
"psutil>=7.0.0",
"requests>=2.32.4",
]
# MARK: build system
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
# set this to disable publish to pypi (pip)
# classifiers = ["Private :: Do Not Upload"]
@@ -20,12 +28,11 @@ dependencies = [
name = "opj-pypi"
url = "https://git.egplusww.jp/api/packages/PyPI/pypi/simple/"
publish-url = "https://git.egplusww.jp/api/packages/PyPI/pypi"
explicit = true
# MARK: build system
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.uv.sources]
corelibs-enum-base = { index = "opj-pypi" }
corelibs-datetime = { index = "opj-pypi" }
corelibs-var = { index = "opj-pypi" }
[dependency-groups]
dev = [

View File

@@ -8,9 +8,9 @@ import re
import configparser
from typing import Any, Tuple, Sequence, cast
from pathlib import Path
from corelibs_var.var_helpers import is_int, is_float, str_to_bool
from corelibs.logging_handling.log import Log
from corelibs.iterator_handling.list_helpers import convert_to_list, is_list_in_list
from corelibs.var_handling.var_helpers import is_int, is_float, str_to_bool
from corelibs.config_handling.settings_loader_handling.settings_loader_check import SettingsLoaderCheck

View File

@@ -2,26 +2,13 @@
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()}
from warnings import deprecated
from zoneinfo import ZoneInfo
from corelibs_datetime import datetime_helpers
@deprecated("Use corelibs_datetime.datetime_helpers.create_time instead")
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
@@ -35,21 +22,17 @@ def create_time(timestamp: float, timestamp_format: str = "%Y-%m-%d %H:%M:%S") -
Returns:
str -- _description_
"""
return time_t.strftime(timestamp_format, time_t.localtime(timestamp))
return datetime_helpers.create_time(timestamp, timestamp_format)
@deprecated("Use corelibs_datetime.datetime_helpers.get_system_timezone instead")
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
return datetime_helpers.get_system_timezone()
@deprecated("Use corelibs_datetime.datetime_helpers.parse_timezone_data instead")
def parse_timezone_data(timezone_tz: str = '') -> ZoneInfo:
"""
parses a string to get the ZoneInfo
@@ -62,40 +45,10 @@ def parse_timezone_data(timezone_tz: str = '') -> ZoneInfo:
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
return datetime_helpers.parse_timezone_data(timezone_tz)
@deprecated("Use corelibs_datetime.datetime_helpers.get_datetime_iso8601 instead")
def get_datetime_iso8601(timezone_tz: str | ZoneInfo = '', sep: str = 'T', timespec: str = 'microseconds') -> str:
"""
set a datetime in the iso8601 format with microseconds
@@ -103,12 +56,13 @@ def get_datetime_iso8601(timezone_tz: str | ZoneInfo = '', sep: str = 'T', times
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)
try:
return datetime_helpers.get_datetime_iso8601(timezone_tz, sep, timespec)
except KeyError as e:
raise ValueError(f"Deprecated ValueError, change to KeyError: {e}") from e
@deprecated("Use corelibs_datetime.datetime_helpers.validate_date instead")
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
@@ -119,20 +73,10 @@ def validate_date(date: str, not_before: datetime | None = None, not_after: date
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
return datetime_helpers.validate_date(date, not_before, not_after)
@deprecated("Use corelibs_datetime.datetime_helpers.parse_flexible_date instead")
def parse_flexible_date(
date_str: str,
timezone_tz: str | ZoneInfo | None = None,
@@ -154,49 +98,14 @@ def parse_flexible_date(
Returns:
datetime | None -- _description_
"""
date_str = date_str.strip()
# Try different parsing methods
parsers: list[Callable[[str], datetime]] = [
# ISO 8601 format, also with missing "T"
lambda x: datetime.fromisoformat(x), # pylint: disable=W0108
lambda x: datetime.fromisoformat(x.replace(' ', 'T')), # pylint: disable=W0108
# Simple date format
lambda x: datetime.strptime(x, "%Y-%m-%d"),
# datetime without T
lambda x: datetime.strptime(x, "%Y-%m-%d %H:%M:%S"),
lambda x: datetime.strptime(x, "%Y-%m-%d %H:%M:%S.%f"),
# 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
return datetime_helpers.parse_flexible_date(
date_str,
timezone_tz,
shift_time_zone
)
@deprecated("Use corelibs_datetime.datetime_helpers.compare_dates instead")
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
@@ -210,23 +119,10 @@ def compare_dates(date1_str: str, date2_str: str) -> None | bool:
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
return datetime_helpers.compare_dates(date1_str, date2_str)
@deprecated("Use corelibs_datetime.datetime_helpers.find_newest_datetime_in_list instead")
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.
@@ -238,31 +134,10 @@ def find_newest_datetime_in_list(date_list: list[str]) -> None | str:
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
return datetime_helpers.find_newest_datetime_in_list(date_list)
@deprecated("Use corelibs_datetime.datetime_helpers.parse_day_of_week_range instead")
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.
@@ -279,59 +154,13 @@ def parse_day_of_week_range(dow_days: str) -> list[tuple[int, str]]:
"""
# 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
try:
return datetime_helpers.parse_day_of_week_range(dow_days)
except KeyError as e:
raise ValueError(f"Deprecated ValueError, change to KeyError: {e}") from e
@deprecated("Use corelibs_datetime.datetime_helpers.parse_time_range instead")
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.
@@ -347,22 +176,13 @@ def parse_time_range(time_str: str, time_format: str = "%H:%M") -> tuple[time, t
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
return datetime_helpers.parse_time_range(time_str, time_format)
except KeyError as e:
raise ValueError(f"Deprecated ValueError, change to KeyError: {e}") from e
@deprecated("Use corelibs_datetime.datetime_helpers.times_overlap_or_connect instead")
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
@@ -375,16 +195,10 @@ def times_overlap_or_connect(time1: tuple[time, time], time2: tuple[time, time],
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
return datetime_helpers.times_overlap_or_connect(time1, time2, allow_touching)
@deprecated("Use corelibs_datetime.datetime_helpers.is_time_in_range instead")
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)
@@ -399,18 +213,10 @@ def is_time_in_range(current_time: str, start_time: str, end_time: str) -> bool:
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
return datetime_helpers.is_time_in_range(current_time, start_time, end_time)
@deprecated("Use corelibs_datetime.datetime_helpers.reorder_weekdays_from_today instead")
def reorder_weekdays_from_today(base_day: str) -> dict[int, str]:
"""
Reorder the days of the week starting from the specified base_day.
@@ -422,18 +228,8 @@ def reorder_weekdays_from_today(base_day: str) -> dict[int, str]:
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)
return datetime_helpers.reorder_weekdays_from_today(base_day)
except KeyError as e:
raise ValueError(f"Deprecated ValueError, change to KeyError: {e}") from e
# __END__

View File

@@ -2,19 +2,22 @@
Convert timestamp strings with time units into seconds and vice versa.
"""
from math import floor
import re
from corelibs.var_handling.var_helpers import is_float
from warnings import deprecated
from corelibs_datetime import timestamp_convert
from corelibs_datetime.timestamp_convert import TimeParseError as NewTimeParseError, TimeUnitError as NewTimeUnitError
@deprecated("Use corelibs_datetime.timestamp_convert.TimeParseError instead")
class TimeParseError(Exception):
"""Custom exception for time parsing errors."""
@deprecated("Use corelibs_datetime.timestamp_convert.TimeUnitError instead")
class TimeUnitError(Exception):
"""Custom exception for time parsing errors."""
@deprecated("Use corelibs_datetime.timestamp_convert.convert_to_seconds instead")
def convert_to_seconds(time_string: str | int | float) -> int:
"""
Conver a string with time units into a seconds string
@@ -35,67 +38,15 @@ def convert_to_seconds(time_string: str | int | float) -> int:
# skip out if this is a number of any type
# numbers will br made float, rounded and then converted to int
if is_float(time_string):
return int(round(float(time_string)))
time_string = str(time_string)
# Check if the time string is negative
negative = time_string.startswith('-')
if negative:
time_string = time_string[1:] # Remove the negative sign for processing
# Define time unit conversion factors
unit_factors: dict[str, int] = {
'Y': 31536000, # 365 days * 86400 seconds/day
'M': 2592000 * 12, # 1 year in seconds (assuming 365 days per year)
'd': 86400, # 1 day in seconds
'h': 3600, # 1 hour in seconds
'm': 60, # minutes to seconds
's': 1 # 1 second in seconds
}
long_unit_names: dict[str, str] = {
'year': 'Y',
'years': 'Y',
'month': 'M',
'months': 'M',
'day': 'd',
'days': 'd',
'hour': 'h',
'hours': 'h',
'minute': 'm',
'minutes': 'm',
'min': 'm',
'second': 's',
'seconds': 's',
'sec': 's',
}
total_seconds = 0
seen_units: list[str] = [] # Track units that have been encountered
# Use regex to match number and time unit pairs
for match in re.finditer(r'(\d+)\s*([a-zA-Z]+)', time_string):
value, unit = int(match.group(1)), match.group(2)
# full name check, fallback to original name
unit = long_unit_names.get(unit.lower(), unit)
# Check for duplicate units
if unit in seen_units:
raise TimeParseError(f"Unit '{unit}' appears more than once.")
# Check invalid unit
if unit not in unit_factors:
raise TimeUnitError(f"Unit '{unit}' is not a valid unit name.")
# Add to total seconds based on the units
if unit in unit_factors:
total_seconds += value * unit_factors[unit]
seen_units.append(unit)
return -total_seconds if negative else total_seconds
try:
return timestamp_convert.convert_to_seconds(time_string)
except NewTimeParseError as e:
raise TimeParseError(f"Deprecated, use corelibs_datetime.timestamp_convert.TimeParseError: {e}") from e
except NewTimeUnitError as e:
raise TimeUnitError(f"Deprecated, use corelibs_datetime.timestamp_convert.TimeUnitError: {e}") from e
@deprecated("Use corelibs_datetime.timestamp_convert.seconds_to_string instead")
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")
@@ -111,46 +62,10 @@ def seconds_to_string(seconds: str | int | float, show_microseconds: bool = Fals
Returns:
str: Compact human readable time format
"""
# if not int or float, return as is
if not isinstance(seconds, (int, float)):
return seconds
# Handle negative values
negative = seconds < 0
seconds = abs(seconds)
whole_seconds = int(seconds)
fractional = seconds - whole_seconds
days = whole_seconds // 86400
hours = (whole_seconds % 86400) // 3600
minutes = (whole_seconds % 3600) // 60
secs = whole_seconds % 60
parts: list[str] = []
if days > 0:
parts.append(f"{days}d")
if hours > 0:
parts.append(f"{hours}h")
if minutes > 0:
parts.append(f"{minutes}m")
# Handle seconds with fractional part
if fractional > 0:
if show_microseconds:
total_seconds = secs + fractional
formatted = f"{total_seconds:.6f}".rstrip('0').rstrip('.')
parts.append(f"{formatted}s")
else:
total_seconds = secs + fractional
formatted = f"{total_seconds:.3f}".rstrip('0').rstrip('.')
parts.append(f"{formatted}s")
elif secs > 0 or not parts:
parts.append(f"{secs}s")
result = " ".join(parts)
return f"-{result}" if negative else result
return timestamp_convert.seconds_to_string(seconds, show_microseconds)
@deprecated("Use corelibs_datetime.timestamp_convert.convert_timestamp instead")
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
@@ -168,33 +83,6 @@ def convert_timestamp(timestamp: float | int | str, show_microseconds: bool = Tr
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
return timestamp_convert.convert_timestamp(timestamp, show_microseconds)
# __END__

View File

@@ -2,31 +2,20 @@
Current timestamp strings and time zones
"""
from datetime import datetime
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from warnings import deprecated
from zoneinfo import ZoneInfo
from corelibs_datetime import timestamp_strings
class TimestampStrings:
class TimestampStrings(timestamp_strings.TimestampStrings):
"""
set default time stamps
"""
TIME_ZONE: str = 'Asia/Tokyo'
@deprecated("Use corelibs_datetime.timestamp_strings.TimestampStrings instead")
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")
super().__init__(time_zone)
# __END__

View File

@@ -11,6 +11,7 @@ from datetime import datetime
import time
from pathlib import Path
import atexit
from enum import Flag, auto
from typing import MutableMapping, TextIO, TypedDict, Any, TYPE_CHECKING, cast
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
from corelibs.string_handling.text_colors import Colors
@@ -20,6 +21,58 @@ if TYPE_CHECKING:
from multiprocessing import Queue
class ConsoleFormat(Flag):
"""console format type bitmap flags"""
TIME = auto()
TIME_SECONDS = auto()
TIME_MILLISECONDS = auto()
TIME_MICROSECONDS = auto()
TIMEZONE = auto()
NAME = auto()
FILE = auto()
FUNCTION = auto()
LINENO = auto()
LEVEL = auto()
class ConsoleFormatSettings:
"""Console format quick settings groups"""
# shows everything, time with milliseconds, and time zone, log name, file, function, line number
ALL = (
ConsoleFormat.TIME |
ConsoleFormat.TIMEZONE |
ConsoleFormat.NAME |
ConsoleFormat.FILE |
ConsoleFormat.FUNCTION |
ConsoleFormat.LINENO |
ConsoleFormat.LEVEL
)
# show time with no time zone, file, line and level
CONDENSED = ConsoleFormat.TIME | ConsoleFormat.FILE | ConsoleFormat.LINENO | ConsoleFormat.LEVEL
# only time and level
MINIMAL = ConsoleFormat.TIME | ConsoleFormat.LEVEL
# only level
BARE = ConsoleFormat.LEVEL
# only message
NONE = ConsoleFormat(0)
@staticmethod
def from_string(setting_str: str, default: ConsoleFormat | None = None) -> ConsoleFormat | None:
"""
Get a console format setting, if does not exist set to None
Arguments:
setting_str {str} -- what to search for
default {ConsoleFormat | None} -- if not found return this (default: {None})
Returns:
ConsoleFormat | None -- found ConsoleFormat or None
"""
if hasattr(ConsoleFormatSettings, setting_str):
return getattr(ConsoleFormatSettings, setting_str)
return default
# MARK: Log settings TypedDict
class LogSettings(TypedDict):
"""log settings, for Log setup"""
@@ -28,6 +81,7 @@ class LogSettings(TypedDict):
per_run_log: bool
console_enabled: bool
console_color_output_enabled: bool
console_format_type: ConsoleFormat
add_start_info: bool
add_end_info: bool
log_queue: 'Queue[str] | None'
@@ -395,6 +449,9 @@ class Log(LogParent):
logger setup
"""
CONSOLE_HANDLER: str = 'stream_handler'
FILE_HANDLER: str = 'file_handler'
# spacer lenght characters and the character
SPACER_CHAR: str = '='
SPACER_LENGTH: int = 32
@@ -409,6 +466,8 @@ class Log(LogParent):
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": True,
# do not print log title, file, function and line number
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": True,
"add_end_info": False,
"log_queue": None,
@@ -419,7 +478,10 @@ class Log(LogParent):
self,
log_path: Path,
log_name: str,
log_settings: dict[str, 'LoggingLevel | str | bool | None | Queue[str]'] | LogSettings | None = None,
log_settings: (
dict[str, 'LoggingLevel | str | bool | None | Queue[str] | ConsoleFormat'] | # noqa: E501 # pylint: disable=line-too-long
LogSettings | None
) = None,
other_handlers: dict[str, Any] | None = None
):
LogParent.__init__(self)
@@ -455,14 +517,16 @@ class Log(LogParent):
# in the file writer too, for the ones where color is set BEFORE the format
# Any is logging.StreamHandler, logging.FileHandler and all logging.handlers.*
self.handlers: dict[str, Any] = {}
self.add_handler('file_handler', self.__create_file_handler(
'file_handler', self.log_settings['log_level_file'], log_path)
self.add_handler(self.FILE_HANDLER, self.__create_file_handler(
self.FILE_HANDLER, self.log_settings['log_level_file'], log_path)
)
if self.log_settings['console_enabled']:
# console
self.add_handler('stream_handler', self.__create_console_handler(
'stream_handler', self.log_settings['log_level_console'])
)
self.add_handler(self.CONSOLE_HANDLER, self.__create_console_handler(
self.CONSOLE_HANDLER,
self.log_settings['log_level_console'],
console_format_type=self.log_settings['console_format_type'],
))
# add other handlers,
if other_handlers is not None:
for handler_key, handler in other_handlers.items():
@@ -488,7 +552,8 @@ class Log(LogParent):
# MARK: parse log settings
def __parse_log_settings(
self,
log_settings: dict[str, 'LoggingLevel | str | bool | None | Queue[str]'] | LogSettings | None
log_settings: dict[str, 'LoggingLevel | str | bool | None | Queue[str] | ConsoleFormat'] | # noqa: E501 # pylint: disable=line-too-long
LogSettings | None
) -> LogSettings:
# skip with defaul it not set
if log_settings is None:
@@ -518,6 +583,10 @@ class Log(LogParent):
if not isinstance(__setting := log_settings.get(__log_entry, ''), bool):
__setting = self.DEFAULT_LOG_SETTINGS.get(__log_entry, True)
default_log_settings[__log_entry] = __setting
# check console log type
default_log_settings['console_format_type'] = cast('ConsoleFormat', log_settings.get(
'console_format_type', self.DEFAULT_LOG_SETTINGS['console_format_type']
))
# check log queue
__setting = log_settings.get('log_queue', self.DEFAULT_LOG_SETTINGS['log_queue'])
if __setting is not None:
@@ -551,34 +620,167 @@ class Log(LogParent):
self.handlers[handler_name] = handler
return True
# MARK: console logger format
def __build_console_format_from_string(self, console_format_type: ConsoleFormat) -> str:
"""
Build console format string from the given console format type
Arguments:
console_format_type {ConsoleFormat} -- _description_
Returns:
str -- _description_
"""
format_string = ''
# time part if any of the times are requested
if (
ConsoleFormat.TIME in console_format_type or
ConsoleFormat.TIME_SECONDS in console_format_type or
ConsoleFormat.TIME_MILLISECONDS in console_format_type or
ConsoleFormat.TIME_MICROSECONDS in console_format_type
):
format_string += '[%(asctime)s] '
# set log name
if ConsoleFormat.NAME in console_format_type:
format_string += '[%(name)s] '
# for any file/function/line number call
if (
ConsoleFormat.FILE in console_format_type or
ConsoleFormat.FUNCTION in console_format_type or
ConsoleFormat.LINENO in console_format_type
):
format_string += '['
set_group: list[str] = []
if ConsoleFormat.FILE in console_format_type:
set_group.append('%(filename)s')
if ConsoleFormat.FUNCTION in console_format_type:
set_group.append('%(funcName)s')
if ConsoleFormat.LINENO in console_format_type:
set_group.append('%(lineno)d')
format_string += ':'.join(set_group)
format_string += '] '
# level if wanted
if ConsoleFormat.LEVEL in console_format_type:
format_string += '<%(levelname)s> '
# always message
format_string += '%(message)s'
return format_string
def __set_time_format_for_console_formatter(
self, formatter_console: CustomConsoleFormatter | logging.Formatter, console_format_type: ConsoleFormat
) -> None:
"""
Format time for a given format handler, this is for console format only
Arguments:
formatter_console {CustomConsoleFormatter | logging.Formatter} -- _description_
console_format_type {ConsoleFormat} -- _description_
"""
# default for TIME is milliseconds
# if we have multiple set, the smallest precision wins
if ConsoleFormat.TIME_MICROSECONDS in console_format_type:
iso_precision = 'microseconds'
elif (
ConsoleFormat.TIME_MILLISECONDS in console_format_type or
ConsoleFormat.TIME in console_format_type
):
iso_precision = 'milliseconds'
elif ConsoleFormat.TIME_SECONDS in console_format_type:
iso_precision = 'seconds'
else:
iso_precision = 'milliseconds'
# do timestamp modification only if we have time requested
if (
ConsoleFormat.TIME in console_format_type or
ConsoleFormat.TIME_SECONDS in console_format_type or
ConsoleFormat.TIME_MILLISECONDS in console_format_type or
ConsoleFormat.TIME_MICROSECONDS in console_format_type
):
# if we have with TZ we as the asttimezone call
if ConsoleFormat.TIMEZONE in console_format_type:
formatter_console.formatTime = (
lambda record, datefmt=None:
datetime
.fromtimestamp(record.created)
.astimezone()
.isoformat(sep=" ", timespec=iso_precision)
)
else:
formatter_console.formatTime = (
lambda record, datefmt=None:
datetime
.fromtimestamp(record.created)
.isoformat(sep=" ", timespec=iso_precision)
)
def __set_console_formatter(self, console_format_type: ConsoleFormat) -> CustomConsoleFormatter | logging.Formatter:
"""
Build the full formatter and return it
Arguments:
console_format_type {ConsoleFormat} -- _description_
Returns:
CustomConsoleFormatter | logging.Formatter -- _description_
"""
format_string = self.__build_console_format_from_string(console_format_type)
if self.log_settings['console_color_output_enabled']:
# formatter_console = CustomConsoleFormatter(format_string, datefmt=format_date)
formatter_console = CustomConsoleFormatter(format_string)
else:
# formatter_console = logging.Formatter(format_string, datefmt=format_date)
formatter_console = logging.Formatter(format_string)
self.__set_time_format_for_console_formatter(formatter_console, console_format_type)
return formatter_console
# MARK: console handler update
def update_console_formatter(
self,
console_format_type: ConsoleFormat,
):
"""
Update the console formatter for format layout and time stamp format
Arguments:
console_format_type {ConsoleFormat} -- _description_
"""
if not self.log_settings['console_enabled']:
return
# update the formatter
self.handlers[self.CONSOLE_HANDLER].setFormatter(
self.__set_console_formatter(console_format_type)
)
# MARK: console handler
def __create_console_handler(
self, handler_name: str,
log_level_console: LoggingLevel = LoggingLevel.WARNING, filter_exceptions: bool = True
log_level_console: LoggingLevel = LoggingLevel.WARNING,
filter_exceptions: bool = True,
console_format_type: ConsoleFormat = ConsoleFormatSettings.ALL,
) -> logging.StreamHandler[TextIO]:
# console logger
if not self.validate_log_level(log_level_console):
log_level_console = self.DEFAULT_LOG_LEVEL_CONSOLE
console_handler = logging.StreamHandler()
# format layouts
format_string = (
'[%(asctime)s.%(msecs)03d] '
'[%(name)s] '
'[%(filename)s:%(funcName)s:%(lineno)d] '
'<%(levelname)s> '
'%(message)s'
)
format_date = "%Y-%m-%d %H:%M:%S"
# color or not
if self.log_settings['console_color_output_enabled']:
formatter_console = CustomConsoleFormatter(format_string, datefmt=format_date)
else:
formatter_console = logging.Formatter(format_string, datefmt=format_date)
# print(f"Console format type: {console_format_type}")
# build the format string based on what flags are set
# format_string = self.__build_console_format_from_string(console_format_type)
# # basic date, but this will be overridden to ISO in formatTime
# # format_date = "%Y-%m-%d %H:%M:%S"
# # color or not
# if self.log_settings['console_color_output_enabled']:
# # formatter_console = CustomConsoleFormatter(format_string, datefmt=format_date)
# formatter_console = CustomConsoleFormatter(format_string)
# else:
# # formatter_console = logging.Formatter(format_string, datefmt=format_date)
# formatter_console = logging.Formatter(format_string)
# # set the time format
# self.__set_time_format_for_console_formatter(formatter_console, console_format_type)
console_handler.set_name(handler_name)
console_handler.setLevel(log_level_console.name)
# do not show exceptions logs on console
console_handler.addFilter(CustomHandlerFilter('console', filter_exceptions))
console_handler.setFormatter(formatter_console)
console_handler.setFormatter(self.__set_console_formatter(console_format_type))
return console_handler
# MARK: file handler
@@ -614,13 +816,14 @@ class Log(LogParent):
formatter_file_handler = logging.Formatter(
(
# time stamp
'[%(asctime)s.%(msecs)03d] '
# '[%(asctime)s.%(msecs)03d] '
'[%(asctime)s] '
# log name
'[%(name)s] '
# filename + pid
'[%(filename)s:%(process)d] '
# path + func + line number
'[%(pathname)s:%(funcName)s:%(lineno)d] '
# '[%(filename)s:%(process)d] '
# pid + path/filename + func + line number
'[%(process)d:%(pathname)s:%(funcName)s:%(lineno)d] '
# error level
'<%(levelname)s> '
# message
@@ -628,6 +831,13 @@ class Log(LogParent):
),
datefmt="%Y-%m-%dT%H:%M:%S",
)
formatter_file_handler.formatTime = (
lambda record, datefmt=None:
datetime
.fromtimestamp(record.created)
.astimezone()
.isoformat(sep="T", timespec="microseconds")
)
file_handler.set_name(handler_name)
file_handler.setLevel(log_level_file.name)
# do not show errors flagged with console (they are from exceptions)

View File

@@ -32,7 +32,7 @@ show_position(file pos optional)
import time
from typing import Literal
from math import floor
from corelibs.datetime_handling.timestamp_convert import convert_timestamp
from corelibs_datetime.timestamp_convert import convert_timestamp
from corelibs.string_handling.byte_helpers import format_bytes

View File

@@ -3,7 +3,9 @@ Enum base classes
"""
from enum import Enum
from warnings import deprecated
from typing import Any
# from corelibs_enum_base.enum_base import EnumBase as CorelibsEnumBase
class EnumBase(Enum):
@@ -14,6 +16,7 @@ class EnumBase(Enum):
"""
@classmethod
@deprecated("Use corelibs_enum_base.EnumBase instead")
def lookup_key(cls, enum_key: str):
"""Lookup from key side (must be string)"""
# if there is a ":", then this is legacy, replace with ___
@@ -27,6 +30,7 @@ class EnumBase(Enum):
raise ValueError(f"Invalid key: {enum_key}") from e
@classmethod
@deprecated("Use corelibs_enum_base.EnumBase instead")
def lookup_value(cls, enum_value: Any):
"""Lookup through value side"""
try:
@@ -35,6 +39,7 @@ class EnumBase(Enum):
raise ValueError(f"Invalid value: {enum_value}") from e
@classmethod
@deprecated("Use corelibs_enum_base.EnumBase instead")
def from_any(cls, enum_any: Any):
"""
This only works in the following order
@@ -62,14 +67,17 @@ class EnumBase(Enum):
raise ValueError(f"Could not find as key or value: {enum_any}") from e
return cls.lookup_value(enum_any)
@deprecated("Use corelibs_enum_base.EnumBase instead")
def to_value(self) -> Any:
"""Convert to value"""
return self.value
@deprecated("Use corelibs_enum_base.EnumBase instead")
def to_lower_case(self) -> str:
"""return lower case"""
return self.name.lower()
@deprecated("Use corelibs_enum_base.EnumBase instead")
def __str__(self) -> str:
"""return [Enum].NAME like it was called with .name"""
return self.name

View File

@@ -3,8 +3,11 @@ variable convert, check, etc helepr
"""
from typing import Any
from warnings import deprecated
import corelibs_var.var_helpers
@deprecated("Use corelibs_var.var_helpers.is_int instead")
def is_int(string: Any) -> bool:
"""
check if a value is int
@@ -15,15 +18,10 @@ def is_int(string: Any) -> bool:
Returns:
bool -- _description_
"""
try:
int(string)
return True
except TypeError:
return False
except ValueError:
return False
return corelibs_var.var_helpers.is_int(string)
@deprecated("Use corelibs_var.var_helpers.is_float instead")
def is_float(string: Any) -> bool:
"""
check if a value is float
@@ -34,15 +32,10 @@ def is_float(string: Any) -> bool:
Returns:
bool -- _description_
"""
try:
float(string)
return True
except TypeError:
return False
except ValueError:
return False
return corelibs_var.var_helpers.is_float(string)
@deprecated("Use corelibs_var.var_helpers.str_to_bool instead")
def str_to_bool(string: str):
"""
convert string to bool
@@ -56,10 +49,6 @@ def str_to_bool(string: str):
Returns:
_type_ -- _description_
"""
if string == "True" or string == "true":
return True
if string == "False" or string == "false":
return False
raise ValueError(f"Invalid boolean string: {string}")
return corelibs_var.var_helpers.str_to_bool(string)
# __END__

View File

@@ -6,7 +6,7 @@ Log logging_handling.log testing
import sys
from pathlib import Path
# this is for testing only
from corelibs.logging_handling.log import Log, Logger
from corelibs.logging_handling.log import Log, Logger, ConsoleFormat, ConsoleFormatSettings
from corelibs.debug_handling.debug_helpers import exception_stack, call_stack
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -24,11 +24,22 @@ def main():
# "log_level_console": None,
"log_level_file": 'DEBUG',
# "console_color_output_enabled": False,
"per_run_log": True
"per_run_log": True,
# "console_format_type": ConsoleFormatSettings.NONE,
# "console_format_type": ConsoleFormatSettings.MINIMAL,
"console_format_type": ConsoleFormat.TIME_MICROSECONDS | ConsoleFormat.NAME | ConsoleFormat.LEVEL,
# "console_format_type": ConsoleFormat.NAME,
# "console_format_type": (
# ConsoleFormat.TIME | ConsoleFormat.TIMEZONE | ConsoleFormat.LINENO | ConsoleFormat.LEVEL
# ),
}
)
logn = Logger(log.get_logger_settings())
log.info("ConsoleFormatType FILE is: %s", ConsoleFormat.FILE)
log.info("ConsoleFormatSettings ALL is: %s", ConsoleFormatSettings.ALL)
log.info("ConsoleFormatSettings lookup is: %s", ConsoleFormatSettings.from_string('ALL'))
log.logger.debug('[NORMAL] Debug test: %s', log.logger.name)
log.lg.debug('[NORMAL] Debug test: %s', log.logger.name)
log.debug('[NORMAL-] Debug test: %s', log.logger.name)
@@ -94,10 +105,14 @@ def main():
for key, handler in log.handlers.items():
print(f"Handler (handlers) [{key}] {handler} -> {handler.level} -> {LoggingLevel.from_any(handler.level)}")
log.set_log_level('stream_handler', LoggingLevel.ERROR)
log.set_log_level(Log.CONSOLE_HANDLER, LoggingLevel.ERROR)
log.logger.warning('[NORMAL] Invisible Warning test: %s', log.logger.name)
log.logger.error('[NORMAL] Visible Error test: %s', log.logger.name)
# log.handlers['stream_handler'].se
log.set_log_level(Log.CONSOLE_HANDLER, LoggingLevel.DEBUG)
log.debug('Current logging format: %s', log.log_settings['console_format_type'])
log.update_console_formatter(ConsoleFormat.TIME | ConsoleFormat.LINENO)
log.info('Does hit show less')
if __name__ == "__main__":

View File

@@ -1,3 +0,0 @@
"""
Unit tests for encryption_handling module
"""

View File

@@ -1,205 +0,0 @@
"""
Unit tests for convert_to_seconds function from timestamp_strings module.
"""
import pytest
from corelibs.datetime_handling.timestamp_convert import convert_to_seconds, TimeParseError, TimeUnitError
class TestConvertToSeconds:
"""Test class for convert_to_seconds function."""
def test_numeric_input_int(self):
"""Test with integer input."""
assert convert_to_seconds(42) == 42
assert convert_to_seconds(0) == 0
assert convert_to_seconds(-5) == -5
def test_numeric_input_float(self):
"""Test with float input."""
assert convert_to_seconds(42.7) == 43 # rounds to 43
assert convert_to_seconds(42.3) == 42 # rounds to 42
assert convert_to_seconds(42.5) == 42 # rounds to 42 (banker's rounding)
assert convert_to_seconds(0.0) == 0
assert convert_to_seconds(-5.7) == -6
def test_numeric_string_input(self):
"""Test with numeric string input."""
assert convert_to_seconds("42") == 42
assert convert_to_seconds("42.7") == 43
assert convert_to_seconds("42.3") == 42
assert convert_to_seconds("0") == 0
assert convert_to_seconds("-5.7") == -6
def test_single_unit_seconds(self):
"""Test with seconds unit."""
assert convert_to_seconds("30s") == 30
assert convert_to_seconds("1s") == 1
assert convert_to_seconds("0s") == 0
def test_single_unit_minutes(self):
"""Test with minutes unit."""
assert convert_to_seconds("5m") == 300 # 5 * 60
assert convert_to_seconds("1m") == 60
assert convert_to_seconds("0m") == 0
def test_single_unit_hours(self):
"""Test with hours unit."""
assert convert_to_seconds("2h") == 7200 # 2 * 3600
assert convert_to_seconds("1h") == 3600
assert convert_to_seconds("0h") == 0
def test_single_unit_days(self):
"""Test with days unit."""
assert convert_to_seconds("1d") == 86400 # 1 * 86400
assert convert_to_seconds("2d") == 172800 # 2 * 86400
assert convert_to_seconds("0d") == 0
def test_single_unit_months(self):
"""Test with months unit (30 days * 12 = 1 year)."""
# Note: The code has M: 2592000 * 12 which is 1 year, not 1 month
# This seems like a bug in the original code, but testing what it actually does
assert convert_to_seconds("1M") == 31104000 # 2592000 * 12
assert convert_to_seconds("2M") == 62208000 # 2 * 2592000 * 12
def test_single_unit_years(self):
"""Test with years unit."""
assert convert_to_seconds("1Y") == 31536000 # 365 * 86400
assert convert_to_seconds("2Y") == 63072000 # 2 * 365 * 86400
def test_long_unit_names(self):
"""Test with long unit names."""
assert convert_to_seconds("1year") == 31536000
assert convert_to_seconds("2years") == 63072000
assert convert_to_seconds("1month") == 31104000
assert convert_to_seconds("2months") == 62208000
assert convert_to_seconds("1day") == 86400
assert convert_to_seconds("2days") == 172800
assert convert_to_seconds("1hour") == 3600
assert convert_to_seconds("2hours") == 7200
assert convert_to_seconds("1minute") == 60
assert convert_to_seconds("2minutes") == 120
assert convert_to_seconds("30min") == 1800
assert convert_to_seconds("1second") == 1
assert convert_to_seconds("2seconds") == 2
assert convert_to_seconds("30sec") == 30
def test_multiple_units(self):
"""Test with multiple units combined."""
assert convert_to_seconds("1h30m") == 5400 # 3600 + 1800
assert convert_to_seconds("1d2h") == 93600 # 86400 + 7200
assert convert_to_seconds("1h30m45s") == 5445 # 3600 + 1800 + 45
assert convert_to_seconds("2d3h4m5s") == 183845 # 172800 + 10800 + 240 + 5
def test_multiple_units_with_spaces(self):
"""Test with multiple units and spaces."""
assert convert_to_seconds("1h 30m") == 5400
assert convert_to_seconds("1d 2h") == 93600
assert convert_to_seconds("1h 30m 45s") == 5445
assert convert_to_seconds("2d 3h 4m 5s") == 183845
def test_mixed_unit_formats(self):
"""Test with mixed short and long unit names."""
assert convert_to_seconds("1hour 30min") == 5400
assert convert_to_seconds("1day 2hours") == 93600
assert convert_to_seconds("1h 30minutes 45sec") == 5445
def test_negative_values(self):
"""Test with negative time strings."""
assert convert_to_seconds("-30s") == -30
assert convert_to_seconds("-1h") == -3600
assert convert_to_seconds("-1h30m") == -5400
assert convert_to_seconds("-2d3h4m5s") == -183845
def test_case_insensitive_long_names(self):
"""Test that long unit names are case insensitive."""
assert convert_to_seconds("1Hour") == 3600
assert convert_to_seconds("1MINUTE") == 60
assert convert_to_seconds("1Day") == 86400
assert convert_to_seconds("2YEARS") == 63072000
def test_duplicate_units_error(self):
"""Test that duplicate units raise TimeParseError."""
with pytest.raises(TimeParseError, match="Unit 'h' appears more than once"):
convert_to_seconds("1h2h")
with pytest.raises(TimeParseError, match="Unit 's' appears more than once"):
convert_to_seconds("30s45s")
with pytest.raises(TimeParseError, match="Unit 'm' appears more than once"):
convert_to_seconds("1m30m")
def test_invalid_units_error(self):
"""Test that invalid units raise TimeUnitError."""
with pytest.raises(TimeUnitError, match="Unit 'x' is not a valid unit name"):
convert_to_seconds("30x")
with pytest.raises(TimeUnitError, match="Unit 'invalid' is not a valid unit name"):
convert_to_seconds("1invalid")
with pytest.raises(TimeUnitError, match="Unit 'z' is not a valid unit name"):
convert_to_seconds("1h30z")
def test_empty_string(self):
"""Test with empty string."""
assert convert_to_seconds("") == 0
def test_no_matches(self):
"""Test with string that has no time units."""
assert convert_to_seconds("hello") == 0
assert convert_to_seconds("no time here") == 0
def test_zero_values(self):
"""Test with zero values for different units."""
assert convert_to_seconds("0s") == 0
assert convert_to_seconds("0m") == 0
assert convert_to_seconds("0h") == 0
assert convert_to_seconds("0d") == 0
assert convert_to_seconds("0h0m0s") == 0
def test_large_values(self):
"""Test with large time values."""
assert convert_to_seconds("999d") == 86313600 # 999 * 86400
assert convert_to_seconds("100Y") == 3153600000 # 100 * 31536000
def test_order_independence(self):
"""Test that order of units doesn't matter."""
assert convert_to_seconds("30m1h") == 5400 # same as 1h30m
assert convert_to_seconds("45s30m1h") == 5445 # same as 1h30m45s
assert convert_to_seconds("5s4m3h2d") == 183845 # same as 2d3h4m5s
def test_whitespace_handling(self):
"""Test various whitespace scenarios."""
assert convert_to_seconds("1 h") == 3600
assert convert_to_seconds("1h 30m") == 5400
assert convert_to_seconds(" 1h30m ") == 5400
assert convert_to_seconds("1h\t30m") == 5400
def test_mixed_case_short_units(self):
"""Test that short units work with different cases."""
# Note: The regex only matches [a-zA-Z]+ so case matters for the lookup
with pytest.raises(TimeUnitError, match="Unit 'H' is not a valid unit name"):
convert_to_seconds("1H") # 'H' is not in unit_factors, raises error
assert convert_to_seconds("1h") == 3600 # lowercase works
def test_boundary_conditions(self):
"""Test boundary conditions and edge cases."""
# Test with leading zeros
assert convert_to_seconds("01h") == 3600
assert convert_to_seconds("001m") == 60
# Test very small values
assert convert_to_seconds("1s") == 1
def test_negative_with_multiple_units(self):
"""Test negative values with multiple units."""
assert convert_to_seconds("-1h30m45s") == -5445
assert convert_to_seconds("-2d3h") == -183600
def test_duplicate_with_long_names(self):
"""Test duplicate detection with long unit names."""
with pytest.raises(TimeParseError, match="Unit 'h' appears more than once"):
convert_to_seconds("1hour2h") # both resolve to 'h'
with pytest.raises(TimeParseError, match="Unit 's' appears more than once"):
convert_to_seconds("1second30sec") # both resolve to 's'

View File

@@ -1,737 +0,0 @@
"""
PyTest: datetime_handling/datetime_helpers
"""
from datetime import datetime, time
from zoneinfo import ZoneInfo
import pytest
from corelibs.datetime_handling.datetime_helpers import (
create_time,
get_system_timezone,
parse_timezone_data,
get_datetime_iso8601,
validate_date,
parse_flexible_date,
compare_dates,
find_newest_datetime_in_list,
parse_day_of_week_range,
parse_time_range,
times_overlap_or_connect,
is_time_in_range,
reorder_weekdays_from_today,
DAYS_OF_WEEK_LONG_TO_SHORT,
DAYS_OF_WEEK_ISO,
DAYS_OF_WEEK_ISO_REVERSED,
)
class TestConstants:
"""Test suite for module constants"""
def test_days_of_week_long_to_short(self):
"""Test DAYS_OF_WEEK_LONG_TO_SHORT dictionary"""
assert DAYS_OF_WEEK_LONG_TO_SHORT['Monday'] == 'Mon'
assert DAYS_OF_WEEK_LONG_TO_SHORT['Tuesday'] == 'Tue'
assert DAYS_OF_WEEK_LONG_TO_SHORT['Friday'] == 'Fri'
assert DAYS_OF_WEEK_LONG_TO_SHORT['Sunday'] == 'Sun'
assert len(DAYS_OF_WEEK_LONG_TO_SHORT) == 7
def test_days_of_week_iso(self):
"""Test DAYS_OF_WEEK_ISO dictionary"""
assert DAYS_OF_WEEK_ISO[1] == 'Mon'
assert DAYS_OF_WEEK_ISO[5] == 'Fri'
assert DAYS_OF_WEEK_ISO[7] == 'Sun'
assert len(DAYS_OF_WEEK_ISO) == 7
def test_days_of_week_iso_reversed(self):
"""Test DAYS_OF_WEEK_ISO_REVERSED dictionary"""
assert DAYS_OF_WEEK_ISO_REVERSED['Mon'] == 1
assert DAYS_OF_WEEK_ISO_REVERSED['Fri'] == 5
assert DAYS_OF_WEEK_ISO_REVERSED['Sun'] == 7
assert len(DAYS_OF_WEEK_ISO_REVERSED) == 7
class TestCreateTime:
"""Test suite for create_time function"""
def test_create_time_default_format(self):
"""Test create_time with default format"""
timestamp = 1609459200.0 # 2021-01-01 00:00:00 UTC
result = create_time(timestamp)
# Result depends on system timezone, so just check format
assert len(result) == 19
assert '-' in result
assert ':' in result
def test_create_time_custom_format(self):
"""Test create_time with custom format"""
timestamp = 1609459200.0
result = create_time(timestamp, "%Y/%m/%d")
# Check basic format structure
assert '/' in result
assert len(result) == 10
def test_create_time_with_microseconds(self):
"""Test create_time with microseconds in format"""
timestamp = 1609459200.123456
result = create_time(timestamp, "%Y-%m-%d %H:%M:%S")
assert len(result) == 19
class TestGetSystemTimezone:
"""Test suite for get_system_timezone function"""
def test_get_system_timezone_returns_tuple(self):
"""Test that get_system_timezone returns a tuple"""
result = get_system_timezone()
assert isinstance(result, tuple)
assert len(result) == 2
def test_get_system_timezone_returns_valid_data(self):
"""Test that get_system_timezone returns valid timezone info"""
system_tz, timezone_name = get_system_timezone()
assert system_tz is not None
assert isinstance(timezone_name, str)
assert len(timezone_name) > 0
class TestParseTimezoneData:
"""Test suite for parse_timezone_data function"""
def test_parse_timezone_data_valid_timezone(self):
"""Test parse_timezone_data with valid timezone string"""
result = parse_timezone_data('Asia/Tokyo')
assert isinstance(result, ZoneInfo)
assert str(result) == 'Asia/Tokyo'
def test_parse_timezone_data_utc(self):
"""Test parse_timezone_data with UTC"""
result = parse_timezone_data('UTC')
assert isinstance(result, ZoneInfo)
assert str(result) == 'UTC'
def test_parse_timezone_data_empty_string(self):
"""Test parse_timezone_data with empty string falls back to system timezone"""
result = parse_timezone_data('')
assert isinstance(result, ZoneInfo)
def test_parse_timezone_data_invalid_timezone(self):
"""Test parse_timezone_data with invalid timezone falls back to system timezone"""
# Invalid timezones fall back to system timezone or UTC
result = parse_timezone_data('Invalid/Timezone')
assert isinstance(result, ZoneInfo)
# Should be either system timezone or UTC
def test_parse_timezone_data_none(self):
"""Test parse_timezone_data with None falls back to system timezone"""
result = parse_timezone_data()
assert isinstance(result, ZoneInfo)
def test_parse_timezone_data_various_timezones(self):
"""Test parse_timezone_data with various timezone strings"""
timezones = ['America/New_York', 'Europe/London', 'Asia/Seoul']
for tz in timezones:
result = parse_timezone_data(tz)
assert isinstance(result, ZoneInfo)
assert str(result) == tz
class TestGetDatetimeIso8601:
"""Test suite for get_datetime_iso8601 function"""
def test_get_datetime_iso8601_default_params(self):
"""Test get_datetime_iso8601 with default parameters"""
result = get_datetime_iso8601()
# Should be in ISO 8601 format with T separator and microseconds
assert 'T' in result
assert '.' in result # microseconds
# Check basic ISO 8601 format
datetime.fromisoformat(result) # Should not raise
def test_get_datetime_iso8601_custom_timezone_string(self):
"""Test get_datetime_iso8601 with custom timezone string"""
result = get_datetime_iso8601('UTC')
assert '+00:00' in result or 'Z' in result or result.endswith('+00:00')
def test_get_datetime_iso8601_custom_timezone_zoneinfo(self):
"""Test get_datetime_iso8601 with ZoneInfo object"""
tz = ZoneInfo('Asia/Tokyo')
result = get_datetime_iso8601(tz)
assert 'T' in result
datetime.fromisoformat(result) # Should not raise
def test_get_datetime_iso8601_custom_separator(self):
"""Test get_datetime_iso8601 with custom separator"""
result = get_datetime_iso8601(sep=' ')
assert ' ' in result
assert 'T' not in result
def test_get_datetime_iso8601_different_timespec(self):
"""Test get_datetime_iso8601 with different timespec values"""
result_seconds = get_datetime_iso8601(timespec='seconds')
assert '.' not in result_seconds # No microseconds
result_milliseconds = get_datetime_iso8601(timespec='milliseconds')
# Should have milliseconds (3 digits after decimal)
assert '.' in result_milliseconds
class TestValidateDate:
"""Test suite for validate_date function"""
def test_validate_date_valid_hyphen_format(self):
"""Test validate_date with valid Y-m-d format"""
assert validate_date('2023-12-25') is True
assert validate_date('2024-01-01') is True
def test_validate_date_valid_slash_format(self):
"""Test validate_date with valid Y/m/d format"""
assert validate_date('2023/12/25') is True
assert validate_date('2024/01/01') is True
def test_validate_date_invalid_format(self):
"""Test validate_date with invalid format"""
assert validate_date('25-12-2023') is False
assert validate_date('2023.12.25') is False
assert validate_date('invalid') is False
def test_validate_date_invalid_date(self):
"""Test validate_date with invalid date values"""
assert validate_date('2023-13-01') is False # Invalid month
assert validate_date('2023-02-30') is False # Invalid day
def test_validate_date_with_not_before(self):
"""Test validate_date with not_before constraint"""
not_before = datetime(2023, 12, 1)
assert validate_date('2023-12-25', not_before=not_before) is True
assert validate_date('2023-11-25', not_before=not_before) is False
def test_validate_date_with_not_after(self):
"""Test validate_date with not_after constraint"""
not_after = datetime(2023, 12, 31)
assert validate_date('2023-12-25', not_after=not_after) is True
assert validate_date('2024-01-01', not_after=not_after) is False
def test_validate_date_with_both_constraints(self):
"""Test validate_date with both not_before and not_after constraints"""
not_before = datetime(2023, 12, 1)
not_after = datetime(2023, 12, 31)
assert validate_date('2023-12-15', not_before=not_before, not_after=not_after) is True
assert validate_date('2023-11-30', not_before=not_before, not_after=not_after) is False
assert validate_date('2024-01-01', not_before=not_before, not_after=not_after) is False
class TestParseFlexibleDate:
"""Test suite for parse_flexible_date function"""
def test_parse_flexible_date_iso8601_full(self):
"""Test parse_flexible_date with full ISO 8601 format"""
result = parse_flexible_date('2023-12-25T15:30:45')
assert isinstance(result, datetime)
assert result.year == 2023
assert result.month == 12
assert result.day == 25
assert result.hour == 15
assert result.minute == 30
assert result.second == 45
def test_parse_flexible_date_iso8601_with_microseconds(self):
"""Test parse_flexible_date with microseconds"""
result = parse_flexible_date('2023-12-25T15:30:45.123456')
assert isinstance(result, datetime)
assert result.microsecond == 123456
def test_parse_flexible_date_simple_date(self):
"""Test parse_flexible_date with simple date format"""
result = parse_flexible_date('2023-12-25')
assert isinstance(result, datetime)
assert result.year == 2023
assert result.month == 12
assert result.day == 25
def test_parse_flexible_date_with_timezone_string(self):
"""Test parse_flexible_date with timezone string"""
result = parse_flexible_date('2023-12-25T15:30:45', timezone_tz='Asia/Tokyo')
assert isinstance(result, datetime)
assert result.tzinfo is not None
def test_parse_flexible_date_with_timezone_zoneinfo(self):
"""Test parse_flexible_date with ZoneInfo object"""
tz = ZoneInfo('UTC')
result = parse_flexible_date('2023-12-25T15:30:45', timezone_tz=tz)
assert isinstance(result, datetime)
assert result.tzinfo is not None
def test_parse_flexible_date_with_timezone_no_shift(self):
"""Test parse_flexible_date with timezone but no shift"""
result = parse_flexible_date('2023-12-25T15:30:45', timezone_tz='UTC', shift_time_zone=False)
assert isinstance(result, datetime)
assert result.hour == 15 # Should not shift
def test_parse_flexible_date_with_timezone_shift(self):
"""Test parse_flexible_date with timezone shift"""
result = parse_flexible_date('2023-12-25T15:30:45+00:00', timezone_tz='Asia/Tokyo', shift_time_zone=True)
assert isinstance(result, datetime)
assert result.tzinfo is not None
def test_parse_flexible_date_missing_t_with_timezone_shift(self):
"""Test parse_flexible_date with timezone shift"""
result = parse_flexible_date('2023-12-25 15:30:45+00:00', timezone_tz='Asia/Tokyo', shift_time_zone=True)
assert isinstance(result, datetime)
assert result.tzinfo is not None
def test_parse_flexible_date_space_separated_datetime(self):
"""Test parse_flexible_date with space-separated datetime format"""
result = parse_flexible_date('2023-12-25 15:30:45')
assert isinstance(result, datetime)
assert result.year == 2023
assert result.month == 12
assert result.day == 25
assert result.hour == 15
assert result.minute == 30
assert result.second == 45
def test_parse_flexible_date_space_separated_with_microseconds(self):
"""Test parse_flexible_date with space-separated datetime and microseconds"""
result = parse_flexible_date('2023-12-25 15:30:45.123456')
assert isinstance(result, datetime)
assert result.year == 2023
assert result.month == 12
assert result.day == 25
assert result.hour == 15
assert result.minute == 30
assert result.second == 45
assert result.microsecond == 123456
def test_parse_flexible_date_t_separated_datetime(self):
"""Test parse_flexible_date with T-separated datetime (alternative ISO format)"""
result = parse_flexible_date('2023-12-25T15:30:45')
assert isinstance(result, datetime)
assert result.year == 2023
assert result.month == 12
assert result.day == 25
assert result.hour == 15
assert result.minute == 30
assert result.second == 45
def test_parse_flexible_date_t_separated_with_microseconds(self):
"""Test parse_flexible_date with T-separated datetime and microseconds"""
result = parse_flexible_date('2023-12-25T15:30:45.123456')
assert isinstance(result, datetime)
assert result.year == 2023
assert result.microsecond == 123456
def test_parse_flexible_date_invalid_format(self):
"""Test parse_flexible_date with invalid format returns None"""
result = parse_flexible_date('invalid-date')
assert result is None
def test_parse_flexible_date_whitespace(self):
"""Test parse_flexible_date with whitespace"""
result = parse_flexible_date(' 2023-12-25 ')
assert isinstance(result, datetime)
assert result.year == 2023
class TestCompareDates:
"""Test suite for compare_dates function"""
def test_compare_dates_first_newer(self):
"""Test compare_dates when first date is newer"""
result = compare_dates('2024-01-02', '2024-01-01')
assert result is True
def test_compare_dates_first_older(self):
"""Test compare_dates when first date is older"""
result = compare_dates('2024-01-01', '2024-01-02')
assert result is False
def test_compare_dates_equal(self):
"""Test compare_dates when dates are equal"""
result = compare_dates('2024-01-01', '2024-01-01')
assert result is False
def test_compare_dates_with_time(self):
"""Test compare_dates with time components (should only compare dates)"""
result = compare_dates('2024-01-02T10:00:00', '2024-01-01T23:59:59')
assert result is True
def test_compare_dates_invalid_first_date(self):
"""Test compare_dates with invalid first date"""
result = compare_dates('invalid', '2024-01-01')
assert result is None
def test_compare_dates_invalid_second_date(self):
"""Test compare_dates with invalid second date"""
result = compare_dates('2024-01-01', 'invalid')
assert result is None
def test_compare_dates_both_invalid(self):
"""Test compare_dates with both dates invalid"""
result = compare_dates('invalid1', 'invalid2')
assert result is None
class TestFindNewestDatetimeInList:
"""Test suite for find_newest_datetime_in_list function"""
def test_find_newest_datetime_in_list_basic(self):
"""Test find_newest_datetime_in_list with basic list"""
dates = [
'2023-12-25T10:00:00',
'2024-01-01T12:00:00',
'2023-11-15T08:00:00'
]
result = find_newest_datetime_in_list(dates)
assert result == '2024-01-01T12:00:00'
def test_find_newest_datetime_in_list_with_timezone(self):
"""Test find_newest_datetime_in_list with timezone-aware dates"""
dates = [
'2025-08-06T16:17:39.747+09:00',
'2025-08-05T16:17:39.747+09:00',
'2025-08-07T16:17:39.747+09:00'
]
result = find_newest_datetime_in_list(dates)
assert result == '2025-08-07T16:17:39.747+09:00'
def test_find_newest_datetime_in_list_empty_list(self):
"""Test find_newest_datetime_in_list with empty list"""
result = find_newest_datetime_in_list([])
assert result is None
def test_find_newest_datetime_in_list_single_date(self):
"""Test find_newest_datetime_in_list with single date"""
dates = ['2024-01-01T12:00:00']
result = find_newest_datetime_in_list(dates)
assert result == '2024-01-01T12:00:00'
def test_find_newest_datetime_in_list_with_invalid_dates(self):
"""Test find_newest_datetime_in_list with some invalid dates"""
dates = [
'2023-12-25T10:00:00',
'invalid-date',
'2024-01-01T12:00:00'
]
result = find_newest_datetime_in_list(dates)
assert result == '2024-01-01T12:00:00'
def test_find_newest_datetime_in_list_all_invalid(self):
"""Test find_newest_datetime_in_list with all invalid dates"""
dates = ['invalid1', 'invalid2', 'invalid3']
result = find_newest_datetime_in_list(dates)
assert result is None
def test_find_newest_datetime_in_list_mixed_formats(self):
"""Test find_newest_datetime_in_list with mixed date formats"""
dates = [
'2023-12-25',
'2024-01-01T12:00:00',
'2023-11-15T08:00:00.123456'
]
result = find_newest_datetime_in_list(dates)
assert result == '2024-01-01T12:00:00'
class TestParseDayOfWeekRange:
"""Test suite for parse_day_of_week_range function"""
def test_parse_day_of_week_range_single_day(self):
"""Test parse_day_of_week_range with single day"""
result = parse_day_of_week_range('Mon')
assert result == [(1, 'Mon')]
def test_parse_day_of_week_range_multiple_days(self):
"""Test parse_day_of_week_range with multiple days"""
result = parse_day_of_week_range('Mon,Wed,Fri')
assert len(result) == 3
assert (1, 'Mon') in result
assert (3, 'Wed') in result
assert (5, 'Fri') in result
def test_parse_day_of_week_range_simple_range(self):
"""Test parse_day_of_week_range with simple range"""
result = parse_day_of_week_range('Mon-Fri')
assert len(result) == 5
assert result[0] == (1, 'Mon')
assert result[-1] == (5, 'Fri')
def test_parse_day_of_week_range_weekend_spanning(self):
"""Test parse_day_of_week_range with weekend-spanning range"""
result = parse_day_of_week_range('Fri-Mon')
assert len(result) == 4
assert (5, 'Fri') in result
assert (6, 'Sat') in result
assert (7, 'Sun') in result
assert (1, 'Mon') in result
def test_parse_day_of_week_range_long_names(self):
"""Test parse_day_of_week_range with long day names - only works in ranges"""
# Long names only work in ranges, not as standalone days
# This is a limitation of the current implementation
with pytest.raises(ValueError) as exc_info:
parse_day_of_week_range('Monday,Wednesday')
assert 'Invalid day of week entry found' in str(exc_info.value)
def test_parse_day_of_week_range_mixed_format(self):
"""Test parse_day_of_week_range with short names and ranges"""
result = parse_day_of_week_range('Mon,Wed-Fri')
assert len(result) == 4
assert (1, 'Mon') in result
assert (3, 'Wed') in result
assert (4, 'Thu') in result
assert (5, 'Fri') in result
def test_parse_day_of_week_range_invalid_day(self):
"""Test parse_day_of_week_range with invalid day"""
with pytest.raises(ValueError) as exc_info:
parse_day_of_week_range('InvalidDay')
assert 'Invalid day of week entry found' in str(exc_info.value)
def test_parse_day_of_week_range_duplicate_days(self):
"""Test parse_day_of_week_range with duplicate days"""
with pytest.raises(ValueError) as exc_info:
parse_day_of_week_range('Mon,Mon')
assert 'Duplicate day of week entries found' in str(exc_info.value)
def test_parse_day_of_week_range_whitespace_handling(self):
"""Test parse_day_of_week_range with extra whitespace"""
result = parse_day_of_week_range(' Mon , Wed , Fri ')
assert len(result) == 3
assert (1, 'Mon') in result
class TestParseTimeRange:
"""Test suite for parse_time_range function"""
def test_parse_time_range_valid(self):
"""Test parse_time_range with valid time range"""
start, end = parse_time_range('09:00-17:00')
assert start == time(9, 0)
assert end == time(17, 0)
def test_parse_time_range_different_times(self):
"""Test parse_time_range with different time values"""
start, end = parse_time_range('08:30-12:45')
assert start == time(8, 30)
assert end == time(12, 45)
def test_parse_time_range_invalid_block(self):
"""Test parse_time_range with invalid block format"""
with pytest.raises(ValueError) as exc_info:
parse_time_range('09:00')
assert 'Invalid time block' in str(exc_info.value)
def test_parse_time_range_invalid_format(self):
"""Test parse_time_range with invalid time format"""
with pytest.raises(ValueError) as exc_info:
parse_time_range('25:00-26:00')
assert 'Invalid time block format' in str(exc_info.value)
def test_parse_time_range_start_after_end(self):
"""Test parse_time_range with start time after end time"""
with pytest.raises(ValueError) as exc_info:
parse_time_range('17:00-09:00')
assert 'start time after end time' in str(exc_info.value)
def test_parse_time_range_equal_times(self):
"""Test parse_time_range with equal start and end times"""
with pytest.raises(ValueError) as exc_info:
parse_time_range('09:00-09:00')
assert 'start time after end time or equal' in str(exc_info.value)
def test_parse_time_range_custom_format(self):
"""Test parse_time_range with custom time format"""
start, end = parse_time_range('09:00:00-17:00:00', time_format='%H:%M:%S')
assert start == time(9, 0, 0)
assert end == time(17, 0, 0)
def test_parse_time_range_whitespace(self):
"""Test parse_time_range with whitespace"""
start, end = parse_time_range(' 09:00-17:00 ')
assert start == time(9, 0)
assert end == time(17, 0)
class TestTimesOverlapOrConnect:
"""Test suite for times_overlap_or_connect function"""
def test_times_overlap_or_connect_clear_overlap(self):
"""Test times_overlap_or_connect with clear overlap"""
time1 = (time(9, 0), time(12, 0))
time2 = (time(10, 0), time(14, 0))
assert times_overlap_or_connect(time1, time2) is True
def test_times_overlap_or_connect_no_overlap(self):
"""Test times_overlap_or_connect with no overlap"""
time1 = (time(9, 0), time(12, 0))
time2 = (time(13, 0), time(17, 0))
assert times_overlap_or_connect(time1, time2) is False
def test_times_overlap_or_connect_touching_not_allowed(self):
"""Test times_overlap_or_connect with touching ranges (not allowed)"""
time1 = (time(8, 0), time(10, 0))
time2 = (time(10, 0), time(12, 0))
assert times_overlap_or_connect(time1, time2, allow_touching=False) is True
def test_times_overlap_or_connect_touching_allowed(self):
"""Test times_overlap_or_connect with touching ranges (allowed)"""
time1 = (time(8, 0), time(10, 0))
time2 = (time(10, 0), time(12, 0))
assert times_overlap_or_connect(time1, time2, allow_touching=True) is False
def test_times_overlap_or_connect_one_contains_other(self):
"""Test times_overlap_or_connect when one range contains the other"""
time1 = (time(9, 0), time(17, 0))
time2 = (time(10, 0), time(12, 0))
assert times_overlap_or_connect(time1, time2) is True
def test_times_overlap_or_connect_same_start(self):
"""Test times_overlap_or_connect with same start time"""
time1 = (time(9, 0), time(12, 0))
time2 = (time(9, 0), time(14, 0))
assert times_overlap_or_connect(time1, time2) is True
def test_times_overlap_or_connect_same_end(self):
"""Test times_overlap_or_connect with same end time"""
time1 = (time(9, 0), time(12, 0))
time2 = (time(10, 0), time(12, 0))
assert times_overlap_or_connect(time1, time2) is True
class TestIsTimeInRange:
"""Test suite for is_time_in_range function"""
def test_is_time_in_range_within_range(self):
"""Test is_time_in_range with time within range"""
assert is_time_in_range('10:00:00', '09:00:00', '17:00:00') is True
def test_is_time_in_range_at_start(self):
"""Test is_time_in_range with time at start of range"""
assert is_time_in_range('09:00:00', '09:00:00', '17:00:00') is True
def test_is_time_in_range_at_end(self):
"""Test is_time_in_range with time at end of range"""
assert is_time_in_range('17:00:00', '09:00:00', '17:00:00') is True
def test_is_time_in_range_before_range(self):
"""Test is_time_in_range with time before range"""
assert is_time_in_range('08:00:00', '09:00:00', '17:00:00') is False
def test_is_time_in_range_after_range(self):
"""Test is_time_in_range with time after range"""
assert is_time_in_range('18:00:00', '09:00:00', '17:00:00') is False
def test_is_time_in_range_crosses_midnight(self):
"""Test is_time_in_range with range crossing midnight"""
# Range from 22:00 to 06:00
assert is_time_in_range('23:00:00', '22:00:00', '06:00:00') is True
assert is_time_in_range('03:00:00', '22:00:00', '06:00:00') is True
assert is_time_in_range('12:00:00', '22:00:00', '06:00:00') is False
def test_is_time_in_range_midnight_boundary(self):
"""Test is_time_in_range at midnight"""
assert is_time_in_range('00:00:00', '22:00:00', '06:00:00') is True
class TestReorderWeekdaysFromToday:
"""Test suite for reorder_weekdays_from_today function"""
def test_reorder_weekdays_from_monday(self):
"""Test reorder_weekdays_from_today starting from Monday"""
result = reorder_weekdays_from_today('Mon')
values = list(result.values())
assert values[0] == 'Mon'
assert values[-1] == 'Sun'
assert len(result) == 7
def test_reorder_weekdays_from_wednesday(self):
"""Test reorder_weekdays_from_today starting from Wednesday"""
result = reorder_weekdays_from_today('Wed')
values = list(result.values())
assert values[0] == 'Wed'
assert values[1] == 'Thu'
assert values[-1] == 'Tue'
def test_reorder_weekdays_from_sunday(self):
"""Test reorder_weekdays_from_today starting from Sunday"""
result = reorder_weekdays_from_today('Sun')
values = list(result.values())
assert values[0] == 'Sun'
assert values[-1] == 'Sat'
def test_reorder_weekdays_from_long_name(self):
"""Test reorder_weekdays_from_today with long day name"""
result = reorder_weekdays_from_today('Friday')
values = list(result.values())
assert values[0] == 'Fri'
assert values[-1] == 'Thu'
def test_reorder_weekdays_invalid_day(self):
"""Test reorder_weekdays_from_today with invalid day name"""
with pytest.raises(ValueError) as exc_info:
reorder_weekdays_from_today('InvalidDay')
assert 'Invalid day name provided' in str(exc_info.value)
def test_reorder_weekdays_preserves_all_days(self):
"""Test that reorder_weekdays_from_today preserves all 7 days"""
for day in ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']:
result = reorder_weekdays_from_today(day)
assert len(result) == 7
assert set(result.values()) == {'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'}
class TestEdgeCases:
"""Test suite for edge cases and integration scenarios"""
def test_parse_flexible_date_with_various_iso_formats(self):
"""Test parse_flexible_date handles various ISO format variations"""
formats = [
'2023-12-25',
'2023-12-25T15:30:45',
'2023-12-25T15:30:45.123456',
]
for date_str in formats:
result = parse_flexible_date(date_str)
assert result is not None
assert isinstance(result, datetime)
def test_timezone_consistency_across_functions(self):
"""Test timezone handling consistency across functions"""
tz_str = 'Asia/Tokyo'
tz_obj = parse_timezone_data(tz_str)
# Both should work with get_datetime_iso8601
result1 = get_datetime_iso8601(tz_str)
result2 = get_datetime_iso8601(tz_obj)
assert result1 is not None
assert result2 is not None
def test_date_validation_and_parsing_consistency(self):
"""Test that validate_date and parse_flexible_date agree"""
valid_dates = ['2023-12-25', '2024/01/01']
for date_str in valid_dates:
# normalize format for parse_flexible_date
normalized = date_str.replace('/', '-')
assert validate_date(date_str) is True
assert parse_flexible_date(normalized) is not None
def test_day_of_week_range_complex_scenario(self):
"""Test parse_day_of_week_range with complex mixed input"""
result = parse_day_of_week_range('Mon,Wed-Fri,Sun')
assert len(result) == 5
assert (1, 'Mon') in result
assert (3, 'Wed') in result
assert (4, 'Thu') in result
assert (5, 'Fri') in result
assert (7, 'Sun') in result
def test_time_range_boundary_conditions(self):
"""Test parse_time_range with boundary times"""
start, end = parse_time_range('00:00-23:59')
assert start == time(0, 0)
assert end == time(23, 59)
# __END__

View File

@@ -1,462 +0,0 @@
"""
PyTest: datetime_handling/timestamp_convert - seconds_to_string and convert_timestamp functions
"""
from corelibs.datetime_handling.timestamp_convert import seconds_to_string, convert_timestamp
class TestSecondsToString:
"""Test suite for seconds_to_string function"""
def test_basic_integer_seconds(self):
"""Test conversion of basic integer seconds"""
assert seconds_to_string(0) == "0s"
assert seconds_to_string(1) == "1s"
assert seconds_to_string(30) == "30s"
assert seconds_to_string(59) == "59s"
def test_minutes_conversion(self):
"""Test conversion involving minutes"""
assert seconds_to_string(60) == "1m"
assert seconds_to_string(90) == "1m 30s"
assert seconds_to_string(120) == "2m"
assert seconds_to_string(3599) == "59m 59s"
def test_hours_conversion(self):
"""Test conversion involving hours"""
assert seconds_to_string(3600) == "1h"
assert seconds_to_string(3660) == "1h 1m"
assert seconds_to_string(3661) == "1h 1m 1s"
assert seconds_to_string(7200) == "2h"
assert seconds_to_string(7260) == "2h 1m"
def test_days_conversion(self):
"""Test conversion involving days"""
assert seconds_to_string(86400) == "1d"
assert seconds_to_string(86401) == "1d 1s"
assert seconds_to_string(90000) == "1d 1h"
assert seconds_to_string(90061) == "1d 1h 1m 1s"
assert seconds_to_string(172800) == "2d"
def test_complex_combinations(self):
"""Test complex time combinations"""
# 1 day, 2 hours, 3 minutes, 4 seconds
total = 86400 + 7200 + 180 + 4
assert seconds_to_string(total) == "1d 2h 3m 4s"
# 5 days, 23 hours, 59 minutes, 59 seconds
total = 5 * 86400 + 23 * 3600 + 59 * 60 + 59
assert seconds_to_string(total) == "5d 23h 59m 59s"
def test_fractional_seconds_default_precision(self):
"""Test fractional seconds with default precision (3 decimal places)"""
assert seconds_to_string(0.1) == "0.1s"
assert seconds_to_string(0.123) == "0.123s"
assert seconds_to_string(0.1234) == "0.123s"
assert seconds_to_string(1.5) == "1.5s"
assert seconds_to_string(1.567) == "1.567s"
assert seconds_to_string(1.5678) == "1.568s"
def test_fractional_seconds_microsecond_precision(self):
"""Test fractional seconds with microsecond precision"""
assert seconds_to_string(0.1, show_microseconds=True) == "0.1s"
assert seconds_to_string(0.123456, show_microseconds=True) == "0.123456s"
assert seconds_to_string(0.1234567, show_microseconds=True) == "0.123457s"
assert seconds_to_string(1.5, show_microseconds=True) == "1.5s"
assert seconds_to_string(1.567890, show_microseconds=True) == "1.56789s"
def test_fractional_seconds_with_larger_units(self):
"""Test fractional seconds combined with larger time units"""
# 1 minute and 30.5 seconds
assert seconds_to_string(90.5) == "1m 30.5s"
assert seconds_to_string(90.5, show_microseconds=True) == "1m 30.5s"
# 1 hour, 1 minute, and 1.123 seconds
total = 3600 + 60 + 1.123
assert seconds_to_string(total) == "1h 1m 1.123s"
assert seconds_to_string(total, show_microseconds=True) == "1h 1m 1.123s"
def test_negative_values(self):
"""Test negative time values"""
assert seconds_to_string(-1) == "-1s"
assert seconds_to_string(-60) == "-1m"
assert seconds_to_string(-90) == "-1m 30s"
assert seconds_to_string(-3661) == "-1h 1m 1s"
assert seconds_to_string(-86401) == "-1d 1s"
assert seconds_to_string(-1.5) == "-1.5s"
assert seconds_to_string(-90.123) == "-1m 30.123s"
def test_zero_handling(self):
"""Test various zero values"""
assert seconds_to_string(0) == "0s"
assert seconds_to_string(0.0) == "0s"
assert seconds_to_string(-0) == "0s"
assert seconds_to_string(-0.0) == "0s"
def test_float_input_types(self):
"""Test various float input types"""
assert seconds_to_string(1.0) == "1s"
assert seconds_to_string(60.0) == "1m"
assert seconds_to_string(3600.0) == "1h"
assert seconds_to_string(86400.0) == "1d"
def test_large_values(self):
"""Test handling of large time values"""
# 365 days (1 year)
year_seconds = 365 * 86400
assert seconds_to_string(year_seconds) == "365d"
# 1000 days
assert seconds_to_string(1000 * 86400) == "1000d"
# Large number with all units
large_time = 999 * 86400 + 23 * 3600 + 59 * 60 + 59.999
result = seconds_to_string(large_time)
assert result.startswith("999d")
assert "23h" in result
assert "59m" in result
assert "59.999s" in result
def test_rounding_behavior(self):
"""Test rounding behavior for fractional seconds"""
# Default precision (3 decimal places) - values are truncated via rstrip
assert seconds_to_string(1.0004) == "1s" # Truncates trailing zeros after rstrip
assert seconds_to_string(1.0005) == "1s" # Truncates trailing zeros after rstrip
assert seconds_to_string(1.9999) == "2s" # Rounds up and strips .000
# Microsecond precision (6 decimal places)
assert seconds_to_string(1.0000004, show_microseconds=True) == "1s"
assert seconds_to_string(1.0000005, show_microseconds=True) == "1.000001s"
def test_trailing_zero_removal(self):
"""Test that trailing zeros are properly removed"""
assert seconds_to_string(1.100) == "1.1s"
assert seconds_to_string(1.120) == "1.12s"
assert seconds_to_string(1.123) == "1.123s"
assert seconds_to_string(1.100000, show_microseconds=True) == "1.1s"
assert seconds_to_string(1.123000, show_microseconds=True) == "1.123s"
def test_invalid_input_types(self):
"""Test handling of invalid input types"""
# String inputs should be returned as-is
assert seconds_to_string("invalid") == "invalid"
assert seconds_to_string("not a number") == "not a number"
assert seconds_to_string("") == ""
def test_edge_cases_boundary_values(self):
"""Test edge cases at unit boundaries"""
# Exactly 1 minute - 1 second
assert seconds_to_string(59) == "59s"
assert seconds_to_string(59.999) == "59.999s"
# Exactly 1 hour - 1 second
assert seconds_to_string(3599) == "59m 59s"
assert seconds_to_string(3599.999) == "59m 59.999s"
# Exactly 1 day - 1 second
assert seconds_to_string(86399) == "23h 59m 59s"
assert seconds_to_string(86399.999) == "23h 59m 59.999s"
def test_very_small_fractional_seconds(self):
"""Test very small fractional values"""
assert seconds_to_string(0.001) == "0.001s"
assert seconds_to_string(0.0001) == "0s" # Below default precision
assert seconds_to_string(0.000001, show_microseconds=True) == "0.000001s"
assert seconds_to_string(0.0000001, show_microseconds=True) == "0s" # Below microsecond precision
def test_precision_consistency(self):
"""Test that precision is consistent across different scenarios"""
# With other units present
assert seconds_to_string(61.123456) == "1m 1.123s"
assert seconds_to_string(61.123456, show_microseconds=True) == "1m 1.123456s"
# Large values with fractional seconds
large_val = 90061.123456 # 1d 1h 1m 1.123456s
assert seconds_to_string(large_val) == "1d 1h 1m 1.123s"
assert seconds_to_string(large_val, show_microseconds=True) == "1d 1h 1m 1.123456s"
def test_string_numeric_inputs(self):
"""Test string inputs that represent numbers"""
# String inputs should be returned as-is, even if they look like numbers
assert seconds_to_string("60") == "60"
assert seconds_to_string("1.5") == "1.5"
assert seconds_to_string("0") == "0"
assert seconds_to_string("-60") == "-60"
class TestConvertTimestamp:
"""Test suite for convert_timestamp function"""
def test_basic_integer_seconds(self):
"""Test conversion of basic integer seconds"""
assert convert_timestamp(0) == "0s 0ms"
assert convert_timestamp(1) == "1s 0ms"
assert convert_timestamp(30) == "30s 0ms"
assert convert_timestamp(59) == "59s 0ms"
def test_basic_without_microseconds(self):
"""Test conversion without showing microseconds"""
assert convert_timestamp(0, show_microseconds=False) == "0s"
assert convert_timestamp(1, show_microseconds=False) == "1s"
assert convert_timestamp(30, show_microseconds=False) == "30s"
assert convert_timestamp(59, show_microseconds=False) == "59s"
def test_minutes_conversion(self):
"""Test conversion involving minutes"""
assert convert_timestamp(60) == "1m 0s 0ms"
assert convert_timestamp(90) == "1m 30s 0ms"
assert convert_timestamp(120) == "2m 0s 0ms"
assert convert_timestamp(3599) == "59m 59s 0ms"
def test_minutes_conversion_without_microseconds(self):
"""Test conversion involving minutes without microseconds"""
assert convert_timestamp(60, show_microseconds=False) == "1m 0s"
assert convert_timestamp(90, show_microseconds=False) == "1m 30s"
assert convert_timestamp(120, show_microseconds=False) == "2m 0s"
def test_hours_conversion(self):
"""Test conversion involving hours"""
assert convert_timestamp(3600) == "1h 0m 0s 0ms"
assert convert_timestamp(3660) == "1h 1m 0s 0ms"
assert convert_timestamp(3661) == "1h 1m 1s 0ms"
assert convert_timestamp(7200) == "2h 0m 0s 0ms"
assert convert_timestamp(7260) == "2h 1m 0s 0ms"
def test_hours_conversion_without_microseconds(self):
"""Test conversion involving hours without microseconds"""
assert convert_timestamp(3600, show_microseconds=False) == "1h 0m 0s"
assert convert_timestamp(3660, show_microseconds=False) == "1h 1m 0s"
assert convert_timestamp(3661, show_microseconds=False) == "1h 1m 1s"
def test_days_conversion(self):
"""Test conversion involving days"""
assert convert_timestamp(86400) == "1d 0h 0m 0s 0ms"
assert convert_timestamp(86401) == "1d 0h 0m 1s 0ms"
assert convert_timestamp(90000) == "1d 1h 0m 0s 0ms"
assert convert_timestamp(90061) == "1d 1h 1m 1s 0ms"
assert convert_timestamp(172800) == "2d 0h 0m 0s 0ms"
def test_days_conversion_without_microseconds(self):
"""Test conversion involving days without microseconds"""
assert convert_timestamp(86400, show_microseconds=False) == "1d 0h 0m 0s"
assert convert_timestamp(86401, show_microseconds=False) == "1d 0h 0m 1s"
assert convert_timestamp(90000, show_microseconds=False) == "1d 1h 0m 0s"
def test_complex_combinations(self):
"""Test complex time combinations"""
# 1 day, 2 hours, 3 minutes, 4 seconds
total = 86400 + 7200 + 180 + 4
assert convert_timestamp(total) == "1d 2h 3m 4s 0ms"
# 5 days, 23 hours, 59 minutes, 59 seconds
total = 5 * 86400 + 23 * 3600 + 59 * 60 + 59
assert convert_timestamp(total) == "5d 23h 59m 59s 0ms"
def test_fractional_seconds_with_microseconds(self):
"""Test fractional seconds showing microseconds"""
# Note: ms value is the integer of the decimal part string after rounding to 4 places
assert convert_timestamp(0.1) == "0s 1ms" # 0.1 → "0.1" → ms=1
assert convert_timestamp(0.123) == "0s 123ms" # 0.123 → "0.123" → ms=123
assert convert_timestamp(0.1234) == "0s 1234ms" # 0.1234 → "0.1234" → ms=1234
assert convert_timestamp(1.5) == "1s 5ms" # 1.5 → "1.5" → ms=5
assert convert_timestamp(1.567) == "1s 567ms" # 1.567 → "1.567" → ms=567
assert convert_timestamp(1.5678) == "1s 5678ms" # 1.5678 rounds to 1.5678 → ms=5678
def test_fractional_seconds_rounding(self):
"""Test rounding of fractional seconds to 4 decimal places"""
# The function rounds to 4 decimal places before splitting
assert convert_timestamp(0.12345) == "0s 1235ms" # Rounds to 0.1235
assert convert_timestamp(0.123456) == "0s 1235ms" # Rounds to 0.1235
assert convert_timestamp(1.99999) == "2s 0ms" # Rounds to 2.0
def test_fractional_seconds_with_larger_units(self):
"""Test fractional seconds combined with larger time units"""
# 1 minute and 30.5 seconds
assert convert_timestamp(90.5) == "1m 30s 5ms"
# 1 hour, 1 minute, and 1.123 seconds
total = 3600 + 60 + 1.123
assert convert_timestamp(total) == "1h 1m 1s 123ms"
def test_negative_values(self):
"""Test negative time values"""
assert convert_timestamp(-1) == "-1s 0ms"
assert convert_timestamp(-60) == "-1m 0s 0ms"
assert convert_timestamp(-90) == "-1m 30s 0ms"
assert convert_timestamp(-3661) == "-1h 1m 1s 0ms"
assert convert_timestamp(-86401) == "-1d 0h 0m 1s 0ms"
assert convert_timestamp(-1.5) == "-1s 5ms"
assert convert_timestamp(-90.123) == "-1m 30s 123ms"
def test_negative_without_microseconds(self):
"""Test negative values without microseconds"""
assert convert_timestamp(-1, show_microseconds=False) == "-1s"
assert convert_timestamp(-60, show_microseconds=False) == "-1m 0s"
assert convert_timestamp(-90.123, show_microseconds=False) == "-1m 30s"
def test_zero_handling(self):
"""Test various zero values"""
assert convert_timestamp(0) == "0s 0ms"
assert convert_timestamp(0.0) == "0s 0ms"
assert convert_timestamp(-0) == "0s 0ms"
assert convert_timestamp(-0.0) == "0s 0ms"
def test_zero_filling_behavior(self):
"""Test that zeros are filled between set values"""
# If we have days and seconds, hours and minutes should be 0
assert convert_timestamp(86401) == "1d 0h 0m 1s 0ms"
# If we have hours and seconds, minutes should be 0
assert convert_timestamp(3601) == "1h 0m 1s 0ms"
# If we have days and hours, minutes and seconds should be 0
assert convert_timestamp(90000) == "1d 1h 0m 0s 0ms"
def test_milliseconds_display(self):
"""Test milliseconds are always shown when show_microseconds=True"""
# Even with no fractional part, 0ms should be shown
assert convert_timestamp(1) == "1s 0ms"
assert convert_timestamp(60) == "1m 0s 0ms"
assert convert_timestamp(3600) == "1h 0m 0s 0ms"
# With fractional part, ms should be shown
assert convert_timestamp(1.001) == "1s 1ms" # "1.001" → ms=1
assert convert_timestamp(1.0001) == "1s 1ms" # "1.0001" → ms=1
def test_float_input_types(self):
"""Test various float input types"""
assert convert_timestamp(1.0) == "1s 0ms"
assert convert_timestamp(60.0) == "1m 0s 0ms"
assert convert_timestamp(3600.0) == "1h 0m 0s 0ms"
assert convert_timestamp(86400.0) == "1d 0h 0m 0s 0ms"
def test_large_values(self):
"""Test handling of large time values"""
# 365 days (1 year)
year_seconds = 365 * 86400
assert convert_timestamp(year_seconds) == "365d 0h 0m 0s 0ms"
# 1000 days
assert convert_timestamp(1000 * 86400) == "1000d 0h 0m 0s 0ms"
# Large number with all units
large_time = 999 * 86400 + 23 * 3600 + 59 * 60 + 59.999
result = convert_timestamp(large_time)
assert result.startswith("999d")
assert "23h" in result
assert "59m" in result
assert "59s" in result
assert "999ms" in result # 59.999 rounds to 59.999, ms=999
def test_invalid_input_types(self):
"""Test handling of invalid input types"""
# String inputs should be returned as-is
assert convert_timestamp("invalid") == "invalid"
assert convert_timestamp("not a number") == "not a number"
assert convert_timestamp("") == ""
def test_string_numeric_inputs(self):
"""Test string inputs that represent numbers"""
# String inputs should be returned as-is, even if they look like numbers
assert convert_timestamp("60") == "60"
assert convert_timestamp("1.5") == "1.5"
assert convert_timestamp("0") == "0"
assert convert_timestamp("-60") == "-60"
def test_edge_cases_boundary_values(self):
"""Test edge cases at unit boundaries"""
# Exactly 1 minute - 1 second
assert convert_timestamp(59) == "59s 0ms"
assert convert_timestamp(59.999) == "59s 999ms"
# Exactly 1 hour - 1 second
assert convert_timestamp(3599) == "59m 59s 0ms"
assert convert_timestamp(3599.999) == "59m 59s 999ms"
# Exactly 1 day - 1 second
assert convert_timestamp(86399) == "23h 59m 59s 0ms"
assert convert_timestamp(86399.999) == "23h 59m 59s 999ms"
def test_very_small_fractional_seconds(self):
"""Test very small fractional values"""
assert convert_timestamp(0.001) == "0s 1ms" # 0.001 → "0.001" → ms=1
assert convert_timestamp(0.0001) == "0s 1ms" # 0.0001 → "0.0001" → ms=1
assert convert_timestamp(0.00005) == "0s 1ms" # 0.00005 rounds to 0.0001 → ms=1
assert convert_timestamp(0.00004) == "0s 0ms" # 0.00004 rounds to 0.0 → ms=0
def test_milliseconds_extraction(self):
"""Test that milliseconds are correctly extracted from fractional part"""
# The ms value is the integer of the decimal part string, not a conversion
# So 0.1 → "0.1" → ms=1, NOT 100ms as you might expect
assert convert_timestamp(0.1) == "0s 1ms"
# 0.01 seconds → "0.01" → ms=1 (int("01") = 1)
assert convert_timestamp(0.01) == "0s 1ms"
# 0.001 seconds → "0.001" → ms=1
assert convert_timestamp(0.001) == "0s 1ms"
# 0.0001 seconds → "0.0001" → ms=1
assert convert_timestamp(0.0001) == "0s 1ms"
# 0.00004 seconds rounds to "0.0" → ms=0
assert convert_timestamp(0.00004) == "0s 0ms"
def test_comparison_with_seconds_to_string(self):
"""Test differences between convert_timestamp and seconds_to_string"""
# convert_timestamp fills zeros and adds ms
# seconds_to_string omits zeros and no ms
assert convert_timestamp(86401) == "1d 0h 0m 1s 0ms"
assert seconds_to_string(86401) == "1d 1s"
assert convert_timestamp(3661) == "1h 1m 1s 0ms"
assert seconds_to_string(3661) == "1h 1m 1s"
# With microseconds disabled, still different due to zero-filling
assert convert_timestamp(86401, show_microseconds=False) == "1d 0h 0m 1s"
assert seconds_to_string(86401) == "1d 1s"
def test_precision_consistency(self):
"""Test that precision is consistent across different scenarios"""
# With other units present
assert convert_timestamp(61.123456) == "1m 1s 1235ms" # Rounds to 61.1235
# Large values with fractional seconds
large_val = 90061.123456 # 1d 1h 1m 1.123456s
assert convert_timestamp(large_val) == "1d 1h 1m 1s 1235ms" # Rounds to .1235
def test_microseconds_flag_consistency(self):
"""Test that show_microseconds flag works consistently"""
test_values = [0, 1, 60, 3600, 86400, 1.5, 90.123, -60]
for val in test_values:
with_ms = convert_timestamp(val, show_microseconds=True)
without_ms = convert_timestamp(val, show_microseconds=False)
# With microseconds should contain 'ms', without should not
assert "ms" in with_ms
assert "ms" not in without_ms
# Both should start with same sign if negative
if val < 0:
assert with_ms.startswith("-")
assert without_ms.startswith("-")
def test_format_consistency(self):
"""Test that output format is consistent"""
# All outputs should have consistent spacing and unit ordering
# Format should be: [d ]h m s[ ms]
result = convert_timestamp(93784.5678) # 1d 2h 3m 4.5678s
# 93784.5678 rounds to 93784.5678, splits to ["93784", "5678"]
assert result == "1d 2h 3m 4s 5678ms"
# Verify parts are in correct order
parts = result.split()
# Extract units properly: last 1-2 chars that are letters
units: list[str] = []
for p in parts:
if p.endswith('ms'):
units.append('ms')
elif p[-1].isalpha():
units.append(p[-1])
# Should be in order: d, h, m, s, ms
expected_order = ['d', 'h', 'm', 's', 'ms']
assert units == expected_order
# __END__

View File

@@ -1,194 +0,0 @@
"""
PyTest: datetime_handling/timestamp_strings
"""
from datetime import datetime
from unittest.mock import patch
from zoneinfo import ZoneInfo
import pytest
# Assuming the class is in a file called timestamp_strings.py
from corelibs.datetime_handling.timestamp_strings import TimestampStrings
class TestTimestampStrings:
"""Test suite for TimestampStrings class"""
def test_default_initialization(self):
"""Test initialization with default timezone"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings()
assert ts.time_zone == 'Asia/Tokyo'
assert ts.timestamp_now == mock_now
assert ts.today == '2023-12-25'
assert ts.timestamp == '2023-12-25 15:30:45'
assert ts.timestamp_file == '2023-12-25_153045'
def test_custom_timezone_initialization(self):
"""Test initialization with custom timezone"""
custom_tz = 'America/New_York'
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings(time_zone=custom_tz)
assert ts.time_zone == custom_tz
assert ts.timestamp_now == mock_now
def test_invalid_timezone_raises_error(self):
"""Test that invalid timezone raises ValueError"""
invalid_tz = 'Invalid/Timezone'
with pytest.raises(ValueError) as exc_info:
TimestampStrings(time_zone=invalid_tz)
assert 'Zone could not be loaded [Invalid/Timezone]' in str(exc_info.value)
def test_timestamp_formats(self):
"""Test various timestamp format outputs"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
# Mock both datetime.now() calls
mock_now = datetime(2023, 12, 25, 9, 5, 3)
mock_now_tz = datetime(2023, 12, 25, 23, 5, 3, tzinfo=ZoneInfo('Asia/Tokyo'))
mock_datetime.now.side_effect = [mock_now, mock_now_tz]
ts = TimestampStrings()
assert ts.today == '2023-12-25'
assert ts.timestamp == '2023-12-25 09:05:03'
assert ts.timestamp_file == '2023-12-25_090503'
assert 'JST' in ts.timestamp_tz or 'Asia/Tokyo' in ts.timestamp_tz
def test_different_timezones_produce_different_results(self):
"""Test that different timezones produce different timestamp_tz values"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 12, 0, 0)
mock_datetime.now.return_value = mock_now
# Create instances with different timezones
ts_tokyo = TimestampStrings(time_zone='Asia/Tokyo')
ts_ny = TimestampStrings(time_zone='America/New_York')
# The timezone-aware timestamps should be different
assert ts_tokyo.time_zone != ts_ny.time_zone
# Note: The actual timestamp_tz values will depend on the mocked datetime
def test_class_default_timezone(self):
"""Test that class default timezone is correctly set"""
assert TimestampStrings.TIME_ZONE == 'Asia/Tokyo'
def test_none_timezone_uses_default(self):
"""Test that passing None for timezone uses class default"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings(time_zone=None)
assert ts.time_zone == 'Asia/Tokyo'
def test_timestamp_file_format_no_colons(self):
"""Test that timestamp_file format doesn't contain colons (safe for filenames)"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings()
assert ':' not in ts.timestamp_file
assert ' ' not in ts.timestamp_file
assert ts.timestamp_file == '2023-12-25_153045'
def test_multiple_instances_independent(self):
"""Test that multiple instances don't interfere with each other"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
ts1 = TimestampStrings(time_zone='Asia/Tokyo')
ts2 = TimestampStrings(time_zone='Europe/London')
assert ts1.time_zone == 'Asia/Tokyo'
assert ts2.time_zone == 'Europe/London'
assert ts1.time_zone != ts2.time_zone
def test_zoneinfo_called_correctly_with_string(self):
"""Test that ZoneInfo is called with correct timezone when passing string"""
with patch('corelibs.datetime_handling.timestamp_strings.ZoneInfo') as mock_zoneinfo:
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
custom_tz = 'Europe/Paris'
ts = TimestampStrings(time_zone=custom_tz)
assert ts.time_zone == custom_tz
mock_zoneinfo.assert_called_with(custom_tz)
def test_zoneinfo_object_parameter(self):
"""Test that ZoneInfo objects can be passed directly as timezone parameter"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_now_tz = datetime(2023, 12, 25, 15, 30, 45, tzinfo=ZoneInfo('Europe/Paris'))
mock_datetime.now.side_effect = [mock_now, mock_now_tz]
# Create a ZoneInfo object
custom_tz_obj = ZoneInfo('Europe/Paris')
ts = TimestampStrings(time_zone=custom_tz_obj)
# The time_zone should be the ZoneInfo object itself
assert ts.time_zone_zi is custom_tz_obj
assert isinstance(ts.time_zone_zi, ZoneInfo)
def test_zoneinfo_object_vs_string_equivalence(self):
"""Test that ZoneInfo object and string produce equivalent results"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_now_tz = datetime(2023, 12, 25, 15, 30, 45, tzinfo=ZoneInfo('Europe/Paris'))
mock_datetime.now.side_effect = [mock_now, mock_now_tz, mock_now, mock_now_tz]
# Test with string
ts_string = TimestampStrings(time_zone='Europe/Paris')
# Test with ZoneInfo object
ts_zoneinfo = TimestampStrings(time_zone=ZoneInfo('Europe/Paris'))
# Both should produce the same timestamp formats (though time_zone attributes will differ)
assert ts_string.today == ts_zoneinfo.today
assert ts_string.timestamp == ts_zoneinfo.timestamp
assert ts_string.timestamp_file == ts_zoneinfo.timestamp_file
# The time_zone attributes will be different types but represent the same timezone
assert str(ts_string.time_zone) == 'Europe/Paris'
assert isinstance(ts_zoneinfo.time_zone_zi, ZoneInfo)
def test_edge_case_midnight(self):
"""Test timestamp formatting at midnight"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 0, 0, 0)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings()
assert ts.timestamp == '2023-12-25 00:00:00'
assert ts.timestamp_file == '2023-12-25_000000'
def test_edge_case_new_year(self):
"""Test timestamp formatting at new year"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2024, 1, 1, 0, 0, 0)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings()
assert ts.today == '2024-01-01'
assert ts.timestamp == '2024-01-01 00:00:00'
# __END__

View File

@@ -11,6 +11,7 @@ from corelibs.logging_handling.log import (
Log,
LogParent,
LogSettings,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -33,6 +34,7 @@ def basic_log_settings() -> LogSettings:
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -99,6 +101,85 @@ class TestLogSettingsParsing:
assert log.log_settings["console_enabled"] == Log.DEFAULT_LOG_SETTINGS["console_enabled"]
assert log.log_settings["per_run_log"] == Log.DEFAULT_LOG_SETTINGS["per_run_log"]
def test_parse_console_format_type_all(self, tmp_log_path: Path):
"""Test parsing with console_format_type set to ALL"""
settings: dict[str, Any] = {
"console_format_type": ConsoleFormatSettings.ALL,
}
log = Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings # type: ignore
)
assert log.log_settings["console_format_type"] == ConsoleFormatSettings.ALL
def test_parse_console_format_type_condensed(self, tmp_log_path: Path):
"""Test parsing with console_format_type set to CONDENSED"""
settings: dict[str, Any] = {
"console_format_type": ConsoleFormatSettings.CONDENSED,
}
log = Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings # type: ignore
)
assert log.log_settings["console_format_type"] == ConsoleFormatSettings.CONDENSED
def test_parse_console_format_type_minimal(self, tmp_log_path: Path):
"""Test parsing with console_format_type set to MINIMAL"""
settings: dict[str, Any] = {
"console_format_type": ConsoleFormatSettings.MINIMAL,
}
log = Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings # type: ignore
)
assert log.log_settings["console_format_type"] == ConsoleFormatSettings.MINIMAL
def test_parse_console_format_type_bare(self, tmp_log_path: Path):
"""Test parsing with console_format_type set to BARE"""
settings: dict[str, Any] = {
"console_format_type": ConsoleFormatSettings.BARE,
}
log = Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings # type: ignore
)
assert log.log_settings["console_format_type"] == ConsoleFormatSettings.BARE
def test_parse_console_format_type_none(self, tmp_log_path: Path):
"""Test parsing with console_format_type set to NONE"""
settings: dict[str, Any] = {
"console_format_type": ConsoleFormatSettings.NONE,
}
log = Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings # type: ignore
)
assert log.log_settings["console_format_type"] == ConsoleFormatSettings.NONE
def test_parse_console_format_type_invalid(self, tmp_log_path: Path):
"""Test parsing with invalid console_format_type raises TypeError"""
settings: dict[str, Any] = {
"console_format_type": "invalid_format",
}
# Invalid console_format_type causes TypeError during handler creation
# because the code doesn't validate the type before using it
with pytest.raises(TypeError, match="'in <string>' requires string as left operand"):
Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings # type: ignore
)
# MARK: Test Spacer Constants
class TestSpacerConstants:
@@ -115,6 +196,70 @@ class TestSpacerConstants:
assert LogParent.SPACER_LENGTH == 32
# MARK: Test ConsoleFormatSettings.from_string
class TestConsoleFormatSettingsFromString:
"""Test cases for ConsoleFormatSettings.from_string method"""
def test_from_string_all(self):
"""Test from_string with 'ALL' returns correct format"""
result = ConsoleFormatSettings.from_string('ALL')
assert result == ConsoleFormatSettings.ALL
def test_from_string_condensed(self):
"""Test from_string with 'CONDENSED' returns correct format"""
result = ConsoleFormatSettings.from_string('CONDENSED')
assert result == ConsoleFormatSettings.CONDENSED
def test_from_string_minimal(self):
"""Test from_string with 'MINIMAL' returns correct format"""
result = ConsoleFormatSettings.from_string('MINIMAL')
assert result == ConsoleFormatSettings.MINIMAL
def test_from_string_bare(self):
"""Test from_string with 'BARE' returns correct format"""
result = ConsoleFormatSettings.from_string('BARE')
assert result == ConsoleFormatSettings.BARE
def test_from_string_none(self):
"""Test from_string with 'NONE' returns correct format"""
result = ConsoleFormatSettings.from_string('NONE')
assert result == ConsoleFormatSettings.NONE
def test_from_string_invalid_returns_none(self):
"""Test from_string with invalid string returns None"""
result = ConsoleFormatSettings.from_string('INVALID')
assert result is None
def test_from_string_invalid_with_default(self):
"""Test from_string with invalid string returns provided default"""
default = ConsoleFormatSettings.ALL
result = ConsoleFormatSettings.from_string('INVALID', default=default)
assert result == default
def test_from_string_case_sensitive(self):
"""Test from_string is case sensitive"""
# Lowercase should not match
result = ConsoleFormatSettings.from_string('all')
assert result is None
def test_from_string_with_none_default(self):
"""Test from_string with explicit None default"""
result = ConsoleFormatSettings.from_string('NONEXISTENT', default=None)
assert result is None
@pytest.mark.parametrize("setting_name,expected", [
("ALL", ConsoleFormatSettings.ALL),
("CONDENSED", ConsoleFormatSettings.CONDENSED),
("MINIMAL", ConsoleFormatSettings.MINIMAL),
("BARE", ConsoleFormatSettings.BARE),
("NONE", ConsoleFormatSettings.NONE),
])
def test_from_string_all_valid_settings(self, setting_name: str, expected: Any):
"""Test from_string with all valid setting names"""
result = ConsoleFormatSettings.from_string(setting_name)
assert result == expected
# MARK: Parametrized Tests
class TestParametrized:
"""Parametrized tests for comprehensive coverage"""
@@ -160,6 +305,7 @@ class TestParametrized:
("per_run_log", True, "not_bool"),
("console_enabled", False, 123),
("console_color_output_enabled", True, None),
("console_format_type", ConsoleFormatSettings.ALL, "invalid_format"),
("add_start_info", False, []),
("add_end_info", True, {}),
])

View File

@@ -13,6 +13,7 @@ from corelibs.logging_handling.log import (
LogParent,
LogSettings,
CustomConsoleFormatter,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -35,6 +36,7 @@ def basic_log_settings() -> LogSettings:
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -169,6 +171,7 @@ class TestLogInitialization:
"per_run_log": False,
"console_enabled": False,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -190,6 +193,7 @@ class TestLogInitialization:
"per_run_log": True,
"console_enabled": False,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -256,6 +260,7 @@ class TestLogInitialization:
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": True,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,

View File

@@ -11,6 +11,7 @@ from corelibs.logging_handling.log import (
Log,
LogSettings,
CustomConsoleFormatter,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -33,6 +34,7 @@ def basic_log_settings() -> LogSettings:
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -138,4 +140,172 @@ class TestCustomConsoleFormatter:
assert "Critical message" in result
assert "CRITICAL" in result
# MARK: Test update_console_formatter
class TestUpdateConsoleFormatter:
"""Test cases for update_console_formatter method"""
def test_update_console_formatter_to_minimal(self, log_instance: Log):
"""Test updating console formatter to MINIMAL format"""
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter was updated
assert formatter is not None
def test_update_console_formatter_to_condensed(self, log_instance: Log):
"""Test updating console formatter to CONDENSED format"""
log_instance.update_console_formatter(ConsoleFormatSettings.CONDENSED)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter was updated
assert formatter is not None
def test_update_console_formatter_to_bare(self, log_instance: Log):
"""Test updating console formatter to BARE format"""
log_instance.update_console_formatter(ConsoleFormatSettings.BARE)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter was updated
assert formatter is not None
def test_update_console_formatter_to_none(self, log_instance: Log):
"""Test updating console formatter to NONE format"""
log_instance.update_console_formatter(ConsoleFormatSettings.NONE)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter was updated
assert formatter is not None
def test_update_console_formatter_to_all(self, log_instance: Log):
"""Test updating console formatter to ALL format"""
log_instance.update_console_formatter(ConsoleFormatSettings.ALL)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter was updated
assert formatter is not None
def test_update_console_formatter_when_disabled(
self, tmp_log_path: Path, basic_log_settings: LogSettings
):
"""Test that update_console_formatter does nothing when console is disabled"""
# Disable console
basic_log_settings['console_enabled'] = False
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
# This should not raise an error and should return early
log.update_console_formatter(ConsoleFormatSettings.MINIMAL)
# Verify console handler doesn't exist
assert log.CONSOLE_HANDLER not in log.handlers
def test_update_console_formatter_with_color_enabled(
self, tmp_log_path: Path, basic_log_settings: LogSettings
):
"""Test updating console formatter with color output enabled"""
basic_log_settings['console_color_output_enabled'] = True
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
log.update_console_formatter(ConsoleFormatSettings.MINIMAL)
# Get the console handler's formatter
console_handler = log.handlers[log.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter is CustomConsoleFormatter when colors enabled
assert isinstance(formatter, CustomConsoleFormatter)
def test_update_console_formatter_without_color(self, log_instance: Log):
"""Test updating console formatter without color output"""
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter is standard Formatter when colors disabled
assert isinstance(formatter, logging.Formatter)
# But not the colored version
assert not isinstance(formatter, CustomConsoleFormatter)
def test_update_console_formatter_multiple_times(self, log_instance: Log):
"""Test updating console formatter multiple times"""
# Update to MINIMAL
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter1 = console_handler.formatter
# Update to CONDENSED
log_instance.update_console_formatter(ConsoleFormatSettings.CONDENSED)
formatter2 = console_handler.formatter
# Update to ALL
log_instance.update_console_formatter(ConsoleFormatSettings.ALL)
formatter3 = console_handler.formatter
# Verify each update created a new formatter
assert formatter1 is not formatter2
assert formatter2 is not formatter3
assert formatter1 is not formatter3
def test_update_console_formatter_preserves_handler_level(self, log_instance: Log):
"""Test that updating formatter preserves the handler's log level"""
original_level = log_instance.handlers[log_instance.CONSOLE_HANDLER].level
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
new_level = log_instance.handlers[log_instance.CONSOLE_HANDLER].level
assert original_level == new_level
def test_update_console_formatter_format_output(
self, log_instance: Log, caplog: pytest.LogCaptureFixture
):
"""Test that updated formatter actually affects log output"""
# Set to BARE format (message only)
log_instance.update_console_formatter(ConsoleFormatSettings.BARE)
# Configure caplog to capture at the appropriate level
with caplog.at_level(logging.WARNING):
log_instance.warning("Test warning message")
# Verify message was logged
assert "Test warning message" in caplog.text
def test_update_console_formatter_none_format_output(
self, log_instance: Log, caplog: pytest.LogCaptureFixture
):
"""Test that NONE formatter outputs only the message without any formatting"""
# Set to NONE format (message only, no level indicator)
log_instance.update_console_formatter(ConsoleFormatSettings.NONE)
# Configure caplog to capture at the appropriate level
with caplog.at_level(logging.WARNING):
log_instance.warning("Test warning message")
# Verify message was logged
assert "Test warning message" in caplog.text
# __END__

View File

@@ -11,6 +11,7 @@ from corelibs.logging_handling.log import (
Log,
LogSettings,
CustomHandlerFilter,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -33,6 +34,7 @@ def basic_log_settings() -> LogSettings:
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,

View File

@@ -11,6 +11,8 @@ from corelibs.logging_handling.log import (
Log,
LogParent,
LogSettings,
ConsoleFormatSettings,
ConsoleFormat,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -33,6 +35,7 @@ def basic_log_settings() -> LogSettings:
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -61,6 +64,7 @@ class TestHandlerManagement:
"per_run_log": False,
"console_enabled": False,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -105,4 +109,101 @@ class TestHandlerManagement:
result2 = log.add_handler("test", handler2)
assert result2 is False
def test_change_console_format_to_minimal(self, log_instance: Log):
"""Test changing console handler format to MINIMAL"""
original_formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
new_formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert new_formatter is not original_formatter
assert new_formatter is not None
def test_change_console_format_to_condensed(self, log_instance: Log):
"""Test changing console handler format to CONDENSED"""
log_instance.update_console_formatter(ConsoleFormatSettings.CONDENSED)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert formatter is not None
def test_change_console_format_to_bare(self, log_instance: Log):
"""Test changing console handler format to BARE"""
log_instance.update_console_formatter(ConsoleFormatSettings.BARE)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert formatter is not None
def test_change_console_format_to_none(self, log_instance: Log):
"""Test changing console handler format to NONE"""
log_instance.update_console_formatter(ConsoleFormatSettings.NONE)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert formatter is not None
def test_change_console_format_to_all(self, log_instance: Log):
"""Test changing console handler format to ALL"""
# Start with a different format
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
log_instance.update_console_formatter(ConsoleFormatSettings.ALL)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert formatter is not None
def test_change_console_format_multiple_times(self, log_instance: Log):
"""Test changing console handler format multiple times"""
formatters: list[logging.Formatter | None] = []
for format_type in [
ConsoleFormatSettings.MINIMAL,
ConsoleFormatSettings.CONDENSED,
ConsoleFormatSettings.BARE,
ConsoleFormatSettings.NONE,
ConsoleFormatSettings.ALL,
]:
log_instance.update_console_formatter(format_type)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
formatters.append(formatter)
assert formatter is not None
# Verify each formatter is unique (new instance each time)
for i, formatter in enumerate(formatters):
for j, other_formatter in enumerate(formatters):
if i != j:
assert formatter is not other_formatter
def test_change_console_format_with_disabled_console(
self, tmp_log_path: Path, basic_log_settings: LogSettings
):
"""Test changing console format when console is disabled does nothing"""
basic_log_settings['console_enabled'] = False
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
# Should not raise error, just return early
log.update_console_formatter(ConsoleFormatSettings.MINIMAL)
# Console handler should not exist
assert log.CONSOLE_HANDLER not in log.handlers
@pytest.mark.parametrize("format_type", [
ConsoleFormatSettings.ALL,
ConsoleFormatSettings.CONDENSED,
ConsoleFormatSettings.MINIMAL,
ConsoleFormatSettings.BARE,
ConsoleFormatSettings.NONE,
])
def test_change_console_format_parametrized(
self, log_instance: Log, format_type: ConsoleFormat # type: ignore
):
"""Test changing console format with all format types"""
log_instance.update_console_formatter(format_type)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert formatter is not None
assert isinstance(formatter, logging.Formatter)
# __END__

View File

@@ -10,6 +10,7 @@ from corelibs.logging_handling.log import (
Log,
Logger,
LogSettings,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -32,6 +33,7 @@ def basic_log_settings() -> LogSettings:
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,

View File

@@ -10,6 +10,7 @@ import pytest
from corelibs.logging_handling.log import (
Log,
LogSettings,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -32,6 +33,7 @@ def basic_log_settings() -> LogSettings:
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -88,6 +90,7 @@ class TestEdgeCases:
"per_run_log": False,
"console_enabled": False,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": True, # Enable end info
"log_queue": None,

View File

@@ -12,6 +12,7 @@ import pytest
from corelibs.logging_handling.log import (
Log,
LogSettings,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -34,6 +35,7 @@ def basic_log_settings() -> LogSettings:
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -68,6 +70,7 @@ class TestQueueListener:
"per_run_log": False,
"console_enabled": False,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": mock_queue, # type: ignore
@@ -103,6 +106,7 @@ class TestQueueListener:
"per_run_log": False,
"console_enabled": False,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": mock_queue, # type: ignore

View File

@@ -1,3 +0,0 @@
"""
var_handling tests
"""

View File

@@ -1,546 +0,0 @@
"""
var_handling.enum_base tests
"""
from typing import Any
import pytest
from corelibs.var_handling.enum_base import EnumBase
class SampleBlock(EnumBase):
"""Sample block enum for testing purposes"""
BLOCK_A = "block_a"
BLOCK_B = "block_b"
HAS_NUM = 5
HAS_FLOAT = 3.14
LEGACY_KEY = "legacy_value"
class SimpleEnum(EnumBase):
"""Simple enum with string values"""
OPTION_ONE = "one"
OPTION_TWO = "two"
OPTION_THREE = "three"
class NumericEnum(EnumBase):
"""Enum with only numeric values"""
FIRST = 1
SECOND = 2
THIRD = 3
class TestEnumBaseLookupKey:
"""Test cases for lookup_key class method"""
def test_lookup_key_valid_uppercase(self):
"""Test lookup_key with valid uppercase key"""
result = SampleBlock.lookup_key("BLOCK_A")
assert result == SampleBlock.BLOCK_A
assert result.name == "BLOCK_A"
assert result.value == "block_a"
def test_lookup_key_valid_lowercase(self):
"""Test lookup_key with valid lowercase key (should convert to uppercase)"""
result = SampleBlock.lookup_key("block_a")
assert result == SampleBlock.BLOCK_A
assert result.name == "BLOCK_A"
def test_lookup_key_valid_mixed_case(self):
"""Test lookup_key with mixed case key"""
result = SampleBlock.lookup_key("BlOcK_a")
assert result == SampleBlock.BLOCK_A
assert result.name == "BLOCK_A"
def test_lookup_key_with_numeric_enum(self):
"""Test lookup_key with numeric enum member"""
result = SampleBlock.lookup_key("HAS_NUM")
assert result == SampleBlock.HAS_NUM
assert result.value == 5
def test_lookup_key_legacy_colon_replacement(self):
"""Test lookup_key with legacy colon format (converts : to ___)"""
# This assumes the enum has a key that might be accessed with legacy format
# Should convert : to ___ and look up LEGACY___KEY
# Since we don't have this key, we test the behavior with a valid conversion
# Let's test with a known key that would work
with pytest.raises(ValueError, match="Invalid key"):
SampleBlock.lookup_key("BLOCK:A") # Should fail as BLOCK___A doesn't exist
def test_lookup_key_invalid_key(self):
"""Test lookup_key with invalid key"""
with pytest.raises(ValueError, match="Invalid key: NONEXISTENT"):
SampleBlock.lookup_key("NONEXISTENT")
def test_lookup_key_empty_string(self):
"""Test lookup_key with empty string"""
with pytest.raises(ValueError, match="Invalid key"):
SampleBlock.lookup_key("")
def test_lookup_key_with_special_characters(self):
"""Test lookup_key with special characters that might cause AttributeError"""
with pytest.raises(ValueError, match="Invalid key"):
SampleBlock.lookup_key("@#$%")
def test_lookup_key_numeric_string(self):
"""Test lookup_key with numeric string that isn't a key"""
with pytest.raises(ValueError, match="Invalid key"):
SampleBlock.lookup_key("123")
class TestEnumBaseLookupValue:
"""Test cases for lookup_value class method"""
def test_lookup_value_valid_string(self):
"""Test lookup_value with valid string value"""
result = SampleBlock.lookup_value("block_a")
assert result == SampleBlock.BLOCK_A
assert result.name == "BLOCK_A"
assert result.value == "block_a"
def test_lookup_value_valid_integer(self):
"""Test lookup_value with valid integer value"""
result = SampleBlock.lookup_value(5)
assert result == SampleBlock.HAS_NUM
assert result.name == "HAS_NUM"
assert result.value == 5
def test_lookup_value_valid_float(self):
"""Test lookup_value with valid float value"""
result = SampleBlock.lookup_value(3.14)
assert result == SampleBlock.HAS_FLOAT
assert result.name == "HAS_FLOAT"
assert result.value == 3.14
def test_lookup_value_invalid_string(self):
"""Test lookup_value with invalid string value"""
with pytest.raises(ValueError, match="Invalid value: nonexistent"):
SampleBlock.lookup_value("nonexistent")
def test_lookup_value_invalid_integer(self):
"""Test lookup_value with invalid integer value"""
with pytest.raises(ValueError, match="Invalid value: 999"):
SampleBlock.lookup_value(999)
def test_lookup_value_case_sensitive(self):
"""Test that lookup_value is case-sensitive for string values"""
with pytest.raises(ValueError, match="Invalid value"):
SampleBlock.lookup_value("BLOCK_A") # Value is "block_a", not "BLOCK_A"
class TestEnumBaseFromAny:
"""Test cases for from_any class method"""
def test_from_any_with_enum_instance(self):
"""Test from_any with an enum instance (should return as-is)"""
enum_instance = SampleBlock.BLOCK_A
result = SampleBlock.from_any(enum_instance)
assert result is enum_instance
assert result == SampleBlock.BLOCK_A
def test_from_any_with_string_as_key(self):
"""Test from_any with string that matches a key"""
result = SampleBlock.from_any("BLOCK_A")
assert result == SampleBlock.BLOCK_A
assert result.name == "BLOCK_A"
assert result.value == "block_a"
def test_from_any_with_string_as_key_lowercase(self):
"""Test from_any with lowercase string key"""
result = SampleBlock.from_any("block_a")
# Should first try as key (convert to uppercase and find BLOCK_A)
assert result == SampleBlock.BLOCK_A
def test_from_any_with_string_as_value(self):
"""Test from_any with string that only matches a value"""
# Use a value that isn't also a valid key
result = SampleBlock.from_any("block_b")
# Should try key first (fail), then value (succeed)
assert result == SampleBlock.BLOCK_B
assert result.value == "block_b"
def test_from_any_with_integer(self):
"""Test from_any with integer value"""
result = SampleBlock.from_any(5)
assert result == SampleBlock.HAS_NUM
assert result.value == 5
def test_from_any_with_float(self):
"""Test from_any with float value"""
result = SampleBlock.from_any(3.14)
assert result == SampleBlock.HAS_FLOAT
assert result.value == 3.14
def test_from_any_with_invalid_string(self):
"""Test from_any with string that doesn't match key or value"""
with pytest.raises(ValueError, match="Could not find as key or value: invalid_string"):
SampleBlock.from_any("invalid_string")
def test_from_any_with_invalid_integer(self):
"""Test from_any with integer that doesn't match any value"""
with pytest.raises(ValueError, match="Invalid value: 999"):
SampleBlock.from_any(999)
def test_from_any_string_key_priority(self):
"""Test that from_any tries key lookup before value for strings"""
# Create an enum where a value matches another key
class AmbiguousEnum(EnumBase):
KEY_A = "key_b" # Value is the name of another key
KEY_B = "value_b"
# When we look up "KEY_B", it should find it as a key, not as value "key_b"
result = AmbiguousEnum.from_any("KEY_B")
assert result == AmbiguousEnum.KEY_B
assert result.value == "value_b"
class TestEnumBaseToValue:
"""Test cases for to_value instance method"""
def test_to_value_string_value(self):
"""Test to_value with string enum value"""
result = SampleBlock.BLOCK_A.to_value()
assert result == "block_a"
assert isinstance(result, str)
def test_to_value_integer_value(self):
"""Test to_value with integer enum value"""
result = SampleBlock.HAS_NUM.to_value()
assert result == 5
assert isinstance(result, int)
def test_to_value_float_value(self):
"""Test to_value with float enum value"""
result = SampleBlock.HAS_FLOAT.to_value()
assert result == 3.14
assert isinstance(result, float)
def test_to_value_equals_value_attribute(self):
"""Test that to_value returns the same as .value"""
enum_instance = SampleBlock.BLOCK_A
assert enum_instance.to_value() == enum_instance.value
class TestEnumBaseToLowerCase:
"""Test cases for to_lower_case instance method"""
def test_to_lower_case_uppercase_name(self):
"""Test to_lower_case with uppercase enum name"""
result = SampleBlock.BLOCK_A.to_lower_case()
assert result == "block_a"
assert isinstance(result, str)
def test_to_lower_case_mixed_name(self):
"""Test to_lower_case with name containing underscores"""
result = SampleBlock.HAS_NUM.to_lower_case()
assert result == "has_num"
def test_to_lower_case_consistency(self):
"""Test that to_lower_case always returns lowercase"""
for member in SampleBlock:
result = member.to_lower_case()
assert result == result.lower()
assert result == member.name.lower()
class TestEnumBaseStrMethod:
"""Test cases for __str__ magic method"""
def test_str_returns_name(self):
"""Test that str() returns the enum name"""
result = str(SampleBlock.BLOCK_A)
assert result == "BLOCK_A"
assert result == SampleBlock.BLOCK_A.name
def test_str_all_members(self):
"""Test str() for all enum members"""
for member in SampleBlock:
result = str(member)
assert result == member.name
assert isinstance(result, str)
def test_str_in_formatting(self):
"""Test that str works in string formatting"""
formatted = f"Enum: {SampleBlock.BLOCK_A}"
assert formatted == "Enum: BLOCK_A"
def test_str_vs_repr(self):
"""Test difference between str and repr"""
enum_instance = SampleBlock.BLOCK_A
str_result = str(enum_instance)
repr_result = repr(enum_instance)
assert str_result == "BLOCK_A"
# repr should include class name
assert "SampleBlock" in repr_result
# Parametrized tests for comprehensive coverage
class TestParametrized:
"""Parametrized tests for better coverage"""
@pytest.mark.parametrize("key,expected_member", [
("BLOCK_A", SampleBlock.BLOCK_A),
("block_a", SampleBlock.BLOCK_A),
("BLOCK_B", SampleBlock.BLOCK_B),
("HAS_NUM", SampleBlock.HAS_NUM),
("has_num", SampleBlock.HAS_NUM),
("HAS_FLOAT", SampleBlock.HAS_FLOAT),
])
def test_lookup_key_parametrized(self, key: str, expected_member: EnumBase):
"""Test lookup_key with various valid keys"""
result = SampleBlock.lookup_key(key)
assert result == expected_member
@pytest.mark.parametrize("value,expected_member", [
("block_a", SampleBlock.BLOCK_A),
("block_b", SampleBlock.BLOCK_B),
(5, SampleBlock.HAS_NUM),
(3.14, SampleBlock.HAS_FLOAT),
("legacy_value", SampleBlock.LEGACY_KEY),
])
def test_lookup_value_parametrized(self, value: Any, expected_member: EnumBase):
"""Test lookup_value with various valid values"""
result = SampleBlock.lookup_value(value)
assert result == expected_member
@pytest.mark.parametrize("input_any,expected_member", [
("BLOCK_A", SampleBlock.BLOCK_A),
("block_a", SampleBlock.BLOCK_A),
("block_b", SampleBlock.BLOCK_B),
(5, SampleBlock.HAS_NUM),
(3.14, SampleBlock.HAS_FLOAT),
(SampleBlock.BLOCK_A, SampleBlock.BLOCK_A), # Pass enum instance
])
def test_from_any_parametrized(self, input_any: Any, expected_member: EnumBase):
"""Test from_any with various valid inputs"""
result = SampleBlock.from_any(input_any)
assert result == expected_member
@pytest.mark.parametrize("invalid_key", [
"NONEXISTENT",
"invalid",
"123",
"",
"BLOCK_C",
])
def test_lookup_key_invalid_parametrized(self, invalid_key: str):
"""Test lookup_key with various invalid keys"""
with pytest.raises(ValueError, match="Invalid key"):
SampleBlock.lookup_key(invalid_key)
@pytest.mark.parametrize("invalid_value", [
"nonexistent",
999,
-1,
0.0,
"BLOCK_A", # This is a key name, not a value
])
def test_lookup_value_invalid_parametrized(self, invalid_value: Any):
"""Test lookup_value with various invalid values"""
with pytest.raises(ValueError, match="Invalid value"):
SampleBlock.lookup_value(invalid_value)
# Edge cases and special scenarios
class TestEdgeCases:
"""Test edge cases and special scenarios"""
def test_enum_with_single_member(self):
"""Test EnumBase with only one member"""
class SingleEnum(EnumBase):
ONLY_ONE = "single"
result = SingleEnum.from_any("ONLY_ONE")
assert result == SingleEnum.ONLY_ONE
assert result.to_value() == "single"
def test_enum_iteration(self):
"""Test iterating over enum members"""
members = list(SampleBlock)
assert len(members) == 5
assert SampleBlock.BLOCK_A in members
assert SampleBlock.BLOCK_B in members
assert SampleBlock.HAS_NUM in members
def test_enum_membership(self):
"""Test checking membership in enum"""
assert SampleBlock.BLOCK_A in SampleBlock
assert SampleBlock.HAS_NUM in SampleBlock
def test_enum_comparison(self):
"""Test comparing enum members"""
assert SampleBlock.BLOCK_A == SampleBlock.BLOCK_A
assert SampleBlock.BLOCK_A != SampleBlock.BLOCK_B
assert SampleBlock.from_any("BLOCK_A") == SampleBlock.BLOCK_A
def test_enum_identity(self):
"""Test enum member identity"""
member1 = SampleBlock.BLOCK_A
member2 = SampleBlock.lookup_key("BLOCK_A")
member3 = SampleBlock.from_any("BLOCK_A")
assert member1 is member2
assert member1 is member3
assert member2 is member3
def test_different_enum_classes(self):
"""Test that different enum classes are distinct"""
# Even if they have same keys/values, they're different
class OtherEnum(EnumBase):
BLOCK_A = "block_a"
result1 = SampleBlock.from_any("BLOCK_A")
result2 = OtherEnum.from_any("BLOCK_A")
assert result1 != result2
assert not isinstance(result1, type(result2))
def test_numeric_enum_operations(self):
"""Test operations specific to numeric enums"""
assert NumericEnum.FIRST.to_value() == 1
assert NumericEnum.SECOND.to_value() == 2
assert NumericEnum.THIRD.to_value() == 3
# Test from_any with integers
assert NumericEnum.from_any(1) == NumericEnum.FIRST
assert NumericEnum.from_any(2) == NumericEnum.SECOND
def test_mixed_value_types_in_same_enum(self):
"""Test enum with mixed value types"""
# SampleBlock already has mixed types (strings, int, float)
assert isinstance(SampleBlock.BLOCK_A.to_value(), str)
assert isinstance(SampleBlock.HAS_NUM.to_value(), int)
assert isinstance(SampleBlock.HAS_FLOAT.to_value(), float)
def test_from_any_chained_calls(self):
"""Test that from_any can be chained (idempotent)"""
result1 = SampleBlock.from_any("BLOCK_A")
result2 = SampleBlock.from_any(result1)
result3 = SampleBlock.from_any(result2)
assert result1 == result2 == result3
assert result1 is result2 is result3
# Integration tests
class TestIntegration:
"""Integration tests combining multiple methods"""
def test_round_trip_key_lookup(self):
"""Test round-trip from key to enum and back"""
original_key = "BLOCK_A"
enum_member = SampleBlock.lookup_key(original_key)
result_name = str(enum_member)
assert result_name == original_key
def test_round_trip_value_lookup(self):
"""Test round-trip from value to enum and back"""
original_value = "block_a"
enum_member = SampleBlock.lookup_value(original_value)
result_value = enum_member.to_value()
assert result_value == original_value
def test_from_any_workflow(self):
"""Test realistic workflow using from_any"""
# Simulate receiving various types of input
inputs = [
"BLOCK_A", # Key as string
"block_b", # Value as string
5, # Numeric value
SampleBlock.HAS_FLOAT, # Already an enum
]
expected = [
SampleBlock.BLOCK_A,
SampleBlock.BLOCK_B,
SampleBlock.HAS_NUM,
SampleBlock.HAS_FLOAT,
]
for input_val, expected_val in zip(inputs, expected):
result = SampleBlock.from_any(input_val)
assert result == expected_val
def test_enum_in_dictionary(self):
"""Test using enum as dictionary key"""
enum_dict = {
SampleBlock.BLOCK_A: "Value A",
SampleBlock.BLOCK_B: "Value B",
SampleBlock.HAS_NUM: "Value Num",
}
assert enum_dict[SampleBlock.BLOCK_A] == "Value A"
block_b = SampleBlock.from_any("BLOCK_B")
assert isinstance(block_b, SampleBlock)
assert enum_dict[block_b] == "Value B"
def test_enum_in_set(self):
"""Test using enum in a set"""
enum_set = {SampleBlock.BLOCK_A, SampleBlock.BLOCK_B, SampleBlock.BLOCK_A}
assert len(enum_set) == 2 # BLOCK_A should be deduplicated
assert SampleBlock.BLOCK_A in enum_set
assert SampleBlock.from_any("BLOCK_B") in enum_set
# Real-world usage scenarios
class TestRealWorldScenarios:
"""Test real-world usage scenarios from enum_test.py"""
def test_original_enum_test_scenario(self):
"""Test the scenario from the original enum_test.py"""
# BLOCK A: {SampleBlock.from_any('BLOCK_A')}
result_a = SampleBlock.from_any('BLOCK_A')
assert result_a == SampleBlock.BLOCK_A
assert str(result_a) == "BLOCK_A"
# HAS NUM: {SampleBlock.from_any(5)}
result_num = SampleBlock.from_any(5)
assert result_num == SampleBlock.HAS_NUM
assert result_num.to_value() == 5
# DIRECT BLOCK: {SampleBlock.BLOCK_A.name} -> {SampleBlock.BLOCK_A.value}
assert SampleBlock.BLOCK_A.name == "BLOCK_A"
assert SampleBlock.BLOCK_A.value == "block_a"
def test_config_value_parsing(self):
"""Test parsing values from configuration (common use case)"""
# Simulate config values that might come as strings
config_values = ["OPTION_ONE", "option_two", "OPTION_THREE"]
results = [SimpleEnum.from_any(val) for val in config_values]
assert results[0] == SimpleEnum.OPTION_ONE
assert results[1] == SimpleEnum.OPTION_TWO
assert results[2] == SimpleEnum.OPTION_THREE
def test_api_response_mapping(self):
"""Test mapping API response values to enum"""
# Simulate API returning numeric codes
api_codes = [1, 2, 3]
results = [NumericEnum.from_any(code) for code in api_codes]
assert results[0] == NumericEnum.FIRST
assert results[1] == NumericEnum.SECOND
assert results[2] == NumericEnum.THIRD
def test_validation_with_error_handling(self):
"""Test validation with proper error handling"""
valid_input = "BLOCK_A"
invalid_input = "INVALID"
# Valid input should work
result = SampleBlock.from_any(valid_input)
assert result == SampleBlock.BLOCK_A
# Invalid input should raise ValueError
try:
SampleBlock.from_any(invalid_input)
assert False, "Should have raised ValueError"
except ValueError as e:
assert "Could not find as key or value" in str(e)
assert "INVALID" in str(e)

View File

@@ -1,241 +0,0 @@
"""
var helpers
"""
# ADDED 2025/7/11 Replace 'your_module' with actual module name
from typing import Any
import pytest
from corelibs.var_handling.var_helpers import is_int, is_float, str_to_bool
class TestIsInt:
"""Test cases for is_int function"""
def test_valid_integers(self):
"""Test with valid integer strings"""
assert is_int("123") is True
assert is_int("0") is True
assert is_int("-456") is True
assert is_int("+789") is True
assert is_int("000") is True
def test_invalid_integers(self):
"""Test with invalid integer strings"""
assert is_int("12.34") is False
assert is_int("abc") is False
assert is_int("12a") is False
assert is_int("") is False
assert is_int(" ") is False
assert is_int("12.0") is False
assert is_int("1e5") is False
def test_numeric_types(self):
"""Test with actual numeric types"""
assert is_int(123) is True
assert is_int(0) is True
assert is_int(-456) is True
assert is_int(12.34) is True # float can be converted to int
assert is_int(12.0) is True
def test_other_types(self):
"""Test with other data types"""
assert is_int(None) is False
assert is_int([]) is False
assert is_int({}) is False
assert is_int(True) is True # bool is subclass of int
assert is_int(False) is True
class TestIsFloat:
"""Test cases for is_float function"""
def test_valid_floats(self):
"""Test with valid float strings"""
assert is_float("12.34") is True
assert is_float("0.0") is True
assert is_float("-45.67") is True
assert is_float("+78.9") is True
assert is_float("123") is True # integers are valid floats
assert is_float("0") is True
assert is_float("1e5") is True
assert is_float("1.5e-10") is True
assert is_float("inf") is True
assert is_float("-inf") is True
assert is_float("nan") is True
def test_invalid_floats(self):
"""Test with invalid float strings"""
assert is_float("abc") is False
assert is_float("12.34.56") is False
assert is_float("12a") is False
assert is_float("") is False
assert is_float(" ") is False
assert is_float("12..34") is False
def test_numeric_types(self):
"""Test with actual numeric types"""
assert is_float(123) is True
assert is_float(12.34) is True
assert is_float(0) is True
assert is_float(-45.67) is True
def test_other_types(self):
"""Test with other data types"""
assert is_float(None) is False
assert is_float([]) is False
assert is_float({}) is False
assert is_float(True) is True # bool can be converted to float
assert is_float(False) is True
class TestStrToBool:
"""Test cases for str_to_bool function"""
def test_valid_true_strings(self):
"""Test with valid true strings"""
assert str_to_bool("True") is True
assert str_to_bool("true") is True
def test_valid_false_strings(self):
"""Test with valid false strings"""
assert str_to_bool("False") is False
assert str_to_bool("false") is False
def test_invalid_strings(self):
"""Test with invalid boolean strings"""
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool("TRUE")
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool("FALSE")
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool("yes")
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool("no")
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool("1")
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool("0")
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool("")
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool(" True")
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool("True ")
def test_error_message_content(self):
"""Test that error messages contain the invalid input"""
with pytest.raises(ValueError) as exc_info:
str_to_bool("invalid")
assert "Invalid boolean string: invalid" in str(exc_info.value)
def test_case_sensitivity(self):
"""Test that function is case sensitive"""
with pytest.raises(ValueError):
str_to_bool("TRUE")
with pytest.raises(ValueError):
str_to_bool("True ") # with space
with pytest.raises(ValueError):
str_to_bool(" True") # with space
# Additional edge case tests
class TestEdgeCases:
"""Test edge cases and special scenarios"""
def test_is_int_with_whitespace(self):
"""Test is_int with whitespace (should work due to int() behavior)"""
assert is_int(" 123 ") is True
assert is_int("\t456\n") is True
def test_is_float_with_whitespace(self):
"""Test is_float with whitespace (should work due to float() behavior)"""
assert is_float(" 12.34 ") is True
assert is_float("\t45.67\n") is True
def test_large_numbers(self):
"""Test with very large numbers"""
large_int = "123456789012345678901234567890"
assert is_int(large_int) is True
assert is_float(large_int) is True
def test_scientific_notation(self):
"""Test scientific notation"""
assert is_int("1e5") is False # int() doesn't handle scientific notation
assert is_float("1e5") is True
assert is_float("1.5e-10") is True
assert is_float("2E+3") is True
# Parametrized tests for more comprehensive coverage
class TestParametrized:
"""Parametrized tests for better coverage"""
@pytest.mark.parametrize("value,expected", [
("123", True),
("0", True),
("-456", True),
("12.34", False),
("abc", False),
("", False),
(123, True),
(12.5, True),
(None, False),
])
def test_is_int_parametrized(self, value: Any, expected: bool):
"""Test"""
assert is_int(value) == expected
@pytest.mark.parametrize("value,expected", [
("12.34", True),
("123", True),
("0", True),
("-45.67", True),
("inf", True),
("nan", True),
("abc", False),
("", False),
(12.34, True),
(123, True),
(None, False),
])
def test_is_float_parametrized(self, value: Any, expected: bool):
"""test"""
assert is_float(value) == expected
@pytest.mark.parametrize("value,expected", [
("True", True),
("true", True),
("False", False),
("false", False),
])
def test_str_to_bool_valid_parametrized(self, value: Any, expected: bool):
"""test"""
assert str_to_bool(value) == expected
@pytest.mark.parametrize("invalid_value", [
"TRUE",
"FALSE",
"yes",
"no",
"1",
"0",
"",
" True",
"True ",
"invalid",
])
def test_str_to_bool_invalid_parametrized(self, invalid_value: Any):
"""test"""
with pytest.raises(ValueError):
str_to_bool(invalid_value)

164
uv.lock generated
View File

@@ -4,11 +4,11 @@ requires-python = ">=3.13"
[[package]]
name = "certifi"
version = "2025.10.5"
version = "2025.11.12"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
]
[[package]]
@@ -108,9 +108,12 @@ wheels = [
[[package]]
name = "corelibs"
version = "0.34.0"
version = "0.39.1"
source = { editable = "." }
dependencies = [
{ name = "corelibs-datetime" },
{ name = "corelibs-enum-base" },
{ name = "corelibs-var" },
{ name = "cryptography" },
{ name = "jmespath" },
{ name = "jsonpath-ng" },
@@ -127,6 +130,9 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "corelibs-datetime", specifier = ">=1.0.1", index = "https://git.egplusww.jp/api/packages/PyPI/pypi/simple/" },
{ name = "corelibs-enum-base", specifier = ">=1.0.0", index = "https://git.egplusww.jp/api/packages/PyPI/pypi/simple/" },
{ name = "corelibs-var", specifier = ">=1.0.0", index = "https://git.egplusww.jp/api/packages/PyPI/pypi/simple/" },
{ name = "cryptography", specifier = ">=46.0.3" },
{ name = "jmespath", specifier = ">=1.0.1" },
{ name = "jsonpath-ng", specifier = ">=1.7.0" },
@@ -142,64 +148,94 @@ dev = [
]
[[package]]
name = "coverage"
version = "7.11.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905, upload-time = "2025-10-15T15:15:08.542Z" }
name = "corelibs-datetime"
version = "1.0.1"
source = { registry = "https://git.egplusww.jp/api/packages/PyPI/pypi/simple/" }
dependencies = [
{ name = "corelibs-var" },
]
sdist = { url = "https://git.egplusww.jp/api/packages/PyPI/pypi/files/corelibs-datetime/1.0.1/corelibs_datetime-1.0.1.tar.gz", hash = "sha256:ff58c6f824f35b87b1a5c153f65fdd82b65e42bb5a649d46d9115dc5fa61042f" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1", size = 216129, upload-time = "2025-10-15T15:13:25.371Z" },
{ url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be", size = 216380, upload-time = "2025-10-15T15:13:26.976Z" },
{ url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d", size = 247375, upload-time = "2025-10-15T15:13:28.923Z" },
{ url = "https://files.pythonhosted.org/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82", size = 249978, upload-time = "2025-10-15T15:13:30.525Z" },
{ url = "https://files.pythonhosted.org/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52", size = 251253, upload-time = "2025-10-15T15:13:32.174Z" },
{ url = "https://files.pythonhosted.org/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b", size = 247591, upload-time = "2025-10-15T15:13:34.147Z" },
{ url = "https://files.pythonhosted.org/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4", size = 249411, upload-time = "2025-10-15T15:13:38.425Z" },
{ url = "https://files.pythonhosted.org/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd", size = 247303, upload-time = "2025-10-15T15:13:40.464Z" },
{ url = "https://files.pythonhosted.org/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc", size = 247157, upload-time = "2025-10-15T15:13:42.087Z" },
{ url = "https://files.pythonhosted.org/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48", size = 248921, upload-time = "2025-10-15T15:13:43.715Z" },
{ url = "https://files.pythonhosted.org/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040", size = 218526, upload-time = "2025-10-15T15:13:45.336Z" },
{ url = "https://files.pythonhosted.org/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05", size = 219317, upload-time = "2025-10-15T15:13:47.401Z" },
{ url = "https://files.pythonhosted.org/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a", size = 217948, upload-time = "2025-10-15T15:13:49.096Z" },
{ url = "https://files.pythonhosted.org/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b", size = 216837, upload-time = "2025-10-15T15:13:51.09Z" },
{ url = "https://files.pythonhosted.org/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37", size = 217061, upload-time = "2025-10-15T15:13:52.747Z" },
{ url = "https://files.pythonhosted.org/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de", size = 258398, upload-time = "2025-10-15T15:13:54.45Z" },
{ url = "https://files.pythonhosted.org/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f", size = 260574, upload-time = "2025-10-15T15:13:56.145Z" },
{ url = "https://files.pythonhosted.org/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c", size = 262797, upload-time = "2025-10-15T15:13:58.635Z" },
{ url = "https://files.pythonhosted.org/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa", size = 257361, upload-time = "2025-10-15T15:14:00.409Z" },
{ url = "https://files.pythonhosted.org/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740", size = 260349, upload-time = "2025-10-15T15:14:02.188Z" },
{ url = "https://files.pythonhosted.org/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef", size = 258114, upload-time = "2025-10-15T15:14:03.907Z" },
{ url = "https://files.pythonhosted.org/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0", size = 256723, upload-time = "2025-10-15T15:14:06.324Z" },
{ url = "https://files.pythonhosted.org/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca", size = 259238, upload-time = "2025-10-15T15:14:08.002Z" },
{ url = "https://files.pythonhosted.org/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2", size = 219180, upload-time = "2025-10-15T15:14:09.786Z" },
{ url = "https://files.pythonhosted.org/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268", size = 220241, upload-time = "2025-10-15T15:14:11.471Z" },
{ url = "https://files.pythonhosted.org/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836", size = 218510, upload-time = "2025-10-15T15:14:13.46Z" },
{ url = "https://files.pythonhosted.org/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497", size = 216110, upload-time = "2025-10-15T15:14:15.157Z" },
{ url = "https://files.pythonhosted.org/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e", size = 216395, upload-time = "2025-10-15T15:14:16.863Z" },
{ url = "https://files.pythonhosted.org/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1", size = 247433, upload-time = "2025-10-15T15:14:18.589Z" },
{ url = "https://files.pythonhosted.org/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca", size = 249970, upload-time = "2025-10-15T15:14:20.307Z" },
{ url = "https://files.pythonhosted.org/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd", size = 251324, upload-time = "2025-10-15T15:14:22.419Z" },
{ url = "https://files.pythonhosted.org/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43", size = 247445, upload-time = "2025-10-15T15:14:24.205Z" },
{ url = "https://files.pythonhosted.org/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777", size = 249324, upload-time = "2025-10-15T15:14:26.234Z" },
{ url = "https://files.pythonhosted.org/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2", size = 247261, upload-time = "2025-10-15T15:14:28.42Z" },
{ url = "https://files.pythonhosted.org/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d", size = 247092, upload-time = "2025-10-15T15:14:30.784Z" },
{ url = "https://files.pythonhosted.org/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4", size = 248755, upload-time = "2025-10-15T15:14:32.585Z" },
{ url = "https://files.pythonhosted.org/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721", size = 218793, upload-time = "2025-10-15T15:14:34.972Z" },
{ url = "https://files.pythonhosted.org/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad", size = 219587, upload-time = "2025-10-15T15:14:37.047Z" },
{ url = "https://files.pythonhosted.org/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479", size = 218168, upload-time = "2025-10-15T15:14:38.861Z" },
{ url = "https://files.pythonhosted.org/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f", size = 216850, upload-time = "2025-10-15T15:14:40.651Z" },
{ url = "https://files.pythonhosted.org/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e", size = 217071, upload-time = "2025-10-15T15:14:42.755Z" },
{ url = "https://files.pythonhosted.org/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44", size = 258570, upload-time = "2025-10-15T15:14:44.542Z" },
{ url = "https://files.pythonhosted.org/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3", size = 260738, upload-time = "2025-10-15T15:14:46.436Z" },
{ url = "https://files.pythonhosted.org/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b", size = 262994, upload-time = "2025-10-15T15:14:48.3Z" },
{ url = "https://files.pythonhosted.org/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d", size = 257282, upload-time = "2025-10-15T15:14:50.236Z" },
{ url = "https://files.pythonhosted.org/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2", size = 260430, upload-time = "2025-10-15T15:14:52.413Z" },
{ url = "https://files.pythonhosted.org/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e", size = 258190, upload-time = "2025-10-15T15:14:54.268Z" },
{ url = "https://files.pythonhosted.org/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996", size = 256658, upload-time = "2025-10-15T15:14:56.436Z" },
{ url = "https://files.pythonhosted.org/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11", size = 259342, upload-time = "2025-10-15T15:14:58.538Z" },
{ url = "https://files.pythonhosted.org/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73", size = 219568, upload-time = "2025-10-15T15:15:00.382Z" },
{ url = "https://files.pythonhosted.org/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547", size = 220687, upload-time = "2025-10-15T15:15:02.322Z" },
{ url = "https://files.pythonhosted.org/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3", size = 218711, upload-time = "2025-10-15T15:15:04.575Z" },
{ url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" },
{ url = "https://git.egplusww.jp/api/packages/PyPI/pypi/files/corelibs-datetime/1.0.1/corelibs_datetime-1.0.1-py3-none-any.whl", hash = "sha256:f1a4d431f9f913dd39976a119ff8a2db34e966c61b1775c26b0da72a8bdb5ec1" },
]
[[package]]
name = "corelibs-enum-base"
version = "1.0.0"
source = { registry = "https://git.egplusww.jp/api/packages/PyPI/pypi/simple/" }
sdist = { url = "https://git.egplusww.jp/api/packages/PyPI/pypi/files/corelibs-enum-base/1.0.0/corelibs_enum_base-1.0.0.tar.gz", hash = "sha256:c696a297d88f674d40e5d190f396909b5f663a995ac735e545ceb5bb4907121d" }
wheels = [
{ url = "https://git.egplusww.jp/api/packages/PyPI/pypi/files/corelibs-enum-base/1.0.0/corelibs_enum_base-1.0.0-py3-none-any.whl", hash = "sha256:c305d4063c69021aaf9ef75fbcce961039dae3c3de7820febeac7082c998a1f8" },
]
[[package]]
name = "corelibs-var"
version = "1.0.0"
source = { registry = "https://git.egplusww.jp/api/packages/PyPI/pypi/simple/" }
sdist = { url = "https://git.egplusww.jp/api/packages/PyPI/pypi/files/corelibs-var/1.0.0/corelibs_var-1.0.0.tar.gz", hash = "sha256:b85d6fd3802a1b687290666e4b1dbb47cf9723aa72bf73eb004e9e4936776364" }
wheels = [
{ url = "https://git.egplusww.jp/api/packages/PyPI/pypi/files/corelibs-var/1.0.0/corelibs_var-1.0.0-py3-none-any.whl", hash = "sha256:a3546785bf9c94eec08b5c500b69b971e83e11d92bc0e4d3cbd9411a561fdbc2" },
]
[[package]]
name = "coverage"
version = "7.12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725, upload-time = "2025-11-18T13:32:49.22Z" },
{ url = "https://files.pythonhosted.org/packages/17/a7/3aa4144d3bcb719bf67b22d2d51c2d577bf801498c13cb08f64173e80497/coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", size = 218098, upload-time = "2025-11-18T13:32:50.78Z" },
{ url = "https://files.pythonhosted.org/packages/fc/9c/b846bbc774ff81091a12a10203e70562c91ae71badda00c5ae5b613527b1/coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", size = 249093, upload-time = "2025-11-18T13:32:52.554Z" },
{ url = "https://files.pythonhosted.org/packages/76/b6/67d7c0e1f400b32c883e9342de4a8c2ae7c1a0b57c5de87622b7262e2309/coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", size = 251686, upload-time = "2025-11-18T13:32:54.862Z" },
{ url = "https://files.pythonhosted.org/packages/cc/75/b095bd4b39d49c3be4bffbb3135fea18a99a431c52dd7513637c0762fecb/coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", size = 252930, upload-time = "2025-11-18T13:32:56.417Z" },
{ url = "https://files.pythonhosted.org/packages/6e/f3/466f63015c7c80550bead3093aacabf5380c1220a2a93c35d374cae8f762/coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", size = 249296, upload-time = "2025-11-18T13:32:58.074Z" },
{ url = "https://files.pythonhosted.org/packages/27/86/eba2209bf2b7e28c68698fc13437519a295b2d228ba9e0ec91673e09fa92/coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", size = 251068, upload-time = "2025-11-18T13:32:59.646Z" },
{ url = "https://files.pythonhosted.org/packages/ec/55/ca8ae7dbba962a3351f18940b359b94c6bafdd7757945fdc79ec9e452dc7/coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", size = 249034, upload-time = "2025-11-18T13:33:01.481Z" },
{ url = "https://files.pythonhosted.org/packages/7a/d7/39136149325cad92d420b023b5fd900dabdd1c3a0d1d5f148ef4a8cedef5/coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", size = 248853, upload-time = "2025-11-18T13:33:02.935Z" },
{ url = "https://files.pythonhosted.org/packages/fe/b6/76e1add8b87ef60e00643b0b7f8f7bb73d4bf5249a3be19ebefc5793dd25/coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", size = 250619, upload-time = "2025-11-18T13:33:04.336Z" },
{ url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261, upload-time = "2025-11-18T13:33:06.493Z" },
{ url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072, upload-time = "2025-11-18T13:33:07.926Z" },
{ url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702, upload-time = "2025-11-18T13:33:09.631Z" },
{ url = "https://files.pythonhosted.org/packages/9b/62/f87922641c7198667994dd472a91e1d9b829c95d6c29529ceb52132436ad/coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", size = 218420, upload-time = "2025-11-18T13:33:11.153Z" },
{ url = "https://files.pythonhosted.org/packages/85/dd/1cc13b2395ef15dbb27d7370a2509b4aee77890a464fb35d72d428f84871/coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", size = 218773, upload-time = "2025-11-18T13:33:12.569Z" },
{ url = "https://files.pythonhosted.org/packages/74/40/35773cc4bb1e9d4658d4fb669eb4195b3151bef3bbd6f866aba5cd5dac82/coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", size = 260078, upload-time = "2025-11-18T13:33:14.037Z" },
{ url = "https://files.pythonhosted.org/packages/ec/ee/231bb1a6ffc2905e396557585ebc6bdc559e7c66708376d245a1f1d330fc/coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", size = 262144, upload-time = "2025-11-18T13:33:15.601Z" },
{ url = "https://files.pythonhosted.org/packages/28/be/32f4aa9f3bf0b56f3971001b56508352c7753915345d45fab4296a986f01/coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", size = 264574, upload-time = "2025-11-18T13:33:17.354Z" },
{ url = "https://files.pythonhosted.org/packages/68/7c/00489fcbc2245d13ab12189b977e0cf06ff3351cb98bc6beba8bd68c5902/coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", size = 259298, upload-time = "2025-11-18T13:33:18.958Z" },
{ url = "https://files.pythonhosted.org/packages/96/b4/f0760d65d56c3bea95b449e02570d4abd2549dc784bf39a2d4721a2d8ceb/coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", size = 262150, upload-time = "2025-11-18T13:33:20.644Z" },
{ url = "https://files.pythonhosted.org/packages/c5/71/9a9314df00f9326d78c1e5a910f520d599205907432d90d1c1b7a97aa4b1/coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", size = 259763, upload-time = "2025-11-18T13:33:22.189Z" },
{ url = "https://files.pythonhosted.org/packages/10/34/01a0aceed13fbdf925876b9a15d50862eb8845454301fe3cdd1df08b2182/coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", size = 258653, upload-time = "2025-11-18T13:33:24.239Z" },
{ url = "https://files.pythonhosted.org/packages/8d/04/81d8fd64928acf1574bbb0181f66901c6c1c6279c8ccf5f84259d2c68ae9/coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", size = 260856, upload-time = "2025-11-18T13:33:26.365Z" },
{ url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936, upload-time = "2025-11-18T13:33:28.165Z" },
{ url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001, upload-time = "2025-11-18T13:33:29.656Z" },
{ url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273, upload-time = "2025-11-18T13:33:31.415Z" },
{ url = "https://files.pythonhosted.org/packages/bf/2e/fc12db0883478d6e12bbd62d481210f0c8daf036102aa11434a0c5755825/coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", size = 217777, upload-time = "2025-11-18T13:33:32.86Z" },
{ url = "https://files.pythonhosted.org/packages/1f/c1/ce3e525d223350c6ec16b9be8a057623f54226ef7f4c2fee361ebb6a02b8/coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", size = 218100, upload-time = "2025-11-18T13:33:34.532Z" },
{ url = "https://files.pythonhosted.org/packages/15/87/113757441504aee3808cb422990ed7c8bcc2d53a6779c66c5adef0942939/coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", size = 249151, upload-time = "2025-11-18T13:33:36.135Z" },
{ url = "https://files.pythonhosted.org/packages/d9/1d/9529d9bd44049b6b05bb319c03a3a7e4b0a8a802d28fa348ad407e10706d/coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d", size = 251667, upload-time = "2025-11-18T13:33:37.996Z" },
{ url = "https://files.pythonhosted.org/packages/11/bb/567e751c41e9c03dc29d3ce74b8c89a1e3396313e34f255a2a2e8b9ebb56/coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", size = 253003, upload-time = "2025-11-18T13:33:39.553Z" },
{ url = "https://files.pythonhosted.org/packages/e4/b3/c2cce2d8526a02fb9e9ca14a263ca6fc074449b33a6afa4892838c903528/coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", size = 249185, upload-time = "2025-11-18T13:33:42.086Z" },
{ url = "https://files.pythonhosted.org/packages/0e/a7/967f93bb66e82c9113c66a8d0b65ecf72fc865adfba5a145f50c7af7e58d/coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", size = 251025, upload-time = "2025-11-18T13:33:43.634Z" },
{ url = "https://files.pythonhosted.org/packages/b9/b2/f2f6f56337bc1af465d5b2dc1ee7ee2141b8b9272f3bf6213fcbc309a836/coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", size = 248979, upload-time = "2025-11-18T13:33:46.04Z" },
{ url = "https://files.pythonhosted.org/packages/f4/7a/bf4209f45a4aec09d10a01a57313a46c0e0e8f4c55ff2965467d41a92036/coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", size = 248800, upload-time = "2025-11-18T13:33:47.546Z" },
{ url = "https://files.pythonhosted.org/packages/b8/b7/1e01b8696fb0521810f60c5bbebf699100d6754183e6cc0679bf2ed76531/coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", size = 250460, upload-time = "2025-11-18T13:33:49.537Z" },
{ url = "https://files.pythonhosted.org/packages/71/ae/84324fb9cb46c024760e706353d9b771a81b398d117d8c1fe010391c186f/coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", size = 220533, upload-time = "2025-11-18T13:33:51.16Z" },
{ url = "https://files.pythonhosted.org/packages/e2/71/1033629deb8460a8f97f83e6ac4ca3b93952e2b6f826056684df8275e015/coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", size = 221348, upload-time = "2025-11-18T13:33:52.776Z" },
{ url = "https://files.pythonhosted.org/packages/0a/5f/ac8107a902f623b0c251abdb749be282dc2ab61854a8a4fcf49e276fce2f/coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", size = 219922, upload-time = "2025-11-18T13:33:54.316Z" },
{ url = "https://files.pythonhosted.org/packages/79/6e/f27af2d4da367f16077d21ef6fe796c874408219fa6dd3f3efe7751bd910/coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", size = 218511, upload-time = "2025-11-18T13:33:56.343Z" },
{ url = "https://files.pythonhosted.org/packages/67/dd/65fd874aa460c30da78f9d259400d8e6a4ef457d61ab052fd248f0050558/coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", size = 218771, upload-time = "2025-11-18T13:33:57.966Z" },
{ url = "https://files.pythonhosted.org/packages/55/e0/7c6b71d327d8068cb79c05f8f45bf1b6145f7a0de23bbebe63578fe5240a/coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", size = 260151, upload-time = "2025-11-18T13:33:59.597Z" },
{ url = "https://files.pythonhosted.org/packages/49/ce/4697457d58285b7200de6b46d606ea71066c6e674571a946a6ea908fb588/coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", size = 262257, upload-time = "2025-11-18T13:34:01.166Z" },
{ url = "https://files.pythonhosted.org/packages/2f/33/acbc6e447aee4ceba88c15528dbe04a35fb4d67b59d393d2e0d6f1e242c1/coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", size = 264671, upload-time = "2025-11-18T13:34:02.795Z" },
{ url = "https://files.pythonhosted.org/packages/87/ec/e2822a795c1ed44d569980097be839c5e734d4c0c1119ef8e0a073496a30/coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", size = 259231, upload-time = "2025-11-18T13:34:04.397Z" },
{ url = "https://files.pythonhosted.org/packages/72/c5/a7ec5395bb4a49c9b7ad97e63f0c92f6bf4a9e006b1393555a02dae75f16/coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", size = 262137, upload-time = "2025-11-18T13:34:06.068Z" },
{ url = "https://files.pythonhosted.org/packages/67/0c/02c08858b764129f4ecb8e316684272972e60777ae986f3865b10940bdd6/coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", size = 259745, upload-time = "2025-11-18T13:34:08.04Z" },
{ url = "https://files.pythonhosted.org/packages/5a/04/4fd32b7084505f3829a8fe45c1a74a7a728cb251aaadbe3bec04abcef06d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", size = 258570, upload-time = "2025-11-18T13:34:09.676Z" },
{ url = "https://files.pythonhosted.org/packages/48/35/2365e37c90df4f5342c4fa202223744119fe31264ee2924f09f074ea9b6d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", size = 260899, upload-time = "2025-11-18T13:34:11.259Z" },
{ url = "https://files.pythonhosted.org/packages/05/56/26ab0464ca733fa325e8e71455c58c1c374ce30f7c04cebb88eabb037b18/coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", size = 221313, upload-time = "2025-11-18T13:34:12.863Z" },
{ url = "https://files.pythonhosted.org/packages/da/1c/017a3e1113ed34d998b27d2c6dba08a9e7cb97d362f0ec988fcd873dcf81/coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", size = 222423, upload-time = "2025-11-18T13:34:15.14Z" },
{ url = "https://files.pythonhosted.org/packages/4c/36/bcc504fdd5169301b52568802bb1b9cdde2e27a01d39fbb3b4b508ab7c2c/coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", size = 220459, upload-time = "2025-11-18T13:34:17.222Z" },
{ url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" },
]
[[package]]
@@ -391,7 +427,7 @@ wheels = [
[[package]]
name = "pytest"
version = "8.4.2"
version = "9.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -400,9 +436,9 @@ dependencies = [
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
{ url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" },
]
[[package]]