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.
Files changed (28) hide show
  1. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/PKG-INFO +30 -8
  2. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/docs/README-PUB.md +17 -7
  3. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/pyproject.toml +30 -6
  4. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/cli/pyutils.py +1 -1
  5. pyutilities-2.4.2/src/pyutilities/log/log_manager.py +325 -0
  6. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/sys/sys_utils.py +12 -6
  7. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/utils/common_utils.py +1 -56
  8. pyutilities-2.4.2/src/pyutilities/utils/datetime_utils.py +78 -0
  9. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/utils/decorators.py +77 -2
  10. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/utils/lambdas.py +17 -15
  11. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/utils/string_utils.py +103 -30
  12. pyutilities-2.4.2/src/pyutilities/web/__init__.py +0 -0
  13. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/web/http_client.py +2 -2
  14. pyutilities-2.4.0.dev1/src/pyutilities/utils/datetime_utils.py +0 -33
  15. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/LICENSE +0 -0
  16. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/__init__.py +0 -0
  17. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/cli/__init__.py +0 -0
  18. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/cli/fedit.py +0 -0
  19. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/config/__init__.py +0 -0
  20. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/config/configuration.py +0 -0
  21. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/defaults.py +0 -0
  22. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/exception.py +0 -0
  23. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/io/__init__.py +0 -0
  24. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/io/io_utils.py +0 -0
  25. {pyutilities-2.4.0.dev1/src/pyutilities/sys → pyutilities-2.4.2/src/pyutilities/log}/__init__.py +0 -0
  26. {pyutilities-2.4.0.dev1 → pyutilities-2.4.2}/src/pyutilities/logging.py +0 -0
  27. {pyutilities-2.4.0.dev1/src/pyutilities/web → pyutilities-2.4.2/src/pyutilities/sys}/__init__.py +0 -0
  28. {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.0.dev1
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.0, 2018 - 2026, Dmitrii Gusev
47
- *Last update: XX.XX.2026*
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.0
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
- Added new functions:
78
+ Intermediate version with improvements/refactorings.
79
+
80
+ ### 2.2.0
58
81
 
59
- - func1()
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 may be installed only for it!**
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.0, 2018 - 2026, Dmitrii Gusev
4
- *Last update: XX.XX.2026*
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.0
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
- Added new functions:
23
+ Intermediate version with improvements/refactorings.
24
+
25
+ ### 2.2.0
15
26
 
16
- - func1()
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 may be installed only for it!**
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, 19.04.2026
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.0dev1"
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, 28.07.2025
7
+ Modified: Dmitrii Gusev, 15.05.2026
8
8
  """
9
9
 
10
10
  import logging
11
11
  import os
12
12
 
13
- from pyutilities.utils.string_utils import str_2_bool, str_2_float, str_2_int
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 not key or not key.strip():
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, 24.11.2025
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, 25.11.2025
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
- """Decorator - analogue for the functool.lru_cache()."""
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
- # # -*- coding: utf-8 -*-
1
+ # -*- coding: utf-8 -*-
2
2
 
3
- # """
4
- # Module with useful lambdas functions/one-liners.
3
+ """
4
+ Lambdas module. Contains useful lambda functions.
5
5
 
6
- # Created: Dmitrii Gusev, 29.07.2025
7
- # Modified: Dmitrii Gusev, 21.11.2025
8
- # """
6
+ Created: Dmitrii Gusev, 15.05.2026
7
+ Modified: Dmitrii Gusev, 15.05.2026
8
+ """
9
9
 
10
- # from pathlib import Path
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
- # # convert python object to the human-readable JSON
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, 17.04.2026
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
- # set of regex for determining float values
36
- REGEX_FLOAT_1 = "^\d+?\.\d+?$" # original regex # pylint: disable=anomalous-backslash-in-string # noqa: W605
37
- REGEX_FLOAT_2 = "^\\d+?\\.\\d+?$" # original regex with fixed warnings
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
- # cache setup (size) for cached functions/methods
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 values
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 itself). It is
168
- most right part of the string, after the last right symbol (if there are multiple symbols).
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 spaces cut, no
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
- """Convert number of iterable values to a single string value. If iterable is empty - the result is
203
- empty string. If braces == True, the braces will be added around the resulting string. For each value
204
- in the iterable trailing/leading spaces will be cut, duplicates will be removed, duplicate numbers
205
- will be removed as well."""
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(): resulting string is [%s].", resulting_value)
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 filled in - method
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 if string,
320
- converted to lower case, contains in the list: 'true', '1', 't', 'y', 'yes', 'yeah', 'yup', 'certainly',
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 return 0,
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, 24.11.2025
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.common_utils import threadsafe_function
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