Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a00c27c465 | ||
|
|
1f7f4b8d53 | ||
|
|
baca79ce82 | ||
|
|
4265be6430 | ||
|
|
c16b086467 | ||
|
|
48a98c0206 | ||
|
|
f1788f057f | ||
|
|
0ad8883809 | ||
|
|
51e9b1ce7c | ||
|
|
0d3104f60a | ||
|
|
d29f827fc9 |
@@ -1,7 +1,7 @@
|
|||||||
# MARK: Project info
|
# MARK: Project info
|
||||||
[project]
|
[project]
|
||||||
name = "corelibs"
|
name = "corelibs"
|
||||||
version = "0.37.0"
|
version = "0.40.0"
|
||||||
description = "Collection of utils for Python scripts"
|
description = "Collection of utils for Python scripts"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ class SettingsLoader:
|
|||||||
entry_split_char: dict[str, str] = {}
|
entry_split_char: dict[str, str] = {}
|
||||||
# entries that should be converted
|
# entries that should be converted
|
||||||
entry_convert: dict[str, str] = {}
|
entry_convert: dict[str, str] = {}
|
||||||
|
# no args to set
|
||||||
|
args_none: list[str] = []
|
||||||
# all the settings for the config id given
|
# all the settings for the config id given
|
||||||
settings: dict[str, dict[str, Any]] = {
|
settings: dict[str, dict[str, Any]] = {
|
||||||
config_id: {},
|
config_id: {},
|
||||||
@@ -162,6 +164,8 @@ class SettingsLoader:
|
|||||||
f"[!] In [{config_id}] the split character setup for entry failed: {check}: {e}",
|
f"[!] In [{config_id}] the split character setup for entry failed: {check}: {e}",
|
||||||
'CRITICAL'
|
'CRITICAL'
|
||||||
)) from e
|
)) from e
|
||||||
|
if check == "args:no":
|
||||||
|
args_none.append(key)
|
||||||
if skip:
|
if skip:
|
||||||
continue
|
continue
|
||||||
settings[config_id][key] = [
|
settings[config_id][key] = [
|
||||||
@@ -185,9 +189,15 @@ class SettingsLoader:
|
|||||||
error: bool = False
|
error: bool = False
|
||||||
for entry, validate in config_validate.items():
|
for entry, validate in config_validate.items():
|
||||||
# if we have command line option set, this one overrides config
|
# if we have command line option set, this one overrides config
|
||||||
if self.__get_arg(entry):
|
if (args_entry := self.__get_arg(entry)) is not None:
|
||||||
self.__print(f"[*] Command line option override for: {entry}", 'WARNING')
|
self.__print(f"[*] Command line option override for: {entry}", 'WARNING')
|
||||||
settings[config_id][entry] = self.args.get(entry)
|
if (
|
||||||
|
entry not in args_none and
|
||||||
|
(isinstance(args_entry, list) and entry_split_char.get(entry)) or
|
||||||
|
(not isinstance(args_entry, list) and not entry_split_char.get(entry))
|
||||||
|
):
|
||||||
|
# args is list, but entry has not split, do not set
|
||||||
|
settings[config_id][entry] = args_entry
|
||||||
# validate checks
|
# validate checks
|
||||||
for check in validate:
|
for check in validate:
|
||||||
# CHECKS
|
# CHECKS
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class ConsoleFormat(Flag):
|
|||||||
FILE = auto()
|
FILE = auto()
|
||||||
FUNCTION = auto()
|
FUNCTION = auto()
|
||||||
LINENO = auto()
|
LINENO = auto()
|
||||||
|
LEVEL = auto()
|
||||||
|
|
||||||
|
|
||||||
class ConsoleFormatSettings:
|
class ConsoleFormatSettings:
|
||||||
@@ -43,14 +44,17 @@ class ConsoleFormatSettings:
|
|||||||
ConsoleFormat.NAME |
|
ConsoleFormat.NAME |
|
||||||
ConsoleFormat.FILE |
|
ConsoleFormat.FILE |
|
||||||
ConsoleFormat.FUNCTION |
|
ConsoleFormat.FUNCTION |
|
||||||
ConsoleFormat.LINENO
|
ConsoleFormat.LINENO |
|
||||||
|
ConsoleFormat.LEVEL
|
||||||
)
|
)
|
||||||
# show time with no time zone, file and line
|
# show time with no time zone, file, line and level
|
||||||
CONDENSED = ConsoleFormat.TIME | ConsoleFormat.FILE | ConsoleFormat.LINENO
|
CONDENSED = ConsoleFormat.TIME | ConsoleFormat.FILE | ConsoleFormat.LINENO | ConsoleFormat.LEVEL
|
||||||
# only time
|
# only time and level
|
||||||
MINIMAL = ConsoleFormat.TIME
|
MINIMAL = ConsoleFormat.TIME | ConsoleFormat.LEVEL
|
||||||
|
# only level
|
||||||
|
BARE = ConsoleFormat.LEVEL
|
||||||
# only message
|
# only message
|
||||||
BARE = ConsoleFormat(0)
|
NONE = ConsoleFormat(0)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_string(setting_str: str, default: ConsoleFormat | None = None) -> ConsoleFormat | None:
|
def from_string(setting_str: str, default: ConsoleFormat | None = None) -> ConsoleFormat | None:
|
||||||
@@ -445,6 +449,9 @@ class Log(LogParent):
|
|||||||
logger setup
|
logger setup
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
CONSOLE_HANDLER: str = 'stream_handler'
|
||||||
|
FILE_HANDLER: str = 'file_handler'
|
||||||
|
|
||||||
# spacer lenght characters and the character
|
# spacer lenght characters and the character
|
||||||
SPACER_CHAR: str = '='
|
SPACER_CHAR: str = '='
|
||||||
SPACER_LENGTH: int = 32
|
SPACER_LENGTH: int = 32
|
||||||
@@ -510,13 +517,13 @@ class Log(LogParent):
|
|||||||
# in the file writer too, for the ones where color is set BEFORE the format
|
# 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.*
|
# Any is logging.StreamHandler, logging.FileHandler and all logging.handlers.*
|
||||||
self.handlers: dict[str, Any] = {}
|
self.handlers: dict[str, Any] = {}
|
||||||
self.add_handler('file_handler', self.__create_file_handler(
|
self.add_handler(self.FILE_HANDLER, self.__create_file_handler(
|
||||||
'file_handler', self.log_settings['log_level_file'], log_path)
|
self.FILE_HANDLER, self.log_settings['log_level_file'], log_path)
|
||||||
)
|
)
|
||||||
if self.log_settings['console_enabled']:
|
if self.log_settings['console_enabled']:
|
||||||
# console
|
# console
|
||||||
self.add_handler('stream_handler', self.__create_console_handler(
|
self.add_handler(self.CONSOLE_HANDLER, self.__create_console_handler(
|
||||||
'stream_handler',
|
self.CONSOLE_HANDLER,
|
||||||
self.log_settings['log_level_console'],
|
self.log_settings['log_level_console'],
|
||||||
console_format_type=self.log_settings['console_format_type'],
|
console_format_type=self.log_settings['console_format_type'],
|
||||||
))
|
))
|
||||||
@@ -613,19 +620,17 @@ class Log(LogParent):
|
|||||||
self.handlers[handler_name] = handler
|
self.handlers[handler_name] = handler
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# MARK: console handler
|
# MARK: console logger format
|
||||||
def __create_console_handler(
|
def __build_console_format_from_string(self, console_format_type: ConsoleFormat) -> str:
|
||||||
self, handler_name: str,
|
"""
|
||||||
log_level_console: LoggingLevel = LoggingLevel.WARNING,
|
Build console format string from the given console format type
|
||||||
filter_exceptions: bool = True,
|
|
||||||
console_format_type: ConsoleFormat = ConsoleFormatSettings.ALL,
|
Arguments:
|
||||||
) -> logging.StreamHandler[TextIO]:
|
console_format_type {ConsoleFormat} -- _description_
|
||||||
# console logger
|
|
||||||
if not self.validate_log_level(log_level_console):
|
Returns:
|
||||||
log_level_console = self.DEFAULT_LOG_LEVEL_CONSOLE
|
str -- _description_
|
||||||
console_handler = logging.StreamHandler()
|
"""
|
||||||
print(f"Console format type: {console_format_type}")
|
|
||||||
# build the format string based on what flags are set
|
|
||||||
format_string = ''
|
format_string = ''
|
||||||
# time part if any of the times are requested
|
# time part if any of the times are requested
|
||||||
if (
|
if (
|
||||||
@@ -654,17 +659,23 @@ class Log(LogParent):
|
|||||||
set_group.append('%(lineno)d')
|
set_group.append('%(lineno)d')
|
||||||
format_string += ':'.join(set_group)
|
format_string += ':'.join(set_group)
|
||||||
format_string += '] '
|
format_string += '] '
|
||||||
# always level + message
|
# level if wanted
|
||||||
format_string += '<%(levelname)s> %(message)s'
|
if ConsoleFormat.LEVEL in console_format_type:
|
||||||
# basic date, but this will be overridden to ISO in formatTime
|
format_string += '<%(levelname)s> '
|
||||||
# format_date = "%Y-%m-%d %H:%M:%S"
|
# always message
|
||||||
# color or not
|
format_string += '%(message)s'
|
||||||
if self.log_settings['console_color_output_enabled']:
|
return format_string
|
||||||
# formatter_console = CustomConsoleFormatter(format_string, datefmt=format_date)
|
|
||||||
formatter_console = CustomConsoleFormatter(format_string)
|
def __set_time_format_for_console_formatter(
|
||||||
else:
|
self, formatter_console: CustomConsoleFormatter | logging.Formatter, console_format_type: ConsoleFormat
|
||||||
# formatter_console = logging.Formatter(format_string, datefmt=format_date)
|
) -> None:
|
||||||
formatter_console = logging.Formatter(format_string)
|
"""
|
||||||
|
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
|
# default for TIME is milliseconds
|
||||||
# if we have multiple set, the smallest precision wins
|
# if we have multiple set, the smallest precision wins
|
||||||
if ConsoleFormat.TIME_MICROSECONDS in console_format_type:
|
if ConsoleFormat.TIME_MICROSECONDS in console_format_type:
|
||||||
@@ -701,11 +712,80 @@ class Log(LogParent):
|
|||||||
.fromtimestamp(record.created)
|
.fromtimestamp(record.created)
|
||||||
.isoformat(sep=" ", timespec=iso_precision)
|
.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)
|
||||||
|
self.log_settings['console_format_type'] = 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_
|
||||||
|
"""
|
||||||
|
# skip if console not enabled
|
||||||
|
if not self.log_settings['console_enabled']:
|
||||||
|
return
|
||||||
|
# skip if format has not changed
|
||||||
|
if self.log_settings['console_format_type'] == console_format_type:
|
||||||
|
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,
|
||||||
|
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()
|
||||||
|
# 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.set_name(handler_name)
|
||||||
console_handler.setLevel(log_level_console.name)
|
console_handler.setLevel(log_level_console.name)
|
||||||
# do not show exceptions logs on console
|
# do not show exceptions logs on console
|
||||||
console_handler.addFilter(CustomHandlerFilter('console', filter_exceptions))
|
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
|
return console_handler
|
||||||
|
|
||||||
# MARK: file handler
|
# MARK: file handler
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
foo=bar
|
foo=bar
|
||||||
foobar=1
|
foobar=1
|
||||||
bar=st
|
bar=st
|
||||||
|
arg_overload=should_not_be_set_because_of_command_line_is_list
|
||||||
|
arg_overload_list=too,be,long
|
||||||
|
arg_overload_not_set=this should not be set because of override flag
|
||||||
|
just_values=too,be,long
|
||||||
some_match=foo
|
some_match=foo
|
||||||
some_match_list=foo,bar
|
some_match_list=foo,bar
|
||||||
test_list=a,b,c,d f, g h
|
test_list=a,b,c,d f, g h
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ def main():
|
|||||||
|
|
||||||
sl = SettingsLoader(
|
sl = SettingsLoader(
|
||||||
{
|
{
|
||||||
'foo': 'OVERLOAD'
|
'foo': 'OVERLOAD',
|
||||||
|
'arg_overload': ['should', 'not', 'be', 'set'],
|
||||||
|
'arg_overload_list': ['overload', 'this', 'list'],
|
||||||
|
'arg_overload_not_set': "DO_NOT_SET",
|
||||||
},
|
},
|
||||||
ROOT_PATH.joinpath(CONFIG_DIR, CONFIG_FILE),
|
ROOT_PATH.joinpath(CONFIG_DIR, CONFIG_FILE),
|
||||||
log=log
|
log=log
|
||||||
@@ -53,6 +56,8 @@ def main():
|
|||||||
"foo": ["mandatory:yes"],
|
"foo": ["mandatory:yes"],
|
||||||
"foobar": ["check:int"],
|
"foobar": ["check:int"],
|
||||||
"bar": ["mandatory:yes"],
|
"bar": ["mandatory:yes"],
|
||||||
|
"arg_overload_list": ["split:,",],
|
||||||
|
"arg_overload_not_set": ["args:no"],
|
||||||
"some_match": ["matching:foo|bar"],
|
"some_match": ["matching:foo|bar"],
|
||||||
"some_match_list": ["split:,", "matching:foo|bar"],
|
"some_match_list": ["split:,", "matching:foo|bar"],
|
||||||
"test_list": [
|
"test_list": [
|
||||||
|
|||||||
@@ -25,12 +25,13 @@ def main():
|
|||||||
"log_level_file": 'DEBUG',
|
"log_level_file": 'DEBUG',
|
||||||
# "console_color_output_enabled": False,
|
# "console_color_output_enabled": False,
|
||||||
"per_run_log": True,
|
"per_run_log": True,
|
||||||
# Set console log type, must be sent as value for ConsoleFormat or bitwise of ConsoleFormatType
|
# "console_format_type": ConsoleFormatSettings.NONE,
|
||||||
# "console_format_type": ConsoleFormatSettings.BARE,
|
|
||||||
# "console_format_type": ConsoleFormatSettings.MINIMAL,
|
# "console_format_type": ConsoleFormatSettings.MINIMAL,
|
||||||
# "console_format_type": ConsoleFormatType.TIME_MICROSECONDS | ConsoleFormatType.NAME,
|
"console_format_type": ConsoleFormat.TIME_MICROSECONDS | ConsoleFormat.NAME | ConsoleFormat.LEVEL,
|
||||||
# "console_format_type": ConsoleFormatType.NAME,
|
# "console_format_type": ConsoleFormat.NAME,
|
||||||
"console_format_type": ConsoleFormat.TIME | ConsoleFormat.TIMEZONE | ConsoleFormat.LINENO,
|
# "console_format_type": (
|
||||||
|
# ConsoleFormat.TIME | ConsoleFormat.TIMEZONE | ConsoleFormat.LINENO | ConsoleFormat.LEVEL
|
||||||
|
# ),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
logn = Logger(log.get_logger_settings())
|
logn = Logger(log.get_logger_settings())
|
||||||
@@ -104,10 +105,16 @@ def main():
|
|||||||
|
|
||||||
for key, handler in log.handlers.items():
|
for key, handler in log.handlers.items():
|
||||||
print(f"Handler (handlers) [{key}] {handler} -> {handler.level} -> {LoggingLevel.from_any(handler.level)}")
|
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.warning('[NORMAL] Invisible Warning test: %s', log.logger.name)
|
||||||
log.logger.error('[NORMAL] Visible Error 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')
|
||||||
|
log.update_console_formatter(ConsoleFormat.TIME | ConsoleFormat.LINENO)
|
||||||
|
log.info('Does hit show less B')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -528,6 +528,123 @@ class TestLoadSettings:
|
|||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert "Command line option override" in captured.out
|
assert "Command line option override" in captured.out
|
||||||
|
|
||||||
|
def test_load_settings_args_no_flag(self, tmp_path: Path, capsys: CaptureFixture[str]):
|
||||||
|
"""Test args:no flag with list argument that has split"""
|
||||||
|
config_file = tmp_path / "test.ini"
|
||||||
|
config_file.write_text("[TestSection]\nvalue=a,b,c\n")
|
||||||
|
|
||||||
|
loader = SettingsLoader(
|
||||||
|
args={"value": ["x", "y", "z"]},
|
||||||
|
config_file=config_file
|
||||||
|
)
|
||||||
|
result = loader.load_settings(
|
||||||
|
"TestSection",
|
||||||
|
{"value": ["args:no", "split:,"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
# With args:no and split defined for list args, should use config value
|
||||||
|
assert result["value"] == ["a", "b", "c"]
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
# Message is printed but args:no prevents the override
|
||||||
|
assert "Command line option override" in captured.out
|
||||||
|
|
||||||
|
def test_load_settings_args_list_no_split(self, tmp_path: Path, capsys: CaptureFixture[str]):
|
||||||
|
"""Test that list arguments without split entry are skipped"""
|
||||||
|
config_file = tmp_path / "test.ini"
|
||||||
|
config_file.write_text("[TestSection]\nvalue=config_value\n")
|
||||||
|
|
||||||
|
loader = SettingsLoader(
|
||||||
|
args={"value": ["arg1", "arg2", "arg3"]},
|
||||||
|
config_file=config_file
|
||||||
|
)
|
||||||
|
result = loader.load_settings(
|
||||||
|
"TestSection",
|
||||||
|
{"value": []}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should keep config value since args is list but no split defined
|
||||||
|
assert result["value"] == "config_value"
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
# Message is printed but list without split prevents the override
|
||||||
|
assert "Command line option override" in captured.out
|
||||||
|
|
||||||
|
def test_load_settings_args_list_with_split(self, tmp_path: Path, capsys: CaptureFixture[str]):
|
||||||
|
"""Test that list arguments with split entry are applied"""
|
||||||
|
config_file = tmp_path / "test.ini"
|
||||||
|
config_file.write_text("[TestSection]\nvalue=a,b,c\n")
|
||||||
|
|
||||||
|
loader = SettingsLoader(
|
||||||
|
args={"value": ["arg1", "arg2", "arg3"]},
|
||||||
|
config_file=config_file
|
||||||
|
)
|
||||||
|
result = loader.load_settings(
|
||||||
|
"TestSection",
|
||||||
|
{"value": ["split:,"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should use args value since split is defined
|
||||||
|
assert result["value"] == ["arg1", "arg2", "arg3"]
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "Command line option override" in captured.out
|
||||||
|
|
||||||
|
def test_load_settings_args_no_with_mandatory(self, tmp_path: Path, capsys: CaptureFixture[str]):
|
||||||
|
"""Test args:no with mandatory field and list args with split"""
|
||||||
|
config_file = tmp_path / "test.ini"
|
||||||
|
config_file.write_text("[TestSection]\nvalue=config1,config2\n")
|
||||||
|
|
||||||
|
loader = SettingsLoader(
|
||||||
|
args={"value": ["arg1", "arg2"]},
|
||||||
|
config_file=config_file
|
||||||
|
)
|
||||||
|
result = loader.load_settings(
|
||||||
|
"TestSection",
|
||||||
|
{"value": ["mandatory:yes", "args:no", "split:,"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should use config value because of args:no with list args and split
|
||||||
|
assert result["value"] == ["config1", "config2"]
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
# Message is printed but args:no prevents the override
|
||||||
|
assert "Command line option override" in captured.out
|
||||||
|
|
||||||
|
def test_load_settings_args_no_with_mandatory_valid(self, tmp_path: Path, capsys: CaptureFixture[str]):
|
||||||
|
"""Test args:no with string args still allows override (current behavior)"""
|
||||||
|
config_file = tmp_path / "test.ini"
|
||||||
|
config_file.write_text("[TestSection]\nvalue=config_value\n")
|
||||||
|
|
||||||
|
loader = SettingsLoader(
|
||||||
|
args={"value": "arg_value"},
|
||||||
|
config_file=config_file
|
||||||
|
)
|
||||||
|
result = loader.load_settings(
|
||||||
|
"TestSection",
|
||||||
|
{"value": ["mandatory:yes", "args:no"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Current behavior: string args without split still override even with args:no
|
||||||
|
assert result["value"] == "arg_value"
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "Command line option override" in captured.out
|
||||||
|
|
||||||
|
def test_load_settings_args_string_no_split(self, tmp_path: Path, capsys: CaptureFixture[str]):
|
||||||
|
"""Test that string arguments without split entry work normally"""
|
||||||
|
config_file = tmp_path / "test.ini"
|
||||||
|
config_file.write_text("[TestSection]\nvalue=config_value\n")
|
||||||
|
|
||||||
|
loader = SettingsLoader(
|
||||||
|
args={"value": "arg_value"},
|
||||||
|
config_file=config_file
|
||||||
|
)
|
||||||
|
result = loader.load_settings(
|
||||||
|
"TestSection",
|
||||||
|
{"value": []}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should use args value for non-list args
|
||||||
|
assert result["value"] == "arg_value"
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "Command line option override" in captured.out
|
||||||
|
|
||||||
def test_load_settings_no_config_file_with_args(self, tmp_path: Path):
|
def test_load_settings_no_config_file_with_args(self, tmp_path: Path):
|
||||||
"""Test loading settings without config file but with mandatory args"""
|
"""Test loading settings without config file but with mandatory args"""
|
||||||
config_file = tmp_path / "missing.ini"
|
config_file = tmp_path / "missing.ini"
|
||||||
@@ -704,5 +821,48 @@ class TestComplexScenarios:
|
|||||||
assert result["emails"] == "test@example.com"
|
assert result["emails"] == "test@example.com"
|
||||||
assert result["date"] == "2025-01-15"
|
assert result["date"] == "2025-01-15"
|
||||||
|
|
||||||
|
def test_args_no_and_list_skip_combination(self, tmp_path: Path, capsys: CaptureFixture[str]):
|
||||||
|
"""Test combination of args:no flag and list argument skip behavior"""
|
||||||
|
config_file = tmp_path / "test.ini"
|
||||||
|
config_file.write_text(
|
||||||
|
"[Settings]\n"
|
||||||
|
"no_override=a,b,c\n"
|
||||||
|
"list_no_split=config_list\n"
|
||||||
|
"list_with_split=x,y,z\n"
|
||||||
|
"normal=config_normal\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
loader = SettingsLoader(
|
||||||
|
args={
|
||||||
|
"no_override": ["arg1", "arg2"],
|
||||||
|
"list_no_split": ["arg1", "arg2"],
|
||||||
|
"list_with_split": ["p", "q", "r"],
|
||||||
|
"normal": "arg_normal"
|
||||||
|
},
|
||||||
|
config_file=config_file
|
||||||
|
)
|
||||||
|
result = loader.load_settings(
|
||||||
|
"Settings",
|
||||||
|
{
|
||||||
|
"no_override": ["args:no", "split:,"],
|
||||||
|
"list_no_split": [],
|
||||||
|
"list_with_split": ["split:,"],
|
||||||
|
"normal": []
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should use config value due to args:no with list args and split
|
||||||
|
assert result["no_override"] == ["a", "b", "c"]
|
||||||
|
# Should use config value because args is list without split
|
||||||
|
assert result["list_no_split"] == "config_list"
|
||||||
|
# Should use args value because split is defined and no args:no
|
||||||
|
assert result["list_with_split"] == ["p", "q", "r"]
|
||||||
|
# Should use args value normally (string arg without split)
|
||||||
|
assert result["normal"] == "arg_normal"
|
||||||
|
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
# Should see override messages (even though list_no_split prints, it doesn't apply)
|
||||||
|
assert "Command line option override" in captured.out
|
||||||
|
|
||||||
|
|
||||||
# __END__
|
# __END__
|
||||||
|
|||||||
@@ -153,6 +153,19 @@ class TestLogSettingsParsing:
|
|||||||
|
|
||||||
assert log.log_settings["console_format_type"] == ConsoleFormatSettings.BARE
|
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):
|
def test_parse_console_format_type_invalid(self, tmp_log_path: Path):
|
||||||
"""Test parsing with invalid console_format_type raises TypeError"""
|
"""Test parsing with invalid console_format_type raises TypeError"""
|
||||||
settings: dict[str, Any] = {
|
settings: dict[str, Any] = {
|
||||||
@@ -207,6 +220,11 @@ class TestConsoleFormatSettingsFromString:
|
|||||||
result = ConsoleFormatSettings.from_string('BARE')
|
result = ConsoleFormatSettings.from_string('BARE')
|
||||||
assert result == ConsoleFormatSettings.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):
|
def test_from_string_invalid_returns_none(self):
|
||||||
"""Test from_string with invalid string returns None"""
|
"""Test from_string with invalid string returns None"""
|
||||||
result = ConsoleFormatSettings.from_string('INVALID')
|
result = ConsoleFormatSettings.from_string('INVALID')
|
||||||
@@ -234,6 +252,7 @@ class TestConsoleFormatSettingsFromString:
|
|||||||
("CONDENSED", ConsoleFormatSettings.CONDENSED),
|
("CONDENSED", ConsoleFormatSettings.CONDENSED),
|
||||||
("MINIMAL", ConsoleFormatSettings.MINIMAL),
|
("MINIMAL", ConsoleFormatSettings.MINIMAL),
|
||||||
("BARE", ConsoleFormatSettings.BARE),
|
("BARE", ConsoleFormatSettings.BARE),
|
||||||
|
("NONE", ConsoleFormatSettings.NONE),
|
||||||
])
|
])
|
||||||
def test_from_string_all_valid_settings(self, setting_name: str, expected: Any):
|
def test_from_string_all_valid_settings(self, setting_name: str, expected: Any):
|
||||||
"""Test from_string with all valid setting names"""
|
"""Test from_string with all valid setting names"""
|
||||||
|
|||||||
@@ -140,4 +140,172 @@ class TestCustomConsoleFormatter:
|
|||||||
assert "Critical message" in result
|
assert "Critical message" in result
|
||||||
assert "CRITICAL" 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__
|
# __END__
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from corelibs.logging_handling.log import (
|
|||||||
LogParent,
|
LogParent,
|
||||||
LogSettings,
|
LogSettings,
|
||||||
ConsoleFormatSettings,
|
ConsoleFormatSettings,
|
||||||
|
ConsoleFormat,
|
||||||
)
|
)
|
||||||
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
|
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
|
||||||
|
|
||||||
@@ -108,4 +109,101 @@ class TestHandlerManagement:
|
|||||||
result2 = log.add_handler("test", handler2)
|
result2 = log.add_handler("test", handler2)
|
||||||
assert result2 is False
|
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__
|
# __END__
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@@ -108,7 +108,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "corelibs"
|
name = "corelibs"
|
||||||
version = "0.37.0"
|
version = "0.40.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "corelibs-datetime" },
|
{ name = "corelibs-datetime" },
|
||||||
|
|||||||
Reference in New Issue
Block a user