pyutilities 2.5.2__tar.gz → 2.7.0__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.5.2 → pyutilities-2.7.0}/PKG-INFO +4 -4
- {pyutilities-2.5.2 → pyutilities-2.7.0}/pyproject.toml +11 -11
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/cli/fedit.py +9 -7
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/config/configuration.py +4 -3
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/io/io_utils.py +45 -0
- pyutilities-2.7.0/src/pyutilities/utils/datetime_utils.py +263 -0
- pyutilities-2.7.0/src/pyutilities/xls/__init__.py +0 -0
- pyutilities-2.7.0/src/pyutilities/xls/xls_utils.py +99 -0
- pyutilities-2.5.2/src/pyutilities/utils/datetime_utils.py +0 -148
- {pyutilities-2.5.2 → pyutilities-2.7.0}/LICENSE +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/README.md +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/__init__.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/cli/__init__.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/cli/pyutils.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/config/__init__.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/defaults.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/exception.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/io/__init__.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/log/__init__.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/log/log_manager.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/logging.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/sys/__init__.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/sys/sys_utils.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/utils/__init__.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/utils/common_utils.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/utils/decorators.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/utils/lambdas.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/utils/string_utils.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/utils/structures.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/web/__init__.py +0 -0
- {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/web/http_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyutilities
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
4
4
|
Summary: PyUtilities :: Useful python 3+ utilities library for various use cases.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -27,7 +27,7 @@ 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
29
|
Requires-Dist: marshmallow (>=4.1,<5.0)
|
|
30
|
-
Requires-Dist: openpyxl (
|
|
30
|
+
Requires-Dist: openpyxl (>=3.1,<4.0)
|
|
31
31
|
Requires-Dist: paramiko (==5.*)
|
|
32
32
|
Requires-Dist: pendulum (>=3.2,<4.0)
|
|
33
33
|
Requires-Dist: prettytable (>=3.14,<4.0)
|
|
@@ -45,9 +45,9 @@ Requires-Dist: rich (>=15.0,<16.0)
|
|
|
45
45
|
Requires-Dist: rich-pyfiglet (>=2.0,<3.0)
|
|
46
46
|
Requires-Dist: ruamel.yaml (>=0.18,<1.0)
|
|
47
47
|
Requires-Dist: urllib3 (>=2.6,<3.0)
|
|
48
|
-
Requires-Dist: xlrd (>=2.0
|
|
48
|
+
Requires-Dist: xlrd (>=2.0,<3.0)
|
|
49
49
|
Requires-Dist: xlwings (>=0.35,<1.0)
|
|
50
|
-
Requires-Dist: xlwt (
|
|
50
|
+
Requires-Dist: xlwt (>=1.3,<2.0)
|
|
51
51
|
Project-URL: Bug Tracker, https://github.com/dmitry-ed-gusev/pyutilities/issues
|
|
52
52
|
Project-URL: Documentation, https://github.com/dmitry-ed-gusev/pyutilities
|
|
53
53
|
Project-URL: Homepage, https://pypi.org/project/pyutilities
|
|
@@ -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, 09.07.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.
|
|
23
|
+
version = "2.7.0"
|
|
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"},
|
|
@@ -56,10 +56,10 @@ dependencies = [
|
|
|
56
56
|
"loguru==0.7.3",
|
|
57
57
|
|
|
58
58
|
# -- ms word / ms excel libraries
|
|
59
|
-
"xlrd~=2.0
|
|
60
|
-
"xlwt
|
|
61
|
-
"xlwings~=0.35",
|
|
62
|
-
"openpyxl
|
|
59
|
+
"xlrd ~=2.0",
|
|
60
|
+
"xlwt ~=1.3",
|
|
61
|
+
"xlwings ~=0.35",
|
|
62
|
+
"openpyxl ~=3.1",
|
|
63
63
|
|
|
64
64
|
# -- data validation and marshalling (JSON/YAML) --
|
|
65
65
|
"pydantic (>=2.12.5,<3.0.0)",
|
|
@@ -102,10 +102,10 @@ dev = [
|
|
|
102
102
|
"pytest-env ==1.*",
|
|
103
103
|
"pytest-cov ==7.*",
|
|
104
104
|
"pytest-mock ==3.*",
|
|
105
|
-
"pytest-pep8 ==1.0.6",
|
|
105
|
+
# "pytest-pep8 ==1.0.6",
|
|
106
106
|
"pytest-flakes ==4.0.5",
|
|
107
107
|
"pytest-isort ==4.0.0",
|
|
108
|
-
"pytest-black ==0.6.0",
|
|
108
|
+
# "pytest-black ==0.6.0",
|
|
109
109
|
"pytest-variables[hjson,yaml,toml] ==3.1.0",
|
|
110
110
|
|
|
111
111
|
# -- testing-related libraries --
|
|
@@ -118,7 +118,7 @@ dev = [
|
|
|
118
118
|
"mypy ==2.*", # - typing library
|
|
119
119
|
"types-six ==1.*",
|
|
120
120
|
"types-mock ==5.*",
|
|
121
|
-
"types-paramiko ==
|
|
121
|
+
"types-paramiko ==5.*",
|
|
122
122
|
"types-requests ==2.*",
|
|
123
123
|
"types-markdown ==3.*",
|
|
124
124
|
"types-PyYAML ==6.*",
|
|
@@ -152,7 +152,7 @@ dev = [
|
|
|
152
152
|
|
|
153
153
|
# -- readline module depending on the platform
|
|
154
154
|
"pyreadline3 ==3.* ; sys_platform == 'win32' or sys_platform == 'cygwin'",
|
|
155
|
-
"gnureadline ==8
|
|
155
|
+
"gnureadline ==8.* ; sys_platform != 'win32'"
|
|
156
156
|
|
|
157
157
|
]
|
|
158
158
|
|
|
@@ -240,7 +240,7 @@ skips = ["B101", "B601"]
|
|
|
240
240
|
# -- mypy tool
|
|
241
241
|
[tool.mypy]
|
|
242
242
|
mypy_path = "./"
|
|
243
|
-
python_version = "3.
|
|
243
|
+
python_version = "3.14"
|
|
244
244
|
follow_imports = "silent"
|
|
245
245
|
strict_optional = true
|
|
246
246
|
warn_redundant_casts = true
|
|
@@ -23,7 +23,7 @@ CHECK_TYPE_CONTAINS = "contains"
|
|
|
23
23
|
CHECK_TYPES = (CHECK_TYPE_STARTS, CHECK_TYPE_ENDS, CHECK_TYPE_CONTAINS)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def check_str(args, check_type, source_str, test_str):
|
|
26
|
+
def check_str(args, check_type, source_str: str, test_str: str) -> bool:
|
|
27
27
|
"""
|
|
28
28
|
Check relation between string and test string, according to test type
|
|
29
29
|
:param check_type: type of matching
|
|
@@ -32,14 +32,16 @@ def check_str(args, check_type, source_str, test_str):
|
|
|
32
32
|
:return:
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
return source_str.startswith(test_str)
|
|
35
|
+
result: bool = False
|
|
37
36
|
|
|
38
|
-
if
|
|
39
|
-
|
|
37
|
+
if check_type == CHECK_TYPE_STARTS:
|
|
38
|
+
result = source_str.startswith(test_str)
|
|
39
|
+
elif args.edit_type == CHECK_TYPE_ENDS:
|
|
40
|
+
result = source_str.endswith(test_str)
|
|
41
|
+
elif args.edit_type == CHECK_TYPE_CONTAINS:
|
|
42
|
+
result = test_str in source_str
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
return test_str in source_str
|
|
44
|
+
return result
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
def fedit():
|
|
@@ -13,7 +13,7 @@ For more data - see docstring for Configuration class.
|
|
|
13
13
|
02.01.2025 Logging refactoring and minor improvements.
|
|
14
14
|
|
|
15
15
|
Created: Gusev Dmitrii, 2017
|
|
16
|
-
Modified: Gusev Dmitrii,
|
|
16
|
+
Modified: Gusev Dmitrii, 09.07.2026
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
import logging
|
|
@@ -182,10 +182,10 @@ class Configuration:
|
|
|
182
182
|
try:
|
|
183
183
|
result = self.__get_value(key, self.config_dict)
|
|
184
184
|
return result
|
|
185
|
-
except KeyError:
|
|
185
|
+
except KeyError as e:
|
|
186
186
|
if default is not None:
|
|
187
187
|
return default
|
|
188
|
-
raise ConfigError(f"Configuration entry [{key}] not found!")
|
|
188
|
+
raise ConfigError(f"Configuration entry [{key}] not found!") from e
|
|
189
189
|
|
|
190
190
|
def set(self, key, value):
|
|
191
191
|
"""Sets config value, creating all the nested levels if necessary
|
|
@@ -257,6 +257,7 @@ VALUES_COLUMN = 1
|
|
|
257
257
|
class ConfigurationXls(Configuration):
|
|
258
258
|
"""Extension for Configuration class for work with excel config files."""
|
|
259
259
|
|
|
260
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
260
261
|
def __init__(
|
|
261
262
|
self,
|
|
262
263
|
path_to_xls,
|
|
@@ -145,5 +145,50 @@ def file_size_str(file_path: str) -> str:
|
|
|
145
145
|
return "unknown"
|
|
146
146
|
|
|
147
147
|
|
|
148
|
+
def clear_directory_except_files(directory_path: str, files_to_keep: list[str], trace: bool = False) -> int:
|
|
149
|
+
"""Cleanup folder <directory_path>, keeping files by list <files_to_keep>. Subdirectories are
|
|
150
|
+
not affected - they won't be removed (files in the subdirectories won't be touched as well).
|
|
151
|
+
Usage:
|
|
152
|
+
clear_directory_except_files("example_folder", ["keep_files.txt", "another_file.txt"])
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
if not os.path.exists(directory_path) or not os.path.isdir(directory_path):
|
|
156
|
+
log.debug("Path %s doesn't exist or not a directory!", directory_path)
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
counter: int = 0
|
|
160
|
+
for filename in os.listdir(directory_path):
|
|
161
|
+
file_path = os.path.join(directory_path, filename)
|
|
162
|
+
if os.path.isfile(file_path) and filename not in files_to_keep:
|
|
163
|
+
try:
|
|
164
|
+
os.remove(file_path)
|
|
165
|
+
counter += 1
|
|
166
|
+
if trace:
|
|
167
|
+
log.debug("File %s deleted OK.", file_path)
|
|
168
|
+
except FileNotFoundError:
|
|
169
|
+
log.debug("File %s was already removed.", file_path)
|
|
170
|
+
except PermissionError:
|
|
171
|
+
log.error("Permission denied when trying to delete %s!", file_path)
|
|
172
|
+
except OSError as e:
|
|
173
|
+
log.exception("Error deleting file %s. Error: %s.", file_path, e)
|
|
174
|
+
|
|
175
|
+
return counter
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def clear_directory_except_files_recursive(directory_path, files_to_keep):
|
|
179
|
+
for root, _, files in os.walk(directory_path):
|
|
180
|
+
for filename in files:
|
|
181
|
+
file_path = os.path.join(root, filename)
|
|
182
|
+
if os.path.isfile(file_path) and filename not in files_to_keep:
|
|
183
|
+
try:
|
|
184
|
+
os.remove(file_path)
|
|
185
|
+
print(f"Файл {file_path} успешно удалён")
|
|
186
|
+
except Exception as e:
|
|
187
|
+
print(f"Ошибка при удалении {file_path}: {e}")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# Очистить папку `example_folder` от всех файлов, кроме `keep_files.txt` и `another_file.txt`, рекурсивно
|
|
191
|
+
# clear_directory_except_files_recursive("example_folder", ["keep_files.txt", "another_file.txt"])
|
|
192
|
+
|
|
148
193
|
if __name__ == "__main__":
|
|
149
194
|
print(MSG_MODULE_ISNT_RUNNABLE)
|
|
@@ -0,0 +1,263 @@
|
|
|
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, 07.07.2026
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import calendar
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime, timedelta, timezone
|
|
13
|
+
from functools import lru_cache
|
|
14
|
+
|
|
15
|
+
from dateutil.relativedelta import relativedelta
|
|
16
|
+
|
|
17
|
+
from pyutilities.defaults import MSG_MODULE_ISNT_RUNNABLE
|
|
18
|
+
|
|
19
|
+
# DATETIME :: timezones defaults
|
|
20
|
+
MSK_TIMEZONE_NAME: str = "Moscow TZ (GMT+3)"
|
|
21
|
+
MSK_TIMEZONE: timezone = timezone(timedelta(hours=3), name=MSK_TIMEZONE_NAME)
|
|
22
|
+
# - CACHE :: cache setup (size) for cached functions/methods
|
|
23
|
+
LRU_CACHE_SIZE: int = 128
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
log = logging.getLogger(__name__)
|
|
27
|
+
log.addHandler(logging.NullHandler())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
31
|
+
def shift_timestamp(
|
|
32
|
+
base_timestamp: datetime,
|
|
33
|
+
delta_years: int = 0,
|
|
34
|
+
delta_months: int = 0,
|
|
35
|
+
delta_days: float = 0,
|
|
36
|
+
delta_hours: float = 0,
|
|
37
|
+
delta_minutes: float = 0,
|
|
38
|
+
delta_seconds: float = 0,
|
|
39
|
+
trace: bool = False,
|
|
40
|
+
) -> datetime:
|
|
41
|
+
"""Shifts the provided timestamp by the specified delta (in years / months / days / hours / minutes
|
|
42
|
+
/ seconds). Return timestamp."""
|
|
43
|
+
|
|
44
|
+
if not base_timestamp: # fast-check/fail-fast
|
|
45
|
+
raise ValueError("Provided empty datetime value!")
|
|
46
|
+
|
|
47
|
+
# using function relativedelta() solving the issue of incorrect days (example: 31 февраля) and
|
|
48
|
+
# does it atomic for years/months
|
|
49
|
+
result: datetime
|
|
50
|
+
try:
|
|
51
|
+
# 1. Смещаем года и месяцы
|
|
52
|
+
result = base_timestamp + relativedelta(years=delta_years, months=delta_months)
|
|
53
|
+
|
|
54
|
+
# 2. Смещаем дробные/стандартные дни, часы, минуты и секунды
|
|
55
|
+
result += timedelta(days=delta_days, hours=delta_hours, minutes=delta_minutes, seconds=delta_seconds)
|
|
56
|
+
|
|
57
|
+
# 3. Нормализуем таймзону (важно для DST границ)
|
|
58
|
+
if result.tzinfo is not None:
|
|
59
|
+
result = result.astimezone(result.tzinfo)
|
|
60
|
+
|
|
61
|
+
except (ValueError, OverflowError) as e: # we won't 'silence' the issue
|
|
62
|
+
# Защита от выхода за пределы поддерживаемых дат Python (года 1-9999)
|
|
63
|
+
raise ValueError(f"Datetime value issue: {e}") from e
|
|
64
|
+
|
|
65
|
+
if trace:
|
|
66
|
+
log.debug("Generated from [%s] timestamp [%s].", base_timestamp, result)
|
|
67
|
+
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@lru_cache(maxsize=LRU_CACHE_SIZE)
|
|
72
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
73
|
+
def shift_timestampc(
|
|
74
|
+
base_timestamp: datetime,
|
|
75
|
+
delta_years: int = 0,
|
|
76
|
+
delta_months: int = 0,
|
|
77
|
+
delta_days: float = 0,
|
|
78
|
+
delta_hours: float = 0,
|
|
79
|
+
delta_minutes: float = 0,
|
|
80
|
+
delta_seconds: float = 0,
|
|
81
|
+
trace: bool = False,
|
|
82
|
+
) -> datetime:
|
|
83
|
+
"""CASHED version of the shift_timestamp() method. Shifts the provided timestamp by the specified
|
|
84
|
+
delta (in years / months / days / hours / minutes / seconds). Return timestamp."""
|
|
85
|
+
|
|
86
|
+
return shift_timestamp(
|
|
87
|
+
base_timestamp,
|
|
88
|
+
delta_years,
|
|
89
|
+
delta_months,
|
|
90
|
+
delta_days,
|
|
91
|
+
delta_hours,
|
|
92
|
+
delta_minutes,
|
|
93
|
+
delta_seconds,
|
|
94
|
+
trace,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
99
|
+
def get_shifted_timestamp(
|
|
100
|
+
current_timezone: timezone = MSK_TIMEZONE,
|
|
101
|
+
delta_years: int = 0,
|
|
102
|
+
delta_months: int = 0,
|
|
103
|
+
delta_days: float = 0,
|
|
104
|
+
delta_hours: float = 0,
|
|
105
|
+
delta_minutes: float = 0,
|
|
106
|
+
delta_seconds: float = 0,
|
|
107
|
+
trace: bool = False,
|
|
108
|
+
) -> datetime:
|
|
109
|
+
"""Return the current timestamp with delta: now (in the default timezone) +/- the specified time delta
|
|
110
|
+
in years / months / days / hours / minutes / seconds. Default timezone can be changed by the function
|
|
111
|
+
argument."""
|
|
112
|
+
|
|
113
|
+
# generate the current timestamp
|
|
114
|
+
current_timestamp: datetime = datetime.now(current_timezone) if current_timezone else datetime.now()
|
|
115
|
+
|
|
116
|
+
return shift_timestamp(
|
|
117
|
+
current_timestamp,
|
|
118
|
+
delta_years,
|
|
119
|
+
delta_months,
|
|
120
|
+
delta_days,
|
|
121
|
+
delta_hours,
|
|
122
|
+
delta_minutes,
|
|
123
|
+
delta_seconds,
|
|
124
|
+
trace,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@lru_cache(maxsize=LRU_CACHE_SIZE)
|
|
129
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
130
|
+
def get_shifted_timestampc(
|
|
131
|
+
current_timezone: timezone = MSK_TIMEZONE,
|
|
132
|
+
delta_years: int = 0,
|
|
133
|
+
delta_months: int = 0,
|
|
134
|
+
delta_days: float = 0,
|
|
135
|
+
delta_hours: float = 0,
|
|
136
|
+
delta_minutes: float = 0,
|
|
137
|
+
delta_seconds: float = 0,
|
|
138
|
+
trace: bool = False,
|
|
139
|
+
) -> datetime:
|
|
140
|
+
"""CACHED. Cached version of the get_timestamp() method."""
|
|
141
|
+
|
|
142
|
+
return get_shifted_timestamp(
|
|
143
|
+
current_timezone,
|
|
144
|
+
delta_years,
|
|
145
|
+
delta_months,
|
|
146
|
+
delta_days,
|
|
147
|
+
delta_hours,
|
|
148
|
+
delta_minutes,
|
|
149
|
+
delta_seconds,
|
|
150
|
+
trace,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_str_month(
|
|
155
|
+
dt: datetime, month_number_prefix: bool = True, year_number_postfix: bool = False, delimiter: str = "."
|
|
156
|
+
) -> str:
|
|
157
|
+
"""Return string name of the month with variations, for utility usage (folders names etc.)"""
|
|
158
|
+
|
|
159
|
+
prefix: str = ""
|
|
160
|
+
postfix: str = ""
|
|
161
|
+
if month_number_prefix:
|
|
162
|
+
prefix = f"{dt.month:02d}{delimiter}"
|
|
163
|
+
if year_number_postfix:
|
|
164
|
+
postfix = f"{delimiter}{dt.year}"
|
|
165
|
+
|
|
166
|
+
return f"{prefix}{calendar.month_name[dt.month]}{postfix}"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_str_current_month() -> str:
|
|
170
|
+
return get_str_month(datetime.now(tz=MSK_TIMEZONE))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def get_str_prev_month() -> str:
|
|
174
|
+
return get_str_month(get_shifted_timestamp(delta_months=-1))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def get_str_next_month() -> str:
|
|
178
|
+
return get_str_month(get_shifted_timestamp(delta_months=1))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def get_dates_range_before_date(date: datetime, tzinfo: timezone) -> tuple[int, int]:
|
|
182
|
+
"""Generates two dates range - date_from/date_to for the time period 'before today' - from the 1st day
|
|
183
|
+
of the current month till the today - 1 day (current month). In case today is 1st day - range for the
|
|
184
|
+
whole previous month, if today is 01.01.XXXX - range for 01.12.XXXX-1 - 31.12.XXXX-1.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
if not isinstance(date, datetime):
|
|
188
|
+
raise ValueError("The 'date' argument must be a valid datetime object.")
|
|
189
|
+
|
|
190
|
+
log.debug("get_dates_range_before_date(): the date [%s].", date)
|
|
191
|
+
|
|
192
|
+
tstamp_from: int = 0
|
|
193
|
+
tstamp_to: int = 0
|
|
194
|
+
|
|
195
|
+
# - processing and calculating dates
|
|
196
|
+
if date.day == 1: # today - 1st day of month, we need report for the previous month
|
|
197
|
+
|
|
198
|
+
if date.month == 1: # we are at 01.01.XXXX - we need report for Dec.XXXX - 1
|
|
199
|
+
date_from = datetime(date.year - 1, 12, 1, tzinfo=tzinfo) # from: 01 Dec Year-1 (prev. year)
|
|
200
|
+
date_to = datetime(date.year - 1, 12, 31, tzinfo=tzinfo) # to: 31 Dec Year-1 (prev. year)
|
|
201
|
+
|
|
202
|
+
else: # we are at 01.XX.XXXX - we need report for previous month
|
|
203
|
+
date_from = datetime(date.year, date.month - 1, 1, tzinfo=tzinfo) # from: 01 Month-1 Year
|
|
204
|
+
_, last_day = calendar.monthrange(date.year, date.month - 1) # last day of the previous month
|
|
205
|
+
date_to = datetime(date.year, date.month - 1, last_day, tzinfo=tzinfo) # to: LastDay Month-1 Year
|
|
206
|
+
|
|
207
|
+
else: # today isn't the 1st, we need report from 1st till yesterday
|
|
208
|
+
|
|
209
|
+
date_from = datetime(date.year, date.month, 1, tzinfo=tzinfo) # from: 01 Month Year
|
|
210
|
+
date_to = datetime(date.year, date.month, date.day - 1, tzinfo=tzinfo) # to: Day-1 Month Year
|
|
211
|
+
|
|
212
|
+
# - final date/time - date from: XX.XX.XX 00:00:00 - date to: XX.XX.XXXX 23:59:59
|
|
213
|
+
datetime_from = date_from.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
214
|
+
datetime_to = date_to.replace(hour=23, minute=59, second=59, microsecond=0)
|
|
215
|
+
# timestamps from datetime objects
|
|
216
|
+
tstamp_from = int(datetime_from.timestamp())
|
|
217
|
+
tstamp_to = int(datetime_to.timestamp())
|
|
218
|
+
|
|
219
|
+
log.debug("Timestamps for [%s]: from [%s], to [%s].", date, tstamp_from, tstamp_to)
|
|
220
|
+
|
|
221
|
+
# - return the tuple of values
|
|
222
|
+
return (tstamp_from, tstamp_to)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@lru_cache(maxsize=LRU_CACHE_SIZE)
|
|
226
|
+
def get_dates_range_before_datec(date: datetime, tzinfo: timezone) -> tuple[int, int]:
|
|
227
|
+
"""CACHED. Cached version of the get_dates_range_before_date()."""
|
|
228
|
+
|
|
229
|
+
return get_dates_range_before_date(date, tzinfo)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def human_readable_duration_seconds(duration_seconds):
|
|
233
|
+
"""Convert duration in seconds into human-readable string-duration."""
|
|
234
|
+
|
|
235
|
+
days = duration_seconds // (24 * 3600)
|
|
236
|
+
hours = (duration_seconds % (24 * 3600)) // 3600
|
|
237
|
+
minutes = (duration_seconds % (24 * 3600)) % 3600 // 60
|
|
238
|
+
seconds = duration_seconds % 3600 % 60
|
|
239
|
+
|
|
240
|
+
result = ""
|
|
241
|
+
if days > 0:
|
|
242
|
+
result += f"{days} д. "
|
|
243
|
+
if hours > 0:
|
|
244
|
+
result += f"{hours} ч. "
|
|
245
|
+
if minutes > 0:
|
|
246
|
+
result += f"{minutes:02d} мин. "
|
|
247
|
+
result += f"{seconds:02d} сек."
|
|
248
|
+
result = result.strip()
|
|
249
|
+
|
|
250
|
+
log.debug("Converted duration: [%s] into readable str: [%s].", duration_seconds, result)
|
|
251
|
+
|
|
252
|
+
return result
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@lru_cache(maxsize=LRU_CACHE_SIZE)
|
|
256
|
+
def human_readable_duration_secondsc(duration_seconds):
|
|
257
|
+
"""CACHED. Cached version of the human_readable_duration_seconds() method."""
|
|
258
|
+
|
|
259
|
+
return human_readable_duration_seconds(duration_seconds)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
if __name__ == "__main__":
|
|
263
|
+
print(MSG_MODULE_ISNT_RUNNABLE)
|
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Useful Excel utilities for improving excel working in python.
|
|
5
|
+
|
|
6
|
+
Created: Dmitrii Gusev, 24.06.2026
|
|
7
|
+
Modified: Dmitrii Gusev, 24.06.2026
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from openpyxl.cell.cell import Cell
|
|
11
|
+
from openpyxl.styles import Alignment, Font # type: ignore[attr-defined]
|
|
12
|
+
from openpyxl.utils import get_column_letter # type: ignore[attr-defined]
|
|
13
|
+
from openpyxl.worksheet.worksheet import Worksheet
|
|
14
|
+
|
|
15
|
+
from pyutilities.utils.string_utils import is_empty
|
|
16
|
+
|
|
17
|
+
# - styling constants/defaults
|
|
18
|
+
FONT: Font = Font()
|
|
19
|
+
FONT_RED: Font = Font(color="FF0000") # red font
|
|
20
|
+
FONT_GREEN: Font = Font(color="00BB00") # green font
|
|
21
|
+
FONT_BOLD: Font = Font(bold=True)
|
|
22
|
+
FONT_BOLD_RED: Font = Font(bold=True, color="FF0000") # red font, bold
|
|
23
|
+
FONT_BOLD_GREEN: Font = Font(bold=True, color="00BB00") # green font, bold
|
|
24
|
+
ALIGNMENT_CENTER: Alignment = Alignment(horizontal="center", vertical="center", wrapText=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class XlsSheet:
|
|
28
|
+
"""Utility class for working with openpyxl excel worksheet."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, ws: Worksheet):
|
|
31
|
+
self.__ws = ws
|
|
32
|
+
|
|
33
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
34
|
+
def init_cell(
|
|
35
|
+
self,
|
|
36
|
+
address: str,
|
|
37
|
+
value: str,
|
|
38
|
+
bold: bool = False,
|
|
39
|
+
aligned: bool = False,
|
|
40
|
+
color: str | None = None,
|
|
41
|
+
width: int = 0,
|
|
42
|
+
) -> Cell:
|
|
43
|
+
"""Simple yet powerful method for cell initialization. Can init essential cell parameters."""
|
|
44
|
+
|
|
45
|
+
cell: Cell = self.__ws[address]
|
|
46
|
+
cell.value = value
|
|
47
|
+
|
|
48
|
+
if bold: # font boldness
|
|
49
|
+
cell.font = FONT_BOLD
|
|
50
|
+
|
|
51
|
+
if aligned: # cell text alignment
|
|
52
|
+
cell.alignment = ALIGNMENT_CENTER
|
|
53
|
+
|
|
54
|
+
if not is_empty(color): # cell font coloring/boldness
|
|
55
|
+
if color == "red":
|
|
56
|
+
if bold:
|
|
57
|
+
cell.font = FONT_BOLD_RED
|
|
58
|
+
else:
|
|
59
|
+
cell.font = FONT_RED
|
|
60
|
+
elif color == "green":
|
|
61
|
+
if bold:
|
|
62
|
+
cell.font = FONT_BOLD_GREEN
|
|
63
|
+
else:
|
|
64
|
+
cell.font = FONT_GREEN
|
|
65
|
+
|
|
66
|
+
if width > 0: # column width
|
|
67
|
+
self.__ws.column_dimensions[cell.column_letter].width = width
|
|
68
|
+
|
|
69
|
+
return cell
|
|
70
|
+
|
|
71
|
+
def init_cell_bold(
|
|
72
|
+
self, address, value, aligned: bool = False, color: str | None = None, width: int = 0
|
|
73
|
+
) -> Cell:
|
|
74
|
+
"""TBD"""
|
|
75
|
+
return self.init_cell(
|
|
76
|
+
address=address, value=value, bold=True, aligned=aligned, color=color, width=width
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
|
80
|
+
def init_cell_addr(
|
|
81
|
+
self,
|
|
82
|
+
row: int,
|
|
83
|
+
column: int,
|
|
84
|
+
value: str,
|
|
85
|
+
bold: bool = False,
|
|
86
|
+
aligned: bool = False,
|
|
87
|
+
color: str | None = None,
|
|
88
|
+
width: int = 0,
|
|
89
|
+
) -> Cell:
|
|
90
|
+
"""TBD"""
|
|
91
|
+
|
|
92
|
+
letter: str = get_column_letter(column)
|
|
93
|
+
return self.init_cell(
|
|
94
|
+
address=f"{letter}{row}", value=value, bold=bold, aligned=aligned, color=color, width=width
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def set_column_width(self, column: str, width: int):
|
|
98
|
+
"""TBD"""
|
|
99
|
+
self.__ws.column_dimensions[column].width = width
|
|
@@ -1,148 +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, 25.05.2026
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import calendar
|
|
11
|
-
import logging
|
|
12
|
-
from datetime import datetime, timedelta, timezone
|
|
13
|
-
from functools import lru_cache
|
|
14
|
-
|
|
15
|
-
from pyutilities.defaults import MSG_MODULE_ISNT_RUNNABLE
|
|
16
|
-
|
|
17
|
-
# DATETIME :: timezones defaults
|
|
18
|
-
MSK_TIMEZONE: timezone = timezone(timedelta(hours=3), name="Moscow Timezone (GMT+3)")
|
|
19
|
-
MSK_TIMEZONE_NAME: str = "Europe/Moscow"
|
|
20
|
-
# - CACHE :: cache setup (size) for cached functions/methods
|
|
21
|
-
LRU_CACHE_SIZE: int = 128
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
log = logging.getLogger(__name__)
|
|
25
|
-
log.addHandler(logging.NullHandler())
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def get_timestamp(
|
|
29
|
-
current_timezone: timezone = MSK_TIMEZONE,
|
|
30
|
-
days: float = 0,
|
|
31
|
-
hours: float = 0,
|
|
32
|
-
minutes: float = 0,
|
|
33
|
-
seconds: float = 0,
|
|
34
|
-
) -> datetime:
|
|
35
|
-
"""Return timestamp: now (in the default timezone) +/- the specified time delta in hours / minutes /
|
|
36
|
-
seconds. Default timezone can be changed by the function argument."""
|
|
37
|
-
|
|
38
|
-
return datetime.now(current_timezone) + timedelta(
|
|
39
|
-
days=days, hours=hours, minutes=minutes, seconds=seconds
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@lru_cache(maxsize=LRU_CACHE_SIZE)
|
|
44
|
-
def get_timestampc(
|
|
45
|
-
current_timezone: timezone = MSK_TIMEZONE,
|
|
46
|
-
days: float = 0,
|
|
47
|
-
hours: float = 0,
|
|
48
|
-
minutes: float = 0,
|
|
49
|
-
seconds: float = 0,
|
|
50
|
-
) -> datetime:
|
|
51
|
-
"""CACHED. Cached version of the get_timestamp() method."""
|
|
52
|
-
|
|
53
|
-
return get_timestamp(current_timezone, days, hours, minutes, seconds)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def get_dates_range_before_date(date: datetime, tzinfo: timezone) -> tuple[int, int]:
|
|
57
|
-
"""Generates two dates range - date_from/date_to for the time period 'before today' - from the 1st day
|
|
58
|
-
of the current month till the today - 1 day (current month). In case today is 1st day - range for the
|
|
59
|
-
whole previous month, if today is 01.01.XXXX - range for 01.12.XXXX-1 - 31.12.XXXX-1.
|
|
60
|
-
"""
|
|
61
|
-
|
|
62
|
-
log.debug("get_dates_range_before_date(): the date [%s].", date)
|
|
63
|
-
|
|
64
|
-
tstamp_from: int = 0
|
|
65
|
-
tstamp_to: int = 0
|
|
66
|
-
|
|
67
|
-
if date: # if date is OK (not empty/None)
|
|
68
|
-
|
|
69
|
-
# - processing and calculating dates
|
|
70
|
-
if date.day == 1: # today - 1st day of month, we need report for the previous month
|
|
71
|
-
|
|
72
|
-
if date.month == 1: # we are at 01.01.XXXX - we need report for Dec.XXXX - 1
|
|
73
|
-
|
|
74
|
-
date_from = datetime(date.year - 1, 12, 1, tzinfo=tzinfo) # from: 01 Dec Year-1 (prev. year)
|
|
75
|
-
date_to = datetime(date.year - 1, 12, 31, tzinfo=tzinfo) # to: 31 Dec Year-1 (prev. year)
|
|
76
|
-
|
|
77
|
-
else: # we are at 01.XX.XXXX - we need report for previous month
|
|
78
|
-
|
|
79
|
-
# from: 01 Month-1 Year
|
|
80
|
-
date_from = datetime(date.year, date.month - 1, 1, tzinfo=tzinfo)
|
|
81
|
-
# last day of the previous month
|
|
82
|
-
_, last_day = calendar.monthrange(date.year, date.month - 1)
|
|
83
|
-
# to: Last Day Month-1 Year
|
|
84
|
-
date_to = datetime(date.year, date.month - 1, last_day, tzinfo=tzinfo)
|
|
85
|
-
|
|
86
|
-
else: # today isn't the 1st, we need report from 1st till yesterday
|
|
87
|
-
|
|
88
|
-
date_from = datetime(date.year, date.month, 1, tzinfo=tzinfo) # from: 01 Month Year
|
|
89
|
-
date_to = datetime(date.year, date.month, date.day - 1, tzinfo=tzinfo) # to: Day-1 Month Year
|
|
90
|
-
|
|
91
|
-
# - final date/time - date from: XX.XX.XX 00:00:00
|
|
92
|
-
tstamp_from = int(
|
|
93
|
-
datetime(
|
|
94
|
-
date_from.year, date_from.month, date_from.day, hour=0, minute=0, second=0, tzinfo=tzinfo
|
|
95
|
-
).timestamp()
|
|
96
|
-
)
|
|
97
|
-
# - final date/time - # date to: XX.XX.XXXX 23:59:59
|
|
98
|
-
tstamp_to = int(
|
|
99
|
-
datetime(
|
|
100
|
-
date_to.year, date_to.month, date_to.day, hour=23, minute=59, second=59, tzinfo=tzinfo
|
|
101
|
-
).timestamp()
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
log.debug("Timestamps for [%s]: from [%s], to [%s].", date, tstamp_from, tstamp_to)
|
|
105
|
-
|
|
106
|
-
# - return the tuple of values
|
|
107
|
-
return (tstamp_from, tstamp_to)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
@lru_cache(maxsize=LRU_CACHE_SIZE)
|
|
111
|
-
def get_dates_range_before_datec(date: datetime, tzinfo: timezone) -> tuple[int, int]:
|
|
112
|
-
"""CACHED. Cached version of the get_dates_range_before_date()."""
|
|
113
|
-
|
|
114
|
-
return get_dates_range_before_date(date, tzinfo)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def human_readable_duration_seconds(duration_seconds):
|
|
118
|
-
"""Convert duration in seconds into human-readable string-duration."""
|
|
119
|
-
|
|
120
|
-
days = duration_seconds // (24 * 3600)
|
|
121
|
-
hours = (duration_seconds % (24 * 3600)) // 3600
|
|
122
|
-
minutes = (duration_seconds % (24 * 3600)) % 3600 // 60
|
|
123
|
-
seconds = duration_seconds % 3600 % 60
|
|
124
|
-
|
|
125
|
-
result = ""
|
|
126
|
-
if days > 0:
|
|
127
|
-
result += f"{days} д. "
|
|
128
|
-
if hours > 0:
|
|
129
|
-
result += f"{hours} ч. "
|
|
130
|
-
if minutes > 0:
|
|
131
|
-
result += f"{minutes:02d} мин. "
|
|
132
|
-
result += f"{seconds:02d} сек."
|
|
133
|
-
result = result.strip()
|
|
134
|
-
|
|
135
|
-
log.debug("Converted duration: [%s] into readable str: [%s].", duration_seconds, result)
|
|
136
|
-
|
|
137
|
-
return result
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
@lru_cache(maxsize=LRU_CACHE_SIZE)
|
|
141
|
-
def human_readable_duration_secondsc(duration_seconds):
|
|
142
|
-
"""CACHED. Cached version of the human_readable_duration_seconds() method."""
|
|
143
|
-
|
|
144
|
-
return human_readable_duration_seconds(duration_seconds)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if __name__ == "__main__":
|
|
148
|
-
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
|
|
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
|
|
File without changes
|
|
File without changes
|