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.
Files changed (31) hide show
  1. {pyutilities-2.5.2 → pyutilities-2.7.0}/PKG-INFO +4 -4
  2. {pyutilities-2.5.2 → pyutilities-2.7.0}/pyproject.toml +11 -11
  3. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/cli/fedit.py +9 -7
  4. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/config/configuration.py +4 -3
  5. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/io/io_utils.py +45 -0
  6. pyutilities-2.7.0/src/pyutilities/utils/datetime_utils.py +263 -0
  7. pyutilities-2.7.0/src/pyutilities/xls/__init__.py +0 -0
  8. pyutilities-2.7.0/src/pyutilities/xls/xls_utils.py +99 -0
  9. pyutilities-2.5.2/src/pyutilities/utils/datetime_utils.py +0 -148
  10. {pyutilities-2.5.2 → pyutilities-2.7.0}/LICENSE +0 -0
  11. {pyutilities-2.5.2 → pyutilities-2.7.0}/README.md +0 -0
  12. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/__init__.py +0 -0
  13. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/cli/__init__.py +0 -0
  14. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/cli/pyutils.py +0 -0
  15. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/config/__init__.py +0 -0
  16. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/defaults.py +0 -0
  17. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/exception.py +0 -0
  18. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/io/__init__.py +0 -0
  19. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/log/__init__.py +0 -0
  20. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/log/log_manager.py +0 -0
  21. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/logging.py +0 -0
  22. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/sys/__init__.py +0 -0
  23. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/sys/sys_utils.py +0 -0
  24. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/utils/__init__.py +0 -0
  25. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/utils/common_utils.py +0 -0
  26. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/utils/decorators.py +0 -0
  27. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/utils/lambdas.py +0 -0
  28. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/utils/string_utils.py +0 -0
  29. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/utils/structures.py +0 -0
  30. {pyutilities-2.5.2 → pyutilities-2.7.0}/src/pyutilities/web/__init__.py +0 -0
  31. {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.5.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 (==3.1.5)
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.1,<2.1.0)
48
+ Requires-Dist: xlrd (>=2.0,<3.0)
49
49
  Requires-Dist: xlwings (>=0.35,<1.0)
50
- Requires-Dist: xlwt (==1.3.0)
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, 27.05.2026
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.5.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.1", # - allowed: >=2.0.1 <2.1.0
60
- "xlwt==1.3.0",
61
- "xlwings~=0.35",
62
- "openpyxl==3.1.5",
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 ==4.*",
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.2.13 ; sys_platform != 'win32'"
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.10"
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
- if check_type == CHECK_TYPE_STARTS:
36
- return source_str.startswith(test_str)
35
+ result: bool = False
37
36
 
38
- if args.edit_type == CHECK_TYPE_ENDS:
39
- return source_str.endswith(test_str)
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
- if args.edit_type == CHECK_TYPE_CONTAINS:
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, 02.01.2025
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