Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6090995eba | ||
|
|
60db747d6d | ||
|
|
a7a4141f58 | ||
|
|
2b04cbe239 | ||
|
|
765cc061c1 | ||
|
|
80319385f0 | ||
|
|
29dd906fe0 | ||
|
|
d5dc4028c3 | ||
|
|
0df049d453 | ||
|
|
0bd7c1f685 | ||
|
|
2f08ecabbf | ||
|
|
12af1c80dc | ||
|
|
a52b6e0a55 | ||
|
|
a586cf65e2 |
2
ToDo.md
2
ToDo.md
@@ -3,3 +3,5 @@
|
|||||||
- [x] stub files .pyi
|
- [x] stub files .pyi
|
||||||
- [ ] Add tests for all, we need 100% test coverate
|
- [ ] Add tests for all, we need 100% test coverate
|
||||||
- [x] Log: add custom format for "stack_correct" if set, this will override the normal stack block
|
- [x] Log: add custom format for "stack_correct" if set, this will override the normal stack block
|
||||||
|
- [ ] Log: add rotate for size based
|
||||||
|
- [ ] All folders and file names need to be revisited for naming and content collection
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# MARK: Project info
|
# MARK: Project info
|
||||||
[project]
|
[project]
|
||||||
name = "corelibs"
|
name = "corelibs"
|
||||||
version = "0.20.0"
|
version = "0.22.3"
|
||||||
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"
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ attach "init_worker_logging" with the set log_queue
|
|||||||
import re
|
import re
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import atexit
|
||||||
from typing import MutableMapping, TextIO, TypedDict, Any, TYPE_CHECKING, cast
|
from typing import MutableMapping, TextIO, TypedDict, Any, TYPE_CHECKING, cast
|
||||||
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
|
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
|
||||||
from corelibs.string_handling.text_colors import Colors
|
from corelibs.string_handling.text_colors import Colors
|
||||||
@@ -23,6 +25,7 @@ class LogSettings(TypedDict):
|
|||||||
"""log settings, for Log setup"""
|
"""log settings, for Log setup"""
|
||||||
log_level_console: LoggingLevel
|
log_level_console: LoggingLevel
|
||||||
log_level_file: LoggingLevel
|
log_level_file: LoggingLevel
|
||||||
|
per_run_log: bool
|
||||||
console_enabled: bool
|
console_enabled: bool
|
||||||
console_color_output_enabled: bool
|
console_color_output_enabled: bool
|
||||||
add_start_info: bool
|
add_start_info: bool
|
||||||
@@ -280,6 +283,15 @@ class LogParent:
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _cleanup(self):
|
||||||
|
"""cleanup for any open queues in case we have an abort"""
|
||||||
|
if not self.log_queue:
|
||||||
|
return
|
||||||
|
self.flush()
|
||||||
|
# Close the queue properly
|
||||||
|
self.log_queue.close()
|
||||||
|
self.log_queue.join_thread()
|
||||||
|
|
||||||
# MARK: log level handling
|
# MARK: log level handling
|
||||||
def set_log_level(self, handler_name: str, log_level: LoggingLevel) -> bool:
|
def set_log_level(self, handler_name: str, log_level: LoggingLevel) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -392,6 +404,7 @@ class Log(LogParent):
|
|||||||
DEFAULT_LOG_SETTINGS: LogSettings = {
|
DEFAULT_LOG_SETTINGS: LogSettings = {
|
||||||
"log_level_console": DEFAULT_LOG_LEVEL_CONSOLE,
|
"log_level_console": DEFAULT_LOG_LEVEL_CONSOLE,
|
||||||
"log_level_file": DEFAULT_LOG_LEVEL_FILE,
|
"log_level_file": DEFAULT_LOG_LEVEL_FILE,
|
||||||
|
"per_run_log": False,
|
||||||
"console_enabled": True,
|
"console_enabled": True,
|
||||||
"console_color_output_enabled": True,
|
"console_color_output_enabled": True,
|
||||||
"add_start_info": True,
|
"add_start_info": True,
|
||||||
@@ -440,7 +453,7 @@ 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_timed_rotating_file_handler(
|
self.add_handler('file_handler', self.__create_file_handler(
|
||||||
'file_handler', self.log_settings['log_level_file'], log_path)
|
'file_handler', self.log_settings['log_level_file'], log_path)
|
||||||
)
|
)
|
||||||
if self.log_settings['console_enabled']:
|
if self.log_settings['console_enabled']:
|
||||||
@@ -492,6 +505,7 @@ class Log(LogParent):
|
|||||||
default_log_settings[__log_entry] = LoggingLevel.from_any(__log_level)
|
default_log_settings[__log_entry] = LoggingLevel.from_any(__log_level)
|
||||||
# check bool
|
# check bool
|
||||||
for __log_entry in [
|
for __log_entry in [
|
||||||
|
"per_run_log",
|
||||||
"console_enabled",
|
"console_enabled",
|
||||||
"console_color_output_enabled",
|
"console_color_output_enabled",
|
||||||
"add_start_info",
|
"add_start_info",
|
||||||
@@ -566,24 +580,35 @@ class Log(LogParent):
|
|||||||
return console_handler
|
return console_handler
|
||||||
|
|
||||||
# MARK: file handler
|
# MARK: file handler
|
||||||
def __create_timed_rotating_file_handler(
|
def __create_file_handler(
|
||||||
self, handler_name: str,
|
self, handler_name: str,
|
||||||
log_level_file: LoggingLevel, log_path: Path,
|
log_level_file: LoggingLevel, log_path: Path,
|
||||||
|
# for TimedRotating, if per_run_log is off
|
||||||
when: str = "D", interval: int = 1, backup_count: int = 0
|
when: str = "D", interval: int = 1, backup_count: int = 0
|
||||||
) -> logging.handlers.TimedRotatingFileHandler:
|
) -> logging.handlers.TimedRotatingFileHandler | logging.FileHandler:
|
||||||
# file logger
|
# file logger
|
||||||
# when: S/M/H/D/W0-W6/midnight
|
# when: S/M/H/D/W0-W6/midnight
|
||||||
# interval: how many, 1D = every day
|
# interval: how many, 1D = every day
|
||||||
# backup_count: how many old to keep, 0 = all
|
# backup_count: how many old to keep, 0 = all
|
||||||
if not self.validate_log_level(log_level_file):
|
if not self.validate_log_level(log_level_file):
|
||||||
log_level_file = self.DEFAULT_LOG_LEVEL_FILE
|
log_level_file = self.DEFAULT_LOG_LEVEL_FILE
|
||||||
file_handler = logging.handlers.TimedRotatingFileHandler(
|
if self.log_settings['per_run_log']:
|
||||||
filename=log_path,
|
# log path, remove them stem (".log"), then add the datetime and add .log again
|
||||||
encoding="utf-8",
|
now = datetime.now()
|
||||||
when=when,
|
# we add microseconds part to get milli seconds
|
||||||
interval=interval,
|
new_stem=f"{log_path.stem}.{now.strftime('%Y-%m-%d_%H-%M-%S')}.{str(now.microsecond)[:3]}"
|
||||||
backupCount=backup_count
|
file_handler = logging.FileHandler(
|
||||||
)
|
filename=log_path.with_name(f"{new_stem}{log_path.suffix}"),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
file_handler = logging.handlers.TimedRotatingFileHandler(
|
||||||
|
filename=log_path,
|
||||||
|
encoding="utf-8",
|
||||||
|
when=when,
|
||||||
|
interval=interval,
|
||||||
|
backupCount=backup_count
|
||||||
|
)
|
||||||
formatter_file_handler = logging.Formatter(
|
formatter_file_handler = logging.Formatter(
|
||||||
(
|
(
|
||||||
# time stamp
|
# time stamp
|
||||||
@@ -619,6 +644,7 @@ class Log(LogParent):
|
|||||||
if log_queue is None:
|
if log_queue is None:
|
||||||
return
|
return
|
||||||
self.log_queue = log_queue
|
self.log_queue = log_queue
|
||||||
|
atexit.register(self.stop_listener)
|
||||||
self.listener = logging.handlers.QueueListener(
|
self.listener = logging.handlers.QueueListener(
|
||||||
self.log_queue,
|
self.log_queue,
|
||||||
*self.handlers.values(),
|
*self.handlers.values(),
|
||||||
@@ -633,6 +659,7 @@ class Log(LogParent):
|
|||||||
if self.listener is not None:
|
if self.listener is not None:
|
||||||
self.flush()
|
self.flush()
|
||||||
self.listener.stop()
|
self.listener.stop()
|
||||||
|
self._cleanup()
|
||||||
|
|
||||||
# MARK: init main log
|
# MARK: init main log
|
||||||
def __init_log(self, log_name: str) -> None:
|
def __init_log(self, log_name: str) -> None:
|
||||||
|
|||||||
@@ -2,8 +2,18 @@
|
|||||||
Current timestamp strings and time zones
|
Current timestamp strings and time zones
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||||
|
from corelibs.var_handling.var_helpers import is_float
|
||||||
|
|
||||||
|
|
||||||
|
class TimeParseError(Exception):
|
||||||
|
"""Custom exception for time parsing errors."""
|
||||||
|
|
||||||
|
|
||||||
|
class TimeUnitError(Exception):
|
||||||
|
"""Custom exception for time parsing errors."""
|
||||||
|
|
||||||
|
|
||||||
class TimestampStrings:
|
class TimestampStrings:
|
||||||
@@ -24,3 +34,79 @@ class TimestampStrings:
|
|||||||
self.timestamp = self.timestamp_now.strftime("%Y-%m-%d %H:%M:%S")
|
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_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")
|
self.timestamp_file = self.timestamp_now.strftime("%Y-%m-%d_%H%M%S")
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_seconds(time_string: str | int | float) -> int:
|
||||||
|
"""
|
||||||
|
Conver a string with time units into a seconds string
|
||||||
|
The following units are allowed
|
||||||
|
Y: 365 days
|
||||||
|
M: 30 days
|
||||||
|
d, h, m, s
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
time_string {str} -- _description_
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: _description_
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int -- _description_
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ def main():
|
|||||||
# "log_level_console": None,
|
# "log_level_console": None,
|
||||||
"log_level_file": 'DEBUG',
|
"log_level_file": 'DEBUG',
|
||||||
# "console_color_output_enabled": False,
|
# "console_color_output_enabled": False,
|
||||||
|
"per_run_log": True
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
logn = Logger(log.get_logger_settings())
|
logn = Logger(log.get_logger_settings())
|
||||||
|
|||||||
54
test-run/string_handling/timestamp_strings.py
Normal file
54
test-run/string_handling/timestamp_strings.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
timestamp string checks
|
||||||
|
"""
|
||||||
|
|
||||||
|
from corelibs.string_handling.timestamp_strings import convert_to_seconds, TimeParseError, TimeUnitError
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""
|
||||||
|
Comment
|
||||||
|
"""
|
||||||
|
test_cases = [
|
||||||
|
"5M 6d", # 5 months, 6 days
|
||||||
|
"2h 30m 45s", # 2 hours, 30 minutes, 45 seconds
|
||||||
|
"1Y 2M 3d", # 1 year, 2 months, 3 days
|
||||||
|
"1h", # 1 hour
|
||||||
|
"30m", # 30 minutes
|
||||||
|
"2 hours 15 minutes", # 2 hours, 15 minutes
|
||||||
|
"1d 12h", # 1 day, 12 hours
|
||||||
|
"3M 2d 4h", # 3 months, 2 days, 4 hours
|
||||||
|
"45s", # 45 seconds
|
||||||
|
"1 year 2 months", # 1 year, 2 months
|
||||||
|
"2Y 6M 15d 8h 30m 45s", # Complex example
|
||||||
|
# ]
|
||||||
|
# invalid_test_cases = [
|
||||||
|
"5M 6d 2M", # months appears twice
|
||||||
|
"2h 30m 45s 1h", # hours appears twice
|
||||||
|
"1d 2 days", # days appears twice (short and long form)
|
||||||
|
"30m 45 minutes", # minutes appears twice
|
||||||
|
"1Y 2 years", # years appears twice
|
||||||
|
"1x 2 yrs", # invalid names
|
||||||
|
|
||||||
|
123, # int
|
||||||
|
789.12, # float
|
||||||
|
456.56, # float, high
|
||||||
|
"4566", # int as string
|
||||||
|
"5551.12", # float as string
|
||||||
|
"5551.56", # float, high as string
|
||||||
|
]
|
||||||
|
|
||||||
|
for time_string in test_cases:
|
||||||
|
try:
|
||||||
|
result = convert_to_seconds(time_string)
|
||||||
|
print(f"{time_string} => {result}")
|
||||||
|
except (TimeParseError, TimeUnitError) as e:
|
||||||
|
print(f"Error encountered for {time_string}: {type(e).__name__}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
# __END__
|
||||||
4
uv.lock
generated
4
uv.lock
generated
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 2
|
revision = 3
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -44,7 +44,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "corelibs"
|
name = "corelibs"
|
||||||
version = "0.19.1"
|
version = "0.22.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "jmespath" },
|
{ name = "jmespath" },
|
||||||
|
|||||||
Reference in New Issue
Block a user