Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c16b086467 | ||
|
|
0ad8883809 | ||
|
|
51e9b1ce7c | ||
|
|
0d3104f60a | ||
|
|
d29f827fc9 | ||
|
|
282fe1f7c0 | ||
|
|
afce5043e4 | ||
|
|
5996bb1fc0 | ||
|
|
06a17d7c30 | ||
|
|
af7633183c | ||
|
|
1280b2f855 | ||
|
|
2e0b1f5951 | ||
|
|
548d7491b8 | ||
|
|
ad99115544 | ||
|
|
52919cbc49 | ||
|
|
7f2dc13c31 | ||
|
|
592652cff1 | ||
|
|
6a1724695e |
10
README.md
10
README.md
@@ -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/)
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Unit tests for encryption_handling module
|
||||
"""
|
||||
@@ -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'
|
||||
@@ -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__
|
||||
@@ -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__
|
||||
@@ -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__
|
||||
@@ -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, {}),
|
||||
])
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
var_handling tests
|
||||
"""
|
||||
@@ -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)
|
||||
@@ -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
164
uv.lock
generated
@@ -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]]
|
||||
|
||||
Reference in New Issue
Block a user