pyutilities 2.4.0.dev1__tar.gz → 2.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/PKG-INFO +30 -8
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/docs/README-PUB.md +17 -7
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/pyproject.toml +30 -6
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/cli/pyutils.py +1 -1
- pyutilities-2.4.2/src/pyutilities/log/log_manager.py +325 -0
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/sys/sys_utils.py +12 -6
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/utils/common_utils.py +1 -56
- pyutilities-2.4.2/src/pyutilities/utils/datetime_utils.py +78 -0
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/utils/decorators.py +77 -2
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/utils/lambdas.py +17 -15
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/utils/string_utils.py +103 -30
- pyutilities-2.4.2/src/pyutilities/web/__init__.py +0 -0
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/web/http_client.py +2 -2
- pyutilities-2.4.0.dev1/src/pyutilities/utils/datetime_utils.py +0 -33
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/LICENSE +0 -0
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/__init__.py +0 -0
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/cli/__init__.py +0 -0
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/cli/fedit.py +0 -0
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/config/__init__.py +0 -0
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/config/configuration.py +0 -0
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/defaults.py +0 -0
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/exception.py +0 -0
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/io/__init__.py +0 -0
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/io/io_utils.py +0 -0
- {pyutilities-2.4.0.dev1/src/pyutilities/sys → pyutilities-2.4.2/src/pyutilities/log}/__init__.py +0 -0
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/logging.py +0 -0
- {pyutilities-2.4.0.dev1/src/pyutilities/web → pyutilities-2.4.2/src/pyutilities/sys}/__init__.py +0 -0
- {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/utils/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyutilities
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.2
|
|
4
4
|
Summary: PyUtilities :: Useful python 3+ utilities library for various use cases.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -26,11 +26,23 @@ Requires-Dist: click (>=8.3,<9.0)
|
|
|
26
26
|
Requires-Dist: fake-useragent (>=2.0,<3.0)
|
|
27
27
|
Requires-Dist: loguru (==0.7.3)
|
|
28
28
|
Requires-Dist: markdown (>=3.7,<4.0)
|
|
29
|
+
Requires-Dist: marshmallow (>=4.1,<5.0)
|
|
29
30
|
Requires-Dist: openpyxl (==3.1.5)
|
|
30
31
|
Requires-Dist: paramiko (==4.*)
|
|
32
|
+
Requires-Dist: pendulum (>=3.2,<4.0)
|
|
31
33
|
Requires-Dist: prettytable (>=3.14,<4.0)
|
|
34
|
+
Requires-Dist: prettytable (>=3.17,<4.0)
|
|
35
|
+
Requires-Dist: pydantic (>=2.12.5,<3.0.0)
|
|
36
|
+
Requires-Dist: pydantic-settings (==2.*)
|
|
37
|
+
Requires-Dist: pydantic-yaml (==1.*)
|
|
38
|
+
Requires-Dist: pyfiglet (>=1.0,<2.0)
|
|
39
|
+
Requires-Dist: python-dateutil (>=2.9,<3.0)
|
|
40
|
+
Requires-Dist: python-dotenv (>=1.2,<2.0)
|
|
41
|
+
Requires-Dist: pytz (==2026.*)
|
|
32
42
|
Requires-Dist: pyyaml (==6.*)
|
|
33
43
|
Requires-Dist: requests (>=2.32,<3.0)
|
|
44
|
+
Requires-Dist: rich-pyfiglet (>=1.0,<2.0)
|
|
45
|
+
Requires-Dist: ruamel.yaml (==0.*)
|
|
34
46
|
Requires-Dist: urllib3 (>=2.6,<3.0)
|
|
35
47
|
Requires-Dist: xlrd (>=2.0.1,<2.1.0)
|
|
36
48
|
Requires-Dist: xlwings (>=0.35,<1.0)
|
|
@@ -43,8 +55,8 @@ Description-Content-Type: text/markdown
|
|
|
43
55
|
|
|
44
56
|
# Python Utilities Library
|
|
45
57
|
|
|
46
|
-
Version 2.2
|
|
47
|
-
*Last update:
|
|
58
|
+
Version 2.4.2, 2018 - 2026, Dmitrii Gusev
|
|
59
|
+
*Last update: 15.05.2026*
|
|
48
60
|
|
|
49
61
|
## Project Description
|
|
50
62
|
|
|
@@ -52,18 +64,28 @@ Useful utilities functions for various needs for the python 3.10+. Containing 'g
|
|
|
52
64
|
|
|
53
65
|
## Versions history
|
|
54
66
|
|
|
55
|
-
### 2.2.
|
|
67
|
+
### 2.4.0/2.4.1/2.4.2
|
|
68
|
+
|
|
69
|
+
- many refactorings and minor code improvements
|
|
70
|
+
- overall tests coverage now > 50%, working on improvements :)
|
|
71
|
+
- added new date/time function, several decorators
|
|
72
|
+
- added several useful string functions
|
|
73
|
+
- added LoggingManager component for loguru simple configuration
|
|
74
|
+
- v.2.4.1/v.2.4.2 - minor docs fixes
|
|
75
|
+
|
|
76
|
+
### 2.3.0
|
|
56
77
|
|
|
57
|
-
|
|
78
|
+
Intermediate version with improvements/refactorings.
|
|
79
|
+
|
|
80
|
+
### 2.2.0
|
|
58
81
|
|
|
59
|
-
-
|
|
60
|
-
- func2()
|
|
82
|
+
Added several new functions - see sources.
|
|
61
83
|
|
|
62
84
|
### 2.1.2
|
|
63
85
|
|
|
64
86
|
Major release with complete set of useful 'general purpose' functions. Documentation 'is coming'...
|
|
65
87
|
|
|
66
|
-
**Warning! Versions 2.1.0, 2.1.1 are intended only for the python version = 3.10 and
|
|
88
|
+
**Warning! Versions 2.1.0, 2.1.1 are intended only for the python version = 3.10 and can be installed only for it!**
|
|
67
89
|
|
|
68
90
|
### Versions before 2.x.x
|
|
69
91
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Python Utilities Library
|
|
2
2
|
|
|
3
|
-
Version 2.2
|
|
4
|
-
*Last update:
|
|
3
|
+
Version 2.4.2, 2018 - 2026, Dmitrii Gusev
|
|
4
|
+
*Last update: 15.05.2026*
|
|
5
5
|
|
|
6
6
|
## Project Description
|
|
7
7
|
|
|
@@ -9,18 +9,28 @@ Useful utilities functions for various needs for the python 3.10+. Containing 'g
|
|
|
9
9
|
|
|
10
10
|
## Versions history
|
|
11
11
|
|
|
12
|
-
### 2.2.
|
|
12
|
+
### 2.4.0/2.4.1/2.4.2
|
|
13
|
+
|
|
14
|
+
- many refactorings and minor code improvements
|
|
15
|
+
- overall tests coverage now > 50%, working on improvements :)
|
|
16
|
+
- added new date/time function, several decorators
|
|
17
|
+
- added several useful string functions
|
|
18
|
+
- added LoggingManager component for loguru simple configuration
|
|
19
|
+
- v.2.4.1/v.2.4.2 - minor docs fixes
|
|
20
|
+
|
|
21
|
+
### 2.3.0
|
|
13
22
|
|
|
14
|
-
|
|
23
|
+
Intermediate version with improvements/refactorings.
|
|
24
|
+
|
|
25
|
+
### 2.2.0
|
|
15
26
|
|
|
16
|
-
-
|
|
17
|
-
- func2()
|
|
27
|
+
Added several new functions - see sources.
|
|
18
28
|
|
|
19
29
|
### 2.1.2
|
|
20
30
|
|
|
21
31
|
Major release with complete set of useful 'general purpose' functions. Documentation 'is coming'...
|
|
22
32
|
|
|
23
|
-
**Warning! Versions 2.1.0, 2.1.1 are intended only for the python version = 3.10 and
|
|
33
|
+
**Warning! Versions 2.1.0, 2.1.1 are intended only for the python version = 3.10 and can be installed only for it!**
|
|
24
34
|
|
|
25
35
|
### Versions before 2.x.x
|
|
26
36
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# - most of modules should be typed (used mypy stubs libraries)
|
|
9
9
|
#
|
|
10
10
|
# Created: Dmitrii Gusev, 30.05.2025
|
|
11
|
-
# Modified: Dmitrii Gusev,
|
|
11
|
+
# Modified: Dmitrii Gusev, 15.05.2026
|
|
12
12
|
#
|
|
13
13
|
####################################################################################################
|
|
14
14
|
|
|
@@ -20,7 +20,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
20
20
|
# -- project / poetry utility meta-information
|
|
21
21
|
[project]
|
|
22
22
|
name = "pyutilities"
|
|
23
|
-
version = "2.4.
|
|
23
|
+
version = "2.4.2"
|
|
24
24
|
description = "PyUtilities :: Useful python 3+ utilities library for various use cases."
|
|
25
25
|
authors = [
|
|
26
26
|
{name = "Dmitrii Gusev", email = "dmitry.ed.gusev@gmail.com"},
|
|
@@ -52,19 +52,42 @@ classifiers = [
|
|
|
52
52
|
# - project runtime dependencies
|
|
53
53
|
dependencies = [
|
|
54
54
|
|
|
55
|
+
# -- loguru
|
|
56
|
+
"loguru==0.7.3",
|
|
57
|
+
|
|
58
|
+
# -- ms word / ms excel libraries
|
|
55
59
|
"xlrd~=2.0.1", # - allowed: >=2.0.1 <2.1.0
|
|
56
60
|
"xlwt==1.3.0",
|
|
57
61
|
"xlwings~=0.35",
|
|
58
|
-
"loguru==0.7.3",
|
|
59
|
-
"pyyaml==6.*",
|
|
60
62
|
"openpyxl==3.1.5",
|
|
63
|
+
|
|
64
|
+
# -- data validation and marshalling (JSON/YAML) --
|
|
65
|
+
"pydantic (>=2.12.5,<3.0.0)",
|
|
66
|
+
"pydantic-yaml ==1.*",
|
|
67
|
+
"pydantic-settings ==2.*",
|
|
68
|
+
"marshmallow ~=4.1",
|
|
69
|
+
"pyyaml ==6.*",
|
|
70
|
+
"ruamel.yaml ==0.*",
|
|
71
|
+
|
|
72
|
+
# -- technical libraries
|
|
61
73
|
"urllib3~=2.6", # - allowed: >=2.3.0 <3.0.0
|
|
62
74
|
"paramiko==4.*",
|
|
63
75
|
"requests~=2.32", # - allowed: >=2.32.0 <3.0.0
|
|
64
76
|
"markdown~=3.7", # - allowed: >=3.7.0 <4.0.0
|
|
65
77
|
"prettytable~=3.14", # - allowed: >=3.14.0 <4.0.0
|
|
66
78
|
"fake-useragent~=2.0", # - allowed: >=2.0.0 <3.0.0
|
|
67
|
-
"click~=8.3"
|
|
79
|
+
"click~=8.3",
|
|
80
|
+
"python-dotenv ~=1.2", # - loading various .env configs
|
|
81
|
+
|
|
82
|
+
# -- terminal pseudo-graphics / pretty output
|
|
83
|
+
"prettytable ~=3.17", # - terminal tables
|
|
84
|
+
"pyfiglet ~=1.0", # - ASCII graphics
|
|
85
|
+
"rich-pyfiglet ~=1.0", # - ASCII graphics addon (colors)
|
|
86
|
+
|
|
87
|
+
# -- datetime utilities
|
|
88
|
+
"pendulum ~=3.2",
|
|
89
|
+
"python-dateutil ~=2.9",
|
|
90
|
+
"pytz ==2026.*"
|
|
68
91
|
|
|
69
92
|
]
|
|
70
93
|
|
|
@@ -241,7 +264,8 @@ follow_untyped_imports = true
|
|
|
241
264
|
[tool.pytest.ini_options]
|
|
242
265
|
minversion = "6.0"
|
|
243
266
|
addopts = """
|
|
244
|
-
--cov=./src/pyutilities --verbose --cov-report=html --cov-report=xml --cov-report=json --cov-report=annotate --capture=sys -rx --durations=0 --full-trace --color=auto
|
|
267
|
+
--cov=./src/pyutilities --verbose --cov-report=html --cov-report=xml --cov-report=json --cov-report=annotate --capture=sys -rx --durations=0 --full-trace --color=auto --hypothesis-show-statistics --hypothesis-verbosity=verbose
|
|
268
|
+
|
|
245
269
|
"""
|
|
246
270
|
testpaths = [
|
|
247
271
|
"tests/",
|
|
@@ -18,7 +18,7 @@ MSG_VERSION = f"\nPy Utilities, version: {__version__}, (C) Dmitrii Gusev, 2018-
|
|
|
18
18
|
@click.help_option("-h", "--help")
|
|
19
19
|
@click.version_option(__version__, "-v", "--version", message=MSG_VERSION)
|
|
20
20
|
@click.pass_context
|
|
21
|
-
def pyutils(ctx):
|
|
21
|
+
def pyutils(ctx): # pylint: disable=unused-argument
|
|
22
22
|
"""Module [Py Utilities], (C) Dmitrii Gusev, 2018-2026."""
|
|
23
23
|
click.echo("")
|
|
24
24
|
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Application logging init/managing module. For logging used loguru library. Provides flexible logging
|
|
5
|
+
with console and file output, rotation, retention, and compression.
|
|
6
|
+
|
|
7
|
+
Created: Dmitrii Gusev, 06.04.2026
|
|
8
|
+
Modified: Dmitrii Gusev, 15.05.2026
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from types import FrameType
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from loguru import logger
|
|
18
|
+
|
|
19
|
+
from pyutilities.utils.string_utils import is_empty
|
|
20
|
+
|
|
21
|
+
# - Default logger names for Flask (they needs special processing in case we are in Flask app)
|
|
22
|
+
FLASK_LOGGERS: list[str] = ["werkzeug", "flask.app"]
|
|
23
|
+
|
|
24
|
+
# - Log format and coloring constants (application + intercepted) - for more info see loguru details
|
|
25
|
+
DEFAULT_LOG_FORMAT = (
|
|
26
|
+
"<b><green>{time:DD-MM-YYYY HH:mm:ss}</green></b> | "
|
|
27
|
+
"<level>{level: <8}</level> | "
|
|
28
|
+
"<cyan>{extra[name]}</cyan>:<cyan>{function}</cyan>:"
|
|
29
|
+
"<cyan>{line}</cyan> - <level>{message}</level>\n"
|
|
30
|
+
)
|
|
31
|
+
INTERCEPTED_LOG_FORMAT = (
|
|
32
|
+
"<b><green>{time:DD-MM-YYYY HH:mm:ss}</green></b> | "
|
|
33
|
+
"<level>{level: <8}</level> | "
|
|
34
|
+
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:"
|
|
35
|
+
"<cyan>{line}</cyan> - <level>{message}</level>\n"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class InterceptHandler(logging.Handler):
|
|
40
|
+
"""Intercept standard logging messages and redirect them to loguru. This allows Flask and other
|
|
41
|
+
libraries using standard logging to use loguru. This class instance should be added to the logger
|
|
42
|
+
as a handler - and all logging messages will be intercepted by loguru and printed/showed in the
|
|
43
|
+
loguru output (console, log files).
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
47
|
+
|
|
48
|
+
# Get corresponding Loguru level if it exists (for external captured message)
|
|
49
|
+
try:
|
|
50
|
+
level = logger.level(record.levelname).name # matched log level for external message
|
|
51
|
+
except ValueError:
|
|
52
|
+
level = str(record.levelno) # fallback default
|
|
53
|
+
|
|
54
|
+
# Find caller from where originated the logged message
|
|
55
|
+
frame: FrameType | None = logging.currentframe()
|
|
56
|
+
depth = 6 # in some examples this is set to = 6 (six), originally here was = 2 (two)
|
|
57
|
+
while frame and frame.f_code.co_filename == logging.__file__:
|
|
58
|
+
frame = frame.f_back
|
|
59
|
+
depth += 1
|
|
60
|
+
|
|
61
|
+
# emit the external captured log message
|
|
62
|
+
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class LoggerFileAppenderConfig: # pylint: disable=too-few-public-methods
|
|
66
|
+
"""Logger file appender configuration for the LoggerManager, if we need the file appender/handler.
|
|
67
|
+
This class is needed only for configuration simplification. By default - the only console appender /
|
|
68
|
+
handler will be added to the logger, no files logs.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
file: str | None = None,
|
|
74
|
+
rotation: str = "100Mb",
|
|
75
|
+
retention: str = "7 days",
|
|
76
|
+
compression: str = "zip",
|
|
77
|
+
):
|
|
78
|
+
"""Initialize the logger appenders config, with the necessary arguments/parameters.
|
|
79
|
+
Args:
|
|
80
|
+
file: Log file path (None to disable file logging)
|
|
81
|
+
rotation: Log rotation size (e.g., "10 MB", "100 MB", "1 GB")
|
|
82
|
+
retention: Log retention period (e.g., "7 days", "1 week", "1 month")
|
|
83
|
+
compression: Compression format for rotated logs ("zip", "gz", "bz2", "xz")
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
self.file = file
|
|
87
|
+
self.rotation = rotation
|
|
88
|
+
self.retention = retention
|
|
89
|
+
self.compression = compression
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class LoggerManager: # pylint: disable=too-few-public-methods, too-many-instance-attributes
|
|
93
|
+
"""Manages application logging using loguru. Some LoggerManager implementation features are:
|
|
94
|
+
- Console and/or file logging
|
|
95
|
+
- Logs files rotation (default 100MB) + retention (default 7 days)
|
|
96
|
+
- Logs compression (zip) + configurable log level
|
|
97
|
+
- Contextual logging with class/function/line information
|
|
98
|
+
- Intercepts Flask and standard library logging
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
level: str = "INFO",
|
|
104
|
+
console: bool = True,
|
|
105
|
+
log_file_config: Optional[LoggerFileAppenderConfig] = None,
|
|
106
|
+
extra_loggers: Optional[dict[str, str]] = None,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Initialize the logger manager class instance.
|
|
109
|
+
Args:
|
|
110
|
+
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL), default = INFO
|
|
111
|
+
console: Enable/disable console logging (stdout), True/False, default = True
|
|
112
|
+
log_file_config: LoggerFileAppenderConfig - appenders configuration class
|
|
113
|
+
extra_loggers: dict[str, str] - <logger_name>: <logging_level> for additional loggers to be
|
|
114
|
+
configured with the setup, default = None
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
self.file: Optional[str] = None
|
|
118
|
+
self.rotation: Optional[str] = None
|
|
119
|
+
self.retention: Optional[str] = None
|
|
120
|
+
self.compression: Optional[str] = None
|
|
121
|
+
|
|
122
|
+
# - internal state initialization - file appender/handler
|
|
123
|
+
if log_file_config and not is_empty(log_file_config.file):
|
|
124
|
+
self.file = log_file_config.file
|
|
125
|
+
self.rotation = log_file_config.rotation
|
|
126
|
+
self.retention = log_file_config.retention
|
|
127
|
+
self.compression = log_file_config.compression
|
|
128
|
+
|
|
129
|
+
# - internal state initialization - console appender and level
|
|
130
|
+
self.level = level.upper()
|
|
131
|
+
self.console = console
|
|
132
|
+
# - internal state initialization - extra loggers and keys
|
|
133
|
+
self.extra_loggers: Optional[dict[str, str]] = extra_loggers
|
|
134
|
+
self.extra_loggers_keys: list[str] = (
|
|
135
|
+
extra_loggers.keys() if extra_loggers else [] # type: ignore[assignment]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
logger.remove() # Remove default logger (in order to avoid duplicate messages)
|
|
139
|
+
self._configure_logger() # Configure logger (call internal method)
|
|
140
|
+
self._intercept_standard_logging() # Intercept standard logging (call internal method)
|
|
141
|
+
|
|
142
|
+
def _configure_logger(self) -> None:
|
|
143
|
+
"""INTERNAL. Configure loguru logger based on settings. Internal method for class LoggerManager.
|
|
144
|
+
This method configure the records formatting, creating and adding the necessary handlers to the
|
|
145
|
+
loguru logger, some additional logger setup ().
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
# Custom formatter that handles both application logs (with extra[name]) and intercepted logs.
|
|
149
|
+
# This method is used as parameter for console appender - see below.
|
|
150
|
+
def format_record(record) -> str:
|
|
151
|
+
# Map format based on whether 'name' exists in extra
|
|
152
|
+
format_map: dict[bool, str] = {
|
|
153
|
+
True: DEFAULT_LOG_FORMAT, # Application logs with extra[name]
|
|
154
|
+
False: INTERCEPTED_LOG_FORMAT, # Intercepted logs from standard library
|
|
155
|
+
}
|
|
156
|
+
return format_map["name" in record["extra"]]
|
|
157
|
+
|
|
158
|
+
# ! change some coloring of the log messages
|
|
159
|
+
logger.level("DEBUG", color="<green>")
|
|
160
|
+
|
|
161
|
+
# Define handlers configuration
|
|
162
|
+
handlers = [] # handlers storage array
|
|
163
|
+
|
|
164
|
+
if self.console: # if console appender/handler enabled - configure and add it
|
|
165
|
+
|
|
166
|
+
handlers.append( # add console handler (write to console)
|
|
167
|
+
{
|
|
168
|
+
"sink": sys.stdout,
|
|
169
|
+
"format": format_record, # use the method above to format log record
|
|
170
|
+
"level": self.level,
|
|
171
|
+
"colorize": True,
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if self.file: # if file appender/handler enabled - configure and add it
|
|
176
|
+
|
|
177
|
+
log_path: Path = Path(self.file) # Ensure log directory exists (where logs should be stored)
|
|
178
|
+
log_path.parent.mkdir(parents=True, exist_ok=True) # create all necessary dirs (intermediate)
|
|
179
|
+
|
|
180
|
+
handlers.append( # add file handler (write to file specified)
|
|
181
|
+
{
|
|
182
|
+
"sink": self.file,
|
|
183
|
+
"format": format_record, # use the method above to format log record
|
|
184
|
+
"level": self.level,
|
|
185
|
+
"rotation": self.rotation,
|
|
186
|
+
"retention": self.retention,
|
|
187
|
+
"compression": self.compression,
|
|
188
|
+
"enqueue": True, # Thread-safe logging
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Add all configured handlers to logger
|
|
193
|
+
for handler_config in handlers:
|
|
194
|
+
logger.add(**handler_config) # type: ignore[arg-type]
|
|
195
|
+
|
|
196
|
+
def _intercept_standard_logging(self) -> None:
|
|
197
|
+
"""INTERNAL. Intercept standard library logging, used by external libraries and by the flask
|
|
198
|
+
application itself (flask.app, werkzeug) and redirect to loguru. Also set level for some used
|
|
199
|
+
libraries - for development/debug purposes. Internal method for class LoggerManager."""
|
|
200
|
+
|
|
201
|
+
# Intercept werkzeug (Flask)/system logging (A MUST: level=0, force=True -> intercept ALL!)
|
|
202
|
+
logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
|
|
203
|
+
|
|
204
|
+
# Intercept specific loggers (they are using python logging mechanism) - for Flask
|
|
205
|
+
if self.extra_loggers: # if there are extra loggers - process them
|
|
206
|
+
for logger_name in self.extra_loggers_keys:
|
|
207
|
+
if logger_name in FLASK_LOGGERS: # special init for Flask loggers
|
|
208
|
+
log = logging.getLogger(name=logger_name)
|
|
209
|
+
log.handlers = [InterceptHandler()]
|
|
210
|
+
log.propagate = False
|
|
211
|
+
else: # just set level for other loggers
|
|
212
|
+
# ! set the appropriate level for some libraries - it may help to reduce
|
|
213
|
+
# ! logging output amount (brevity) or clarify libraries internals (more logging)
|
|
214
|
+
logging.getLogger(logger_name).setLevel(self.extra_loggers[logger_name])
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def get_logger(name: str):
|
|
218
|
+
"""CLASS METHOD. Get a logger instance with a specific name (typically class name). Factory method,
|
|
219
|
+
binds the specific name to the loguru logger and return the loguru logger.
|
|
220
|
+
Args:
|
|
221
|
+
name: Logger name (usually __name__ or class name)
|
|
222
|
+
Returns:
|
|
223
|
+
Logger instance bound with the name
|
|
224
|
+
Usage:
|
|
225
|
+
class MyService:
|
|
226
|
+
def __init__(self):
|
|
227
|
+
self.logger = LoggerManager.get_logger(self.__class__.__name__)
|
|
228
|
+
|
|
229
|
+
def my_method(self):
|
|
230
|
+
self.logger.info("Processing data")
|
|
231
|
+
"""
|
|
232
|
+
return logger.bind(name=name)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
|
236
|
+
def setup_logging(
|
|
237
|
+
level: str = "INFO",
|
|
238
|
+
console: bool = True,
|
|
239
|
+
file: str | None = None,
|
|
240
|
+
rotation: str = "100 MB",
|
|
241
|
+
retention: str = "7 days",
|
|
242
|
+
compression: str = "zip",
|
|
243
|
+
extra_loggers: Optional[dict[str, str]] = None,
|
|
244
|
+
) -> LoggerManager:
|
|
245
|
+
"""MODULE METHOD. Setup application logging. This is the MAIN ENTRY POINT FOR configuring LOGGING
|
|
246
|
+
in the application. Should be called once at application startup - this method creates and initializes
|
|
247
|
+
the only instance of the LoggerManager class.
|
|
248
|
+
Args:
|
|
249
|
+
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
250
|
+
console: Enable console logging (stdout)
|
|
251
|
+
file: Log file path (None to disable file logging)
|
|
252
|
+
rotation: Log rotation size (e.g., "10 MB", "100 MB", "1 GB")
|
|
253
|
+
retention: Log retention period (e.g., "7 days", "1 week", "1 month")
|
|
254
|
+
compression: Compression format for rotated logs ("zip", "gz", "bz2", "xz")
|
|
255
|
+
extra_loggers: dict[str, str] - <logger_name>: <logging_level> for additional loggers to be
|
|
256
|
+
configured with the setup
|
|
257
|
+
Returns:
|
|
258
|
+
LoggerManager instance
|
|
259
|
+
Usage:
|
|
260
|
+
from app.core.log import setup_logging
|
|
261
|
+
|
|
262
|
+
# Console only (default)
|
|
263
|
+
setup_logging(level="INFO")
|
|
264
|
+
|
|
265
|
+
# File only
|
|
266
|
+
setup_logging(console=False, file="logs/app.log")
|
|
267
|
+
|
|
268
|
+
# Both console and file
|
|
269
|
+
setup_logging(console=True, file="logs/app.log")
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
# create instance of file appender config - if file specified
|
|
273
|
+
log_file_config: Optional[LoggerFileAppenderConfig] = (
|
|
274
|
+
LoggerFileAppenderConfig(file=file, rotation=rotation, retention=retention, compression=compression)
|
|
275
|
+
if not is_empty(file)
|
|
276
|
+
else None
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return LoggerManager(
|
|
280
|
+
level=level, console=console, log_file_config=log_file_config, extra_loggers=extra_loggers
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
|
285
|
+
def setup_logging_flask(
|
|
286
|
+
level: str = "INFO",
|
|
287
|
+
console: bool = True,
|
|
288
|
+
file: str | None = None,
|
|
289
|
+
rotation: str = "100 MB",
|
|
290
|
+
retention: str = "7 days",
|
|
291
|
+
compression: str = "zip",
|
|
292
|
+
extra_loggers: Optional[dict[str, str]] = None,
|
|
293
|
+
) -> LoggerManager:
|
|
294
|
+
"""MODULE METHOD. Setup application logging for Flask application. This is the MAIN ENTRY POINT
|
|
295
|
+
FOR configuring LOGGING in the application. Should be called once at application startup - this
|
|
296
|
+
method creates and initializes the only instance of the LoggerManager class.
|
|
297
|
+
# ! Difference from the setup_logging() method is that the Flask loggers added to extra_loggers.
|
|
298
|
+
Args:
|
|
299
|
+
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
300
|
+
console: Enable console logging (stdout)
|
|
301
|
+
file: Log file path (None to disable file logging)
|
|
302
|
+
rotation: Log rotation size (e.g., "10 MB", "100 MB", "1 GB")
|
|
303
|
+
retention: Log retention period (e.g., "7 days", "1 week", "1 month")
|
|
304
|
+
compression: Compression format for rotated logs ("zip", "gz", "bz2", "xz")
|
|
305
|
+
extra_loggers: dict[str, str] - <logger_name>: <logging_level> for additional loggers to be
|
|
306
|
+
configured with the setup
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
# create instance of file appender config - if file specified
|
|
310
|
+
log_file_config: Optional[LoggerFileAppenderConfig] = (
|
|
311
|
+
LoggerFileAppenderConfig(file=file, rotation=rotation, retention=retention, compression=compression)
|
|
312
|
+
if not is_empty(file)
|
|
313
|
+
else None
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# add Flask loggers to the dictionary of extra_loggers
|
|
317
|
+
if not extra_loggers: # init extra loggers in case it is not initialized
|
|
318
|
+
extra_loggers = {}
|
|
319
|
+
|
|
320
|
+
for logger_name in FLASK_LOGGERS:
|
|
321
|
+
extra_loggers[logger_name] = "INFO" # by default set the INFO, but this value will be ignored
|
|
322
|
+
|
|
323
|
+
return LoggerManager(
|
|
324
|
+
level=level, console=console, log_file_config=log_file_config, extra_loggers=extra_loggers
|
|
325
|
+
)
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
System Utilities module.
|
|
5
5
|
|
|
6
6
|
Created: Dmitrii Gusev, 18.07.2025
|
|
7
|
-
Modified: Dmitrii Gusev,
|
|
7
|
+
Modified: Dmitrii Gusev, 15.05.2026
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import logging
|
|
11
11
|
import os
|
|
12
12
|
|
|
13
|
-
from pyutilities.
|
|
13
|
+
from pyutilities.defaults import MSG_MODULE_ISNT_RUNNABLE
|
|
14
|
+
from pyutilities.utils.string_utils import is_empty, str_2_bool, str_2_float, str_2_int
|
|
14
15
|
|
|
15
16
|
log = logging.getLogger(__name__)
|
|
16
17
|
log.addHandler(logging.NullHandler())
|
|
@@ -19,10 +20,7 @@ log.addHandler(logging.NullHandler())
|
|
|
19
20
|
def get_str_from_env(key: str, default: str = "") -> str:
|
|
20
21
|
"""TBD"""
|
|
21
22
|
|
|
22
|
-
if
|
|
23
|
-
return default
|
|
24
|
-
|
|
25
|
-
return os.environ.get(key) or default
|
|
23
|
+
return default if is_empty(key) else (os.environ.get(key) or default)
|
|
26
24
|
|
|
27
25
|
|
|
28
26
|
def get_int_from_env(key: str, default: int = 0) -> int:
|
|
@@ -53,3 +51,11 @@ def get_bool_from_env(key: str, default: bool = False) -> bool:
|
|
|
53
51
|
|
|
54
52
|
env_value = os.environ.get(key)
|
|
55
53
|
return str_2_bool(env_value) if env_value else default
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# # show all env variables sorted by name:
|
|
57
|
+
# # print('\n'.join(f'{k}={v}' for k, v in sorted(os.environ.items())))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
print(MSG_MODULE_ISNT_RUNNABLE)
|
|
@@ -9,15 +9,13 @@ Useful materials:
|
|
|
9
9
|
how-do-i-convert-this-list-of-dictionaries-to-a-csv-file
|
|
10
10
|
|
|
11
11
|
Created: Gusev Dmitrii, 10.10.2022
|
|
12
|
-
Modified: Dmitrii Gusev,
|
|
12
|
+
Modified: Dmitrii Gusev, 14.05.2026
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
import csv
|
|
16
16
|
import inspect
|
|
17
17
|
import logging
|
|
18
18
|
import os
|
|
19
|
-
import threading
|
|
20
|
-
import time
|
|
21
19
|
from typing import Any, Dict, List, Tuple
|
|
22
20
|
|
|
23
21
|
from pyutilities.defaults import DEFAULT_ENCODING, MSG_MODULE_ISNT_RUNNABLE
|
|
@@ -36,59 +34,6 @@ NUM_CHARS = "0123456789"
|
|
|
36
34
|
SPEC_CHARS = "-"
|
|
37
35
|
|
|
38
36
|
|
|
39
|
-
def singleton(class_):
|
|
40
|
-
"""Decorator: singleton class decorator. Use it on the class level to make class Singleton."""
|
|
41
|
-
|
|
42
|
-
instances = {} # classes instances storage
|
|
43
|
-
|
|
44
|
-
def get_instance(*args, **kwargs):
|
|
45
|
-
if class_ not in instances:
|
|
46
|
-
instances[class_] = class_(*args, **kwargs)
|
|
47
|
-
return instances[class_]
|
|
48
|
-
|
|
49
|
-
return get_instance
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def threadsafe_function(fn):
|
|
53
|
-
"""Decorator: it is making sure that the decorated function is thread safe."""
|
|
54
|
-
|
|
55
|
-
lock = threading.Lock() # acquire lock
|
|
56
|
-
|
|
57
|
-
def new(*args, **kwargs):
|
|
58
|
-
lock.acquire()
|
|
59
|
-
try:
|
|
60
|
-
r = fn(*args, **kwargs)
|
|
61
|
-
finally:
|
|
62
|
-
lock.release() # release lock in any case
|
|
63
|
-
return r
|
|
64
|
-
|
|
65
|
-
return new
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def debug_benchmark(func):
|
|
69
|
-
"""Decorator: logs the given function execution time."""
|
|
70
|
-
|
|
71
|
-
def wrapper(*args, **kwargs):
|
|
72
|
-
t = time.process_time()
|
|
73
|
-
res = func(*args, **kwargs)
|
|
74
|
-
log.debug("Function [%s] executed in [%s] second(s).", func.__name__, time.process_time() - t)
|
|
75
|
-
return res
|
|
76
|
-
|
|
77
|
-
return wrapper
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def debug_function_name(func):
|
|
81
|
-
"""Decorator: logs the name of the decorating function."""
|
|
82
|
-
|
|
83
|
-
def wrapper(*args, **kwargs):
|
|
84
|
-
log.debug("Function [%s] is working.", func.__name__)
|
|
85
|
-
# print(func.__name__, args, kwargs)
|
|
86
|
-
res = func(*args, **kwargs)
|
|
87
|
-
return res
|
|
88
|
-
|
|
89
|
-
return wrapper
|
|
90
|
-
|
|
91
|
-
|
|
92
37
|
def myself():
|
|
93
38
|
"""Handy utility function/lambda for getting name of executing function from inside the function.
|
|
94
39
|
Can be rewritten as lambda: myself = lambda: inspect.stack()[1][3]"""
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Useful date/time utilities and functions.
|
|
5
|
+
|
|
6
|
+
Created: Dmitrii Gusev, 22.03.2026
|
|
7
|
+
Modified: Dmitrii Gusev, 10.05.2026
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from datetime import datetime, timedelta, timezone
|
|
11
|
+
|
|
12
|
+
import pendulum
|
|
13
|
+
from pendulum import Date
|
|
14
|
+
|
|
15
|
+
from pyutilities.defaults import MSG_MODULE_ISNT_RUNNABLE
|
|
16
|
+
|
|
17
|
+
MSK_TIMEZONE: timezone = timezone(timedelta(hours=3), name="Moscow Timezone (Russia)")
|
|
18
|
+
MSK_TIMEZONE_NAME: str = "Europe/Moscow"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_timestamp(
|
|
22
|
+
current_timezone: timezone = MSK_TIMEZONE,
|
|
23
|
+
days: float = 0,
|
|
24
|
+
hours: float = 0,
|
|
25
|
+
minutes: float = 0,
|
|
26
|
+
seconds: float = 0,
|
|
27
|
+
) -> datetime:
|
|
28
|
+
"""Return timestamp: now (in the default timezone) +/- the specified time delta in hours / minutes /
|
|
29
|
+
seconds. Default timezone can be changed by the function argument."""
|
|
30
|
+
|
|
31
|
+
return datetime.now(current_timezone) + timedelta(
|
|
32
|
+
days=days, hours=hours, minutes=minutes, seconds=seconds
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_dates_range_before_date(date: datetime) -> tuple[int, int]:
|
|
37
|
+
"""Generates two dates range - date_from/date_to for the time period 'before today' - from the 1st day
|
|
38
|
+
of the current month till the today - 1 day (current month). In case today is 1st day - range for the
|
|
39
|
+
whole previous month, if today is 01.01.XXXX - range for 01.12.XXXX-1 - 31.12.XXXX-1.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# - local variables
|
|
43
|
+
local_today: Date = Date(date.year, date.month, date.day)
|
|
44
|
+
date_from: Date
|
|
45
|
+
date_to: Date
|
|
46
|
+
|
|
47
|
+
# - processing and calculating dates
|
|
48
|
+
if local_today.day == 1: # today - 1st day of month, we need report for the previous month
|
|
49
|
+
|
|
50
|
+
if local_today.month == 1: # we are at 01.01.XXXX - we need report for Dec.XXXX - 1
|
|
51
|
+
|
|
52
|
+
date_from = pendulum.date(local_today.year - 1, 12, 1) # from: 01 Dec Year-1
|
|
53
|
+
date_to = pendulum.date(local_today.year - 1, 12, 31) # to: 31 Dec Year-1
|
|
54
|
+
|
|
55
|
+
else: # we are at 01.XX.XXXX - we need report for previous month
|
|
56
|
+
|
|
57
|
+
# from: 01 Month-1 Year
|
|
58
|
+
date_from = pendulum.date(local_today.year, local_today.month - 1, 1)
|
|
59
|
+
# to: Last Day Month-1 Year
|
|
60
|
+
date_to = pendulum.date(
|
|
61
|
+
local_today.year, local_today.month - 1, local_today.subtract(months=1).days_in_month
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
else: # today isn't the 1st, we need report from 1st till yesterday
|
|
65
|
+
|
|
66
|
+
date_from = pendulum.date(local_today.year, local_today.month, 1) # from: 01 Month Year
|
|
67
|
+
date_to = pendulum.date(local_today.year, local_today.month, date.day - 1) # to: Day-1 Month Year
|
|
68
|
+
|
|
69
|
+
# - generate from/to timestamps
|
|
70
|
+
tstamp_from = pendulum.datetime(date_from.year, date_from.month, date_from.day)
|
|
71
|
+
tstamp_to = pendulum.datetime(date_to.year, date_to.month, date_to.day)
|
|
72
|
+
|
|
73
|
+
# - return the tuple of values
|
|
74
|
+
return (tstamp_from.int_timestamp, tstamp_to.int_timestamp)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
print(MSG_MODULE_ISNT_RUNNABLE)
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
Decorators module. Contains some useful decorators.
|
|
5
5
|
|
|
6
6
|
Created: Gusev Dmitrii, 10.10.2022
|
|
7
|
-
Modified: Dmitrii Gusev,
|
|
7
|
+
Modified: Dmitrii Gusev, 15.05.2026
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import functools
|
|
11
11
|
import logging
|
|
12
|
+
import threading
|
|
12
13
|
import time
|
|
13
14
|
from typing import Any
|
|
14
15
|
|
|
@@ -18,6 +19,78 @@ log = logging.getLogger(__name__)
|
|
|
18
19
|
log.addHandler(logging.NullHandler())
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
def singleton_v1(clazz):
|
|
23
|
+
"""Decorator: singleton class decorator. Use it on the class level to make class Singleton.
|
|
24
|
+
Version 1: uses set of instances for storing the one class instance.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
instances = {} # classes instances storage
|
|
28
|
+
|
|
29
|
+
@functools.wraps(clazz)
|
|
30
|
+
def get_instance(*args, **kwargs):
|
|
31
|
+
if clazz not in instances:
|
|
32
|
+
instances[clazz] = clazz(*args, **kwargs)
|
|
33
|
+
return instances[clazz]
|
|
34
|
+
|
|
35
|
+
return get_instance
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def singleton_v2(clazz):
|
|
39
|
+
"""Decorator: singleton class decorator. Use it on the class level to make class Singleton.
|
|
40
|
+
Version 2: uses reference to the singleton instance for storing the lnk to the one class instance.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
@functools.wraps(clazz)
|
|
44
|
+
def wrapper_singleton(*args, **kwargs):
|
|
45
|
+
if wrapper_singleton.instance is None: # type: ignore[attr-defined]
|
|
46
|
+
wrapper_singleton.instance = clazz(*args, **kwargs) # type: ignore[attr-defined]
|
|
47
|
+
return wrapper_singleton.instance # type: ignore[attr-defined]
|
|
48
|
+
|
|
49
|
+
wrapper_singleton.instance = None # type: ignore[attr-defined]
|
|
50
|
+
|
|
51
|
+
return wrapper_singleton
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def threadsafe_function(fn):
|
|
55
|
+
"""Decorator: it is making sure that the decorated function is thread safe."""
|
|
56
|
+
|
|
57
|
+
lock = threading.Lock() # acquire lock
|
|
58
|
+
|
|
59
|
+
@functools.wraps(fn)
|
|
60
|
+
def new(*args, **kwargs):
|
|
61
|
+
with lock:
|
|
62
|
+
result = fn(*args, **kwargs)
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
return new
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def debug_benchmark(func):
|
|
69
|
+
"""Decorator: logs the given function execution time."""
|
|
70
|
+
|
|
71
|
+
@functools.wraps(func)
|
|
72
|
+
def wrapper(*args, **kwargs):
|
|
73
|
+
t = time.process_time()
|
|
74
|
+
res = func(*args, **kwargs)
|
|
75
|
+
log.debug("Function [%s] executed in [%s] second(s).", func.__name__, time.process_time() - t)
|
|
76
|
+
return res
|
|
77
|
+
|
|
78
|
+
return wrapper
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def debug_function_name(func):
|
|
82
|
+
"""Decorator: logs the name of the decorating function."""
|
|
83
|
+
|
|
84
|
+
@functools.wraps(func)
|
|
85
|
+
def wrapper(*args, **kwargs):
|
|
86
|
+
log.debug("Function [%s] is working.", func.__name__)
|
|
87
|
+
# print(func.__name__, args, kwargs)
|
|
88
|
+
res = func(*args, **kwargs)
|
|
89
|
+
return res
|
|
90
|
+
|
|
91
|
+
return wrapper
|
|
92
|
+
|
|
93
|
+
|
|
21
94
|
def retry(max_tries=3, delay_seconds=1):
|
|
22
95
|
"""Retrying decorator."""
|
|
23
96
|
|
|
@@ -41,7 +114,9 @@ def retry(max_tries=3, delay_seconds=1):
|
|
|
41
114
|
|
|
42
115
|
|
|
43
116
|
def memoize(func):
|
|
44
|
-
"""
|
|
117
|
+
"""Cache decorator - analogue for the functool.lru_cache().
|
|
118
|
+
WARNING! Unbounded cache!
|
|
119
|
+
"""
|
|
45
120
|
|
|
46
121
|
cache: dict[Any, Any] = {}
|
|
47
122
|
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
"""
|
|
4
|
+
Lambdas module. Contains useful lambda functions.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
Created: Dmitrii Gusev, 15.05.2026
|
|
7
|
+
Modified: Dmitrii Gusev, 15.05.2026
|
|
8
|
+
"""
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from pyutilities.defaults import MSG_MODULE_ISNT_RUNNABLE
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger(__name__)
|
|
15
|
+
log.addHandler(logging.NullHandler())
|
|
11
16
|
|
|
12
17
|
# # execute shell cmd and return output
|
|
13
18
|
# sh = lambda cmd: __import__('subprocess').run(cmd, shell=True, check=True, capture_output=True) \
|
|
@@ -23,22 +28,15 @@
|
|
|
23
28
|
# # split list into n-sized chunks
|
|
24
29
|
# chunked = lambda lst, n: [lst[i:i+n] for i in range(0, len(lst), n)]
|
|
25
30
|
|
|
26
|
-
# #
|
|
27
|
-
# pretty_json = lambda obj: __import__('json').dumps(obj, ensure_ascii=False, indent=2)
|
|
28
|
-
|
|
29
|
-
# # get all files with the .py extebsion recursively
|
|
31
|
+
# # get all files with the .py extension recursively
|
|
30
32
|
# files = list(Path('.').rglob('*.py'))
|
|
31
33
|
|
|
32
|
-
# # show all env variables sorted by name:
|
|
33
|
-
# # print('\n'.join(f'{k}={v}' for k, v in sorted(os.environ.items())))
|
|
34
|
-
|
|
35
34
|
# # read all lines from file, removing wrap line
|
|
36
35
|
# lines = Path('file.txt').read_text().splitlines()
|
|
37
36
|
|
|
38
37
|
# # get SHA256 code of the string
|
|
39
38
|
# hash = hashlib.sha256(b"your text").hexdigest()
|
|
40
39
|
|
|
41
|
-
|
|
42
40
|
# # quickly stop the script execution
|
|
43
41
|
# raise SystemExit("Done")
|
|
44
42
|
|
|
@@ -53,3 +51,7 @@
|
|
|
53
51
|
|
|
54
52
|
# # check host liveness without ping
|
|
55
53
|
# ok = socket.create_connection((host, port), timeout=2)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
print(MSG_MODULE_ISNT_RUNNABLE)
|
|
@@ -10,14 +10,17 @@ in 'fast' executing code, where logging will just add additional complexity and
|
|
|
10
10
|
Also most of the functions use LRU feature of the python language.
|
|
11
11
|
|
|
12
12
|
Created: Dmitrii Gusev, 15.04.2019
|
|
13
|
-
Modified: Dmitrii Gusev,
|
|
13
|
+
Modified: Dmitrii Gusev, 14.05.2026
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
+
import json
|
|
16
17
|
import logging
|
|
17
18
|
from functools import lru_cache
|
|
18
19
|
from re import match as re_match
|
|
19
20
|
from typing import Any, Dict, Iterable, Tuple
|
|
20
21
|
|
|
22
|
+
import pyfiglet
|
|
23
|
+
|
|
21
24
|
from pyutilities.defaults import MSG_MODULE_ISNT_RUNNABLE
|
|
22
25
|
|
|
23
26
|
# configure logger on module level. it isn't a good practice, but it's convenient.
|
|
@@ -26,19 +29,31 @@ log = logging.getLogger(__name__)
|
|
|
26
29
|
# to avoid errors like 'no handlers' for libraries it's necessary/convenient to add NullHandler
|
|
27
30
|
log.addHandler(logging.NullHandler())
|
|
28
31
|
|
|
29
|
-
# useful module defaults
|
|
32
|
+
# - Symbols :: useful module defaults
|
|
30
33
|
SPECIAL_SYMBOLS = ".,/-№"
|
|
31
34
|
CYRILLIC_SYMBOLS = "абвгдеёжзийклмнопрстуфхцчшщъыьэюяАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ"
|
|
32
35
|
LATIN_SYMBOLS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
33
36
|
ALL_SYMBOLS = SPECIAL_SYMBOLS + CYRILLIC_SYMBOLS + LATIN_SYMBOLS
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
# - Symbols :: password replacement symbols for various output
|
|
38
|
+
REPLACEMENT_SYMBOLS = "**********"
|
|
39
|
+
# - Symbols :: set of regex for determining float values
|
|
40
|
+
# REGEX_FLOAT_1 = "^\d+?\.\d+?$" # orig regex # pylint: disable=anomalous-backslash-in-string # noqa: W605
|
|
41
|
+
REGEX_FLOAT_2 = "^\\d+?\\.\\d+?$" # orig regex with fixed warnings
|
|
38
42
|
REGEX_FLOAT_3 = "^[+-]?([0-9]*[.])?[0-9]+$" # simplified regex, matches: 123/123.456/.456
|
|
39
43
|
REGEX_FLOAT_4 = "^[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)$" # matches as previous plus: 123.
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
# - Symbols :: string True values (for the conversion purpose)
|
|
45
|
+
TRUE_STRINGS_LIST: list[str] = [
|
|
46
|
+
"true",
|
|
47
|
+
"1",
|
|
48
|
+
"t",
|
|
49
|
+
"y",
|
|
50
|
+
"yes",
|
|
51
|
+
"yeah",
|
|
52
|
+
"yup",
|
|
53
|
+
"certainly",
|
|
54
|
+
"uh-huh",
|
|
55
|
+
]
|
|
56
|
+
# - CACHE :: cache setup (size) for cached functions/methods
|
|
42
57
|
LRU_CACHE_SIZE: int = 128
|
|
43
58
|
|
|
44
59
|
|
|
@@ -110,12 +125,26 @@ def filterc_str(string: str | None, trace: bool = False) -> str | None:
|
|
|
110
125
|
return filter_str(string, trace)
|
|
111
126
|
|
|
112
127
|
|
|
128
|
+
def is_empty(string: str | None) -> bool:
|
|
129
|
+
"""NOT CACHED. Is string empty - None or contains only spaces. Based on non-cached trim_2_none()."""
|
|
130
|
+
|
|
131
|
+
return trim_2_none(string) is None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@lru_cache(maxsize=LRU_CACHE_SIZE)
|
|
135
|
+
def is_emptyc(string: str | None) -> bool:
|
|
136
|
+
"""CACHED. Is string empty - equals to None or contains only spaces. Based on cached trim_2_none()."""
|
|
137
|
+
|
|
138
|
+
return trimc_2_none(string) is None
|
|
139
|
+
|
|
140
|
+
|
|
113
141
|
@lru_cache(maxsize=LRU_CACHE_SIZE)
|
|
114
142
|
def process_url(
|
|
115
143
|
url: str, postfix: str = "", format_values: Tuple[str] | None = None, trace: bool = False
|
|
116
144
|
) -> str:
|
|
117
|
-
"""Process the provided url and update it: add postfix (if provided) and add format
|
|
118
|
-
(if provided). In case empty string provided = empty string will be returned as well.
|
|
145
|
+
"""CACHED. Process the provided url and update it: add postfix (if provided) and add format
|
|
146
|
+
values (if provided). In case empty string provided = empty string will be returned as well.
|
|
147
|
+
"""
|
|
119
148
|
|
|
120
149
|
result: str = ""
|
|
121
150
|
if url and url.strip(): # provided url is not empty
|
|
@@ -148,7 +177,7 @@ def process_url(
|
|
|
148
177
|
def process_urls(
|
|
149
178
|
urls: Dict[str, str], postfix: str = "", format_values: Tuple[str] | None = None, trace: bool = False
|
|
150
179
|
) -> Dict[str, str]:
|
|
151
|
-
"""Process the provided dictionary of urls with the function"""
|
|
180
|
+
"""CACHED. Process the provided dictionary of urls with the function"""
|
|
152
181
|
|
|
153
182
|
processed: Dict[str, str] = {}
|
|
154
183
|
|
|
@@ -164,8 +193,9 @@ def process_urls(
|
|
|
164
193
|
|
|
165
194
|
@lru_cache(maxsize=LRU_CACHE_SIZE)
|
|
166
195
|
def get_str_ending(string: str, symbol: str = "/", trace: bool = False) -> str:
|
|
167
|
-
"""Returns the last right part of the string after the symbol (not including the symbol
|
|
168
|
-
most right part of the string, after the last right symbol (if there are multiple
|
|
196
|
+
"""CACHED. Returns the last right part of the string after the symbol (not including the symbol
|
|
197
|
+
itself). It is most right part of the string, after the last right symbol (if there are multiple
|
|
198
|
+
symbols).
|
|
169
199
|
"""
|
|
170
200
|
|
|
171
201
|
result: str = string
|
|
@@ -183,8 +213,9 @@ def get_str_ending(string: str, symbol: str = "/", trace: bool = False) -> str:
|
|
|
183
213
|
|
|
184
214
|
@lru_cache(maxsize=LRU_CACHE_SIZE)
|
|
185
215
|
def is_number(value: str, trace: bool = False) -> bool:
|
|
186
|
-
"""Returns True if string is a number. String is checked 'as is' - no trailing/leading
|
|
187
|
-
any other transformations.
|
|
216
|
+
"""CACHED. Returns True if string is a number. String is checked 'as is' - no trailing/leading
|
|
217
|
+
spaces cut, no any other transformations.
|
|
218
|
+
"""
|
|
188
219
|
|
|
189
220
|
result: bool = False
|
|
190
221
|
|
|
@@ -199,10 +230,12 @@ def is_number(value: str, trace: bool = False) -> bool:
|
|
|
199
230
|
|
|
200
231
|
|
|
201
232
|
def iter_2_str(values: Iterable[Any], braces: bool = True, trace: bool = False) -> str: # noqa: C901
|
|
202
|
-
"""
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
will be
|
|
233
|
+
"""NOT CACHED. Despite the function itself doesn't use cache, it uses cached function is_number().
|
|
234
|
+
Convert number of iterable values to a single string value. If iterable is empty - the result
|
|
235
|
+
is empty string. If braces == True, the braces will be added around the resulting string. For
|
|
236
|
+
each value in the iterable trailing/leading spaces will be cut, duplicates will be removed,
|
|
237
|
+
duplicate numbers will be removed as well.
|
|
238
|
+
"""
|
|
206
239
|
|
|
207
240
|
# setup for processing
|
|
208
241
|
resulting_value: str = ""
|
|
@@ -244,11 +277,47 @@ def iter_2_str(values: Iterable[Any], braces: bool = True, trace: bool = False)
|
|
|
244
277
|
resulting_value = resulting_value[:-2]
|
|
245
278
|
|
|
246
279
|
if trace:
|
|
247
|
-
log.debug("iter_2_str():
|
|
280
|
+
log.debug("iter_2_str(): result is [%s].", resulting_value)
|
|
248
281
|
|
|
249
282
|
return resulting_value # returning result
|
|
250
283
|
|
|
251
284
|
|
|
285
|
+
def dict_2_json_str(dictionary: dict[Any, Any]) -> str:
|
|
286
|
+
"""NOT CACHED. Convert dictionary to readable/printable JSON string. If contains entries like
|
|
287
|
+
'password'/'pass' - such an entries values will be replaced by '**********' values. Non-serializable
|
|
288
|
+
entries will be replaced by symbols '<not serializable>'.
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
if not dictionary: # quick check
|
|
292
|
+
return ""
|
|
293
|
+
|
|
294
|
+
# - filter out (mask) passwords in the config
|
|
295
|
+
filtered_dictionary = {
|
|
296
|
+
k: (v if "password" not in str(k).lower() and "pass" not in str(k).lower() else REPLACEMENT_SYMBOLS)
|
|
297
|
+
for k, v in dictionary.items()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# - dump JSON and set a lambda for not serializable types
|
|
301
|
+
json_str: str = json.dumps(filtered_dictionary, indent=4, default=lambda o: "<not serializable>")
|
|
302
|
+
|
|
303
|
+
return json_str
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# # convert python object to the human-readable JSON
|
|
307
|
+
# pretty_json = lambda obj: __import__('json').dumps(obj, ensure_ascii=False, indent=2)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def generate_ascii_title(app_title: str, app_subtitle: str) -> str:
|
|
311
|
+
"""NOT CACHED. Generates ASCII title for the application/system, using short/abbreviated name,
|
|
312
|
+
version, author and dates info. ASCII title returned as string.
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
ascii_title: str = pyfiglet.figlet_format(f"{app_title}", font="doom")
|
|
316
|
+
ascii_title += f"\n\n{ascii_title}\n{app_subtitle}\n"
|
|
317
|
+
|
|
318
|
+
return ascii_title
|
|
319
|
+
|
|
320
|
+
|
|
252
321
|
def coalesce(*args, trace: bool = False) -> str:
|
|
253
322
|
"""NOT CACHED. Return first not None and not empty value from provided args list."""
|
|
254
323
|
|
|
@@ -280,8 +349,9 @@ def coalescec(*args, trace: bool = False) -> str:
|
|
|
280
349
|
|
|
281
350
|
@lru_cache(maxsize=LRU_CACHE_SIZE)
|
|
282
351
|
def one_of_2_str(string1: str | None, string2: str | None, trace: bool = False) -> str | None:
|
|
283
|
-
"""Function returning one of two strings, if other is empty. If both are empty or
|
|
284
|
-
returns None (empty value).
|
|
352
|
+
"""CACHED. Function returning one of two strings, if other is empty. If both are empty or
|
|
353
|
+
filled in - method returns None (empty value).
|
|
354
|
+
"""
|
|
285
355
|
|
|
286
356
|
result: str | None = None
|
|
287
357
|
if string1 and string1.strip(): # first string check
|
|
@@ -299,7 +369,7 @@ def one_of_2_str(string1: str | None, string2: str | None, trace: bool = False)
|
|
|
299
369
|
|
|
300
370
|
@lru_cache(maxsize=LRU_CACHE_SIZE)
|
|
301
371
|
def convert_bytes(num: float, trace: bool = False) -> str:
|
|
302
|
-
"""Function will convert bytes to MB.... GB... etc. for readability."""
|
|
372
|
+
"""CACHED. Function will convert bytes to MB.... GB... etc. for readability."""
|
|
303
373
|
|
|
304
374
|
# processing of the value
|
|
305
375
|
result: str = "unknown"
|
|
@@ -316,9 +386,10 @@ def convert_bytes(num: float, trace: bool = False) -> str:
|
|
|
316
386
|
|
|
317
387
|
@lru_cache(maxsize=LRU_CACHE_SIZE)
|
|
318
388
|
def str_2_bool(string: str | None, trace: bool = False) -> bool:
|
|
319
|
-
"""Convert string to bool. If empty string - return False. If string is not empty - return
|
|
320
|
-
converted to lower case, contains in the list: 'true', '1', 't', 'y', 'yes', 'yeah',
|
|
321
|
-
'uh-huh'.
|
|
389
|
+
"""CACHED. Convert string to bool. If empty string - return False. If string is not empty - return
|
|
390
|
+
if string, converted to lower case, contains in the list: 'true', '1', 't', 'y', 'yes', 'yeah',
|
|
391
|
+
'yup', 'certainly', 'uh-huh'.
|
|
392
|
+
"""
|
|
322
393
|
|
|
323
394
|
result: bool
|
|
324
395
|
if not string or not string.strip(): # empty string - returning False
|
|
@@ -344,8 +415,9 @@ def str_2_bool(string: str | None, trace: bool = False) -> bool:
|
|
|
344
415
|
|
|
345
416
|
@lru_cache(maxsize=LRU_CACHE_SIZE)
|
|
346
417
|
def str_2_int(string: str | None, trace: bool = False) -> int:
|
|
347
|
-
"""Convert string to integer number. Empty string or string with non-digit symbols will
|
|
348
|
-
otherwise will return int(string).
|
|
418
|
+
"""CACHED. Convert string to integer number. Empty string or string with non-digit symbols will
|
|
419
|
+
return 0, otherwise will return int(string).
|
|
420
|
+
"""
|
|
349
421
|
|
|
350
422
|
result: int
|
|
351
423
|
if not string or not string.strip(): # empty input string
|
|
@@ -368,8 +440,9 @@ def str_2_int(string: str | None, trace: bool = False) -> int:
|
|
|
368
440
|
|
|
369
441
|
@lru_cache(maxsize=LRU_CACHE_SIZE)
|
|
370
442
|
def str_2_float(string: str | None, trace: bool = False) -> float:
|
|
371
|
-
"""Convert string to float number. Empty string or string with non-digit symbols
|
|
372
|
-
will return 0, otherwise will return float(string).
|
|
443
|
+
"""CACHED. Convert string to float number. Empty string or string with non-digit symbols
|
|
444
|
+
will return 0, otherwise will return float(string).
|
|
445
|
+
"""
|
|
373
446
|
|
|
374
447
|
result: float
|
|
375
448
|
if not string or not string.strip():
|
|
File without changes
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
HTTP client module, based on requests module.
|
|
6
6
|
|
|
7
7
|
Created: Dmitrii Gusev, 01.06.2021
|
|
8
|
-
Modified: Dmitrii Gusev,
|
|
8
|
+
Modified: Dmitrii Gusev, 15.05.2026
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import logging
|
|
@@ -17,7 +17,7 @@ from requests import Response
|
|
|
17
17
|
from requests.adapters import HTTPAdapter, Retry
|
|
18
18
|
|
|
19
19
|
from pyutilities.defaults import MSG_MODULE_ISNT_RUNNABLE
|
|
20
|
-
from pyutilities.utils.
|
|
20
|
+
from pyutilities.utils.decorators import threadsafe_function
|
|
21
21
|
|
|
22
22
|
# init module logger
|
|
23
23
|
log = logging.getLogger(__name__)
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
|
|
3
|
-
"""
|
|
4
|
-
Useful date/time utilities and functions.
|
|
5
|
-
|
|
6
|
-
Created: Dmitrii Gusev, 22.03.2026
|
|
7
|
-
Modified: Dmitrii Gusev, 22.03.2026
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from datetime import datetime, timedelta, timezone
|
|
11
|
-
|
|
12
|
-
from pyutilities.defaults import MSG_MODULE_ISNT_RUNNABLE
|
|
13
|
-
|
|
14
|
-
MSK_TIMEZONE: timezone = timezone(timedelta(hours=3), name="Moscow Timezone (Russia)")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def get_timestamp(
|
|
18
|
-
current_timezone: timezone = MSK_TIMEZONE,
|
|
19
|
-
days: float = 0,
|
|
20
|
-
hours: float = 0,
|
|
21
|
-
minutes: float = 0,
|
|
22
|
-
seconds: float = 0,
|
|
23
|
-
) -> datetime:
|
|
24
|
-
"""Return timestamp: now (in the default timezone) +/- the specified time delta in hours / minutes /
|
|
25
|
-
seconds. Default timezone can be changed by the function argument."""
|
|
26
|
-
|
|
27
|
-
return datetime.now(current_timezone) + timedelta(
|
|
28
|
-
days=days, hours=hours, minutes=minutes, seconds=seconds
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if __name__ == "__main__":
|
|
33
|
-
print(MSG_MODULE_ISNT_RUNNABLE)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyutilities-2.4.0.dev1/src/pyutilities/sys → pyutilities-2.4.2/src/pyutilities/log}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyutilities-2.4.0.dev1/src/pyutilities/web → pyutilities-2.4.2/src/pyutilities/sys}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|