yumako 0.1.7__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.
yumako-0.1.7/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Nanw1103
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
yumako-0.1.7/PKG-INFO ADDED
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.3
2
+ Name: yumako
3
+ Version: 0.1.7
4
+ Summary:
5
+ Home-page: https://github.com/nanw1103/yumako
6
+ License: MIT
7
+ Keywords: util,vanilla
8
+ Author: Nanw1103
9
+ Requires-Python: >=3.9,<4.0
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Project-URL: Documentation, https://github.com/nanw1103/amyumakober#readme
20
+ Project-URL: Repository, https://github.com/nanw1103/yumako
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Yumako ![Yumako](doc/yumako.png)
24
+
25
+ Vanilla python utilities
26
+
27
+
28
+
29
+
30
+
yumako-0.1.7/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # Yumako ![Yumako](doc/yumako.png)
2
+
3
+ Vanilla python utilities
4
+
5
+
6
+
7
+
@@ -0,0 +1,88 @@
1
+ [tool.poetry]
2
+ name = "yumako"
3
+ version = "0.1.7"
4
+ description = ""
5
+ authors = ["Nanw1103"]
6
+ readme = "README.md"
7
+ license = "MIT"
8
+ homepage = "https://github.com/nanw1103/yumako"
9
+ repository = "https://github.com/nanw1103/yumako"
10
+ documentation = "https://github.com/nanw1103/amyumakober#readme"
11
+ keywords = ["util", "vanilla"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3.9",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ ]
22
+
23
+ packages = [
24
+ { include = "yumako", from = "src", format = ["sdist"] }
25
+ ]
26
+
27
+ [tool.poetry.dependencies]
28
+ python = "^3.9"
29
+
30
+ [tool.poetry.group.dev.dependencies]
31
+ pytest = "^7.4.3"
32
+ black = "^23.11.0"
33
+ isort = "^5.12.0"
34
+ mypy = "^1.7.1"
35
+ ruff = "^0.1.6"
36
+ tomlkit = "^0.12.0"
37
+
38
+ [build-system]
39
+ requires = ["poetry-core"]
40
+ build-backend = "poetry.core.masonry.api"
41
+
42
+
43
+ [tool.black]
44
+ line-length = 120
45
+ target-version = ['py39']
46
+ include = '\.pyi?$'
47
+ extend-exclude = '''
48
+ # A regex preceded with ^/ will apply only to files and directories
49
+ # in the root of the project.
50
+ ^/.venv
51
+ '''
52
+
53
+ [tool.isort]
54
+ profile = "black"
55
+ multi_line_output = 3
56
+ line_length = 120
57
+ skip = [".venv"]
58
+ src_paths = ["src", "tests"]
59
+
60
+ [tool.mypy]
61
+ python_version = "3.9"
62
+ warn_return_any = true
63
+ warn_unused_configs = true
64
+ disallow_untyped_defs = true
65
+ check_untyped_defs = true
66
+ exclude = [
67
+ ".venv"
68
+ ]
69
+
70
+ [tool.ruff]
71
+ line-length = 120
72
+ target-version = "py39"
73
+ select = ["E", "F", "I", "N", "W", "B", "UP"]
74
+ exclude = [
75
+ ".venv",
76
+ "__init__.py"
77
+ ]
78
+ src = ["src", "tests"]
79
+
80
+ [tool.pytest.ini_options]
81
+ testpaths = ["tests"]
82
+ python_files = ["test_*.py"]
83
+ addopts = "-ra -q"
84
+ filterwarnings = [
85
+ "error",
86
+ "ignore::DeprecationWarning",
87
+ "ignore::UserWarning",
88
+ ]
@@ -0,0 +1,34 @@
1
+ """
2
+ Yumako - Vanilla python utilities.
3
+ """
4
+
5
+ __version__ = "0.1.6"
6
+
7
+ import importlib
8
+ from types import ModuleType
9
+ from typing import TYPE_CHECKING
10
+
11
+ __submodules = [
12
+ "template",
13
+ "time",
14
+ ]
15
+ __submodule_map = {name: f"yumako.{name}" for name in __submodules}
16
+
17
+ if TYPE_CHECKING:
18
+ from . import template # type: ignore
19
+ from . import time # type: ignore
20
+
21
+
22
+ def __getattr__(name: str) -> ModuleType:
23
+ if name in __submodule_map:
24
+ submodule = importlib.import_module(__submodule_map[name])
25
+ globals()[name] = submodule
26
+ return submodule
27
+ raise AttributeError(f"Module {__name__!r} has no attribute {name!r}")
28
+
29
+
30
+ __all__ = __submodules
31
+
32
+
33
+ def __dir__() -> list[str]:
34
+ return __all__
@@ -0,0 +1,23 @@
1
+ import re
2
+
3
+
4
+ def replace(
5
+ text: str, mapping: dict, strict_on_unresolved_vars: bool = True, strict_on_unused_var: bool = False
6
+ ) -> str:
7
+ unused_vars = set(mapping.keys())
8
+ for k, v in mapping.items():
9
+ new_text = text.replace("{{" + k + "}}", str(v))
10
+ if new_text != text:
11
+ text = new_text
12
+ unused_vars.remove(k)
13
+
14
+ if strict_on_unresolved_vars:
15
+ unresolved_vars = re.findall(r"\{\{([^}]+)\}\}", text)
16
+ if unresolved_vars:
17
+ raise Exception(f"Strict mode: template variables unresolved: {unresolved_vars}")
18
+
19
+ if strict_on_unused_var:
20
+ if unused_vars:
21
+ raise Exception(f"Strict mode: var specified but not in template: {unused_vars}")
22
+
23
+ return text
@@ -0,0 +1,353 @@
1
+ import re
2
+ from datetime import datetime, timedelta, timezone
3
+ from typing import Union
4
+
5
+
6
+ def display(d: timedelta, use_double_digits: bool = False) -> str:
7
+ """
8
+ Format a timedelta into a human-readable string.
9
+
10
+ Args:
11
+ d: The timedelta to format
12
+ use_double_digits: If True, pad numbers with leading zeros
13
+
14
+ Returns:
15
+ A formatted string like "1y2w" or "01y02w" depending on use_double_digits
16
+ """
17
+ is_negative = d.total_seconds() < 0
18
+ if is_negative:
19
+ d = abs(d)
20
+
21
+ total_days = d.days
22
+
23
+ # Only use years if we have exactly 365 days or more
24
+ years = total_days // 365 if total_days >= 365 else 0
25
+ remaining_days = total_days % 365 if years > 0 else total_days
26
+
27
+ weeks = remaining_days // 7
28
+ days = remaining_days % 7
29
+
30
+ hours = d.seconds // 3600
31
+ remaining = d.seconds % 3600
32
+ minutes = remaining // 60
33
+ seconds = remaining % 60
34
+
35
+ parts = []
36
+ if years > 0:
37
+ parts.append((years, "y"))
38
+ if weeks > 0:
39
+ parts.append((weeks, "w"))
40
+ if days > 0:
41
+ parts.append((days, "d"))
42
+ if hours > 0:
43
+ parts.append((hours, "h"))
44
+ if minutes > 0:
45
+ parts.append((minutes, "m"))
46
+ if seconds > 0:
47
+ parts.append((seconds, "s"))
48
+
49
+ if len(parts) > 2:
50
+ parts = parts[:2]
51
+ elif len(parts) == 0:
52
+ return "00s" if use_double_digits else "0s"
53
+
54
+ ret = "-" if is_negative else ""
55
+ for part in parts:
56
+ if use_double_digits:
57
+ ret += f"{part[0]:02d}{part[1]}"
58
+ else:
59
+ ret += f"{part[0]}{part[1]}"
60
+ return ret
61
+
62
+
63
+ def _parse_iso_with_colon_offset(x: str) -> datetime:
64
+ if abs(int(x[-5:-3])) > 14:
65
+ raise ValueError("Invalid timezone offset")
66
+ return datetime.strptime(
67
+ f"{x[:-6]}{x[-6:].replace(':','')}",
68
+ "%Y-%m-%dT%H:%M:%S.%f%z" if "." in x else "%Y-%m-%dT%H:%M:%S%z",
69
+ )
70
+
71
+
72
+ def _parse_iso_with_colon_offset2(x: str) -> datetime:
73
+ if abs(int(x[-4:-2])) > 14:
74
+ raise ValueError("Invalid timezone offset")
75
+ return datetime.strptime(x, "%Y-%m-%dT%H:%M:%S.%f%z" if "." in x else "%Y-%m-%dT%H:%M:%S%z")
76
+
77
+
78
+ # Try parsing common formats
79
+ _all_human_time_formats = [
80
+ # Unix timestamps
81
+ (r"^\d{13}$", lambda x: datetime.fromtimestamp(int(x) / 1000, tz=timezone.utc)),
82
+ (r"^\d{10}$", lambda x: datetime.fromtimestamp(int(x), tz=timezone.utc)),
83
+ (r"^\d+\.\d+$", lambda x: datetime.fromtimestamp(float(x), tz=timezone.utc)),
84
+ # ISO formats
85
+ (r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$", "%Y-%m-%dT%H:%M:%S.%fZ"),
86
+ (r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$", "%Y-%m-%dT%H:%M:%SZ"),
87
+ (r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$", "%Y-%m-%dT%H:%M:%S"),
88
+ (r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$", "%Y-%m-%d %H:%M:%S"),
89
+ (
90
+ r"^\d{2}:\d{2}:\d{2}Z$",
91
+ lambda x: datetime.combine(
92
+ datetime.now(timezone.utc).date(),
93
+ datetime.strptime(x, "%H:%M:%SZ").time(),
94
+ tzinfo=timezone.utc,
95
+ ),
96
+ ),
97
+ (r"^\d{4}-\d{2}-\d{2}$", "%Y-%m-%d"),
98
+ (
99
+ r"^\d{2}:\d{2}$",
100
+ lambda x: datetime.combine(
101
+ datetime.now(timezone.utc).date(),
102
+ datetime.strptime(x, "%H:%M").time(),
103
+ tzinfo=timezone.utc,
104
+ ),
105
+ ),
106
+ # Non-standard formats
107
+ (r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+$", "%Y-%m-%d %H:%M:%S.%f"),
108
+ (r"^\d{2}/\d{2}/\d{4}$", "%m/%d/%Y"),
109
+ (r"^\d{4}\.\d{2}\.\d{2}$", "%Y.%m.%d"),
110
+ (r"^\d{8}$", "%Y%m%d"),
111
+ (r"^\d{4}/\d{2}/\d{2}$", "%Y/%m/%d"),
112
+ # RFC 2822
113
+ (
114
+ r"^[A-Z]{3}, \d{2} [A-Z]{3} \d{4} \d{2}:\d{2}:\d{2} [+-]\d{4}$",
115
+ "%a, %d %b %Y %H:%M:%S %z",
116
+ ),
117
+ # ISO with timezone offset (with colon)
118
+ (
119
+ r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{2}:\d{2}$",
120
+ _parse_iso_with_colon_offset,
121
+ ),
122
+ # ISO with timezone offset (without colon)
123
+ (
124
+ r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?[+-]\d{4}$",
125
+ _parse_iso_with_colon_offset2,
126
+ ),
127
+ # European date formats
128
+ (r"^\d{2}-\d{2}-\d{4}$", ["%d-%m-%Y", "%m-%d-%Y"]),
129
+ (r"^\d{2}/\d{2}/\d{4}$", ["%d/%m/%Y", "%m/%d/%Y"]),
130
+ (r"^\d{2}\.\d{2}\.\d{4}$", ["%d.%m.%Y", "%m.%d.%Y"]),
131
+ # Additional common formats
132
+ (r"^[A-Z]{3} \d{1,2}, \d{4}$", "%b %d, %Y"), # DEC 4, 2023
133
+ (r"^[A-Z]{3} \d{1,2} \d{4}$", "%b %d %Y"), # DEC 4 2023
134
+ (r"^\d{1,2} [A-Z]{3} \d{4}$", "%d %b %Y"), # 4 DEC 2023
135
+ (r"^[A-Z]{6,9} \d{1,2}, \d{4}$", "%B %d, %Y"), # DECEMBER 4, 2023
136
+ # Time only with Z
137
+ (
138
+ r"^\d{2}:\d{2}:\d{2}Z$",
139
+ lambda x: datetime.combine(
140
+ datetime.now(timezone.utc).date(),
141
+ datetime.strptime(x, "%H:%M:%SZ").time(),
142
+ tzinfo=timezone.utc,
143
+ ),
144
+ ),
145
+ # Time only without Z
146
+ (
147
+ r"^\d{2}:\d{2}:\d{2}$",
148
+ lambda x: datetime.combine(
149
+ datetime.now(timezone.utc).date(),
150
+ datetime.strptime(x, "%H:%M:%S").time(),
151
+ tzinfo=timezone.utc,
152
+ ),
153
+ ),
154
+ # Time only (HH:MM)
155
+ (
156
+ r"^\d{2}:\d{2}$",
157
+ lambda x: datetime.combine(
158
+ datetime.now(timezone.utc).date(),
159
+ datetime.strptime(x, "%H:%M").time(),
160
+ tzinfo=timezone.utc,
161
+ ),
162
+ ),
163
+ ]
164
+
165
+ # Could cache compiled regex patterns
166
+ _COMPILED_PATTERNS = [(re.compile(pattern), handler) for pattern, handler in _all_human_time_formats]
167
+
168
+
169
+ def of(human_time: Union[str, datetime, int, float]) -> datetime:
170
+ """
171
+ Convert various time formats (ISO-8601, Unix timestamps, relative times, etc.) to a datetime object.
172
+
173
+ Args:
174
+ human_time: Input time in one of these formats:
175
+ - int: Unix timestamp in milliseconds
176
+ - datetime: Returns the input unchanged
177
+ - str: One of:
178
+ - "now": Current UTC time
179
+ - Relative time with +/- (e.g. "-1h", "-7d","+30m", "+1w")
180
+ - Relative time with multiple units (e.g. "-1h30m", "+1w2d", "-7d12h30m")
181
+ - Unix timestamp in milliseconds as string (e.g. "1734010792148") or seconds (e.g. "1734010792")
182
+ - ISO formats:
183
+ - With milliseconds and Z (e.g. "2023-12-04T00:19:22.854Z")
184
+ - With Z (e.g. "2023-12-04T00:19:22Z")
185
+ - Without timezone (e.g. "2023-12-04T00:19:22")
186
+ - With space separator (e.g. "2023-12-04 00:19:22")
187
+ - Time only with Z (e.g. "00:19:22Z")
188
+ - Date only (e.g. "2023-12-04")
189
+ - Time only (e.g. "00:19")
190
+ - With timezone offset (e.g. "2023-12-04T00:19:22+01:00", "2023-12-04T00:19:22-0500")
191
+ Valid offsets are between -14:00 and +14:00
192
+ - Common formats:
193
+ - With milliseconds (e.g. "2023-12-04 00:19:22.854")
194
+ - US date (e.g. "12/04/2023")
195
+ - Dotted date (e.g. "2023.12.04")
196
+ - Compact date (e.g. "20231204")
197
+ - Forward slash date (e.g. "2023/12/04")
198
+ - RFC 2822 (e.g. "Mon, 04 Dec 2023 00:19:22 +0000")
199
+ - European dates (e.g. "04-12-2023", "04/12/2023", "04.12.2023")
200
+ - Month name formats:
201
+ - Short with comma (e.g. "Dec 4, 2023")
202
+ - Short without comma (e.g. "Dec 4 2023")
203
+ - Day first (e.g. "4 Dec 2023")
204
+ - Full month (e.g. "December 4, 2023")
205
+
206
+ Returns:
207
+ datetime: A timezone-aware datetime object
208
+
209
+ Raises:
210
+ ValueError: If the input format is not recognized or invalid (e.g., timezone offset > ±14:00)
211
+
212
+ Examples:
213
+ >>> of("-1d")
214
+ minus 1 day from now
215
+ >>> of("+2w")
216
+ plus 2 weeks from now
217
+ >>> of("2023-12-04T12:30:45+02:00")
218
+ Parsing common time formats
219
+ """
220
+ if isinstance(human_time, int):
221
+ return datetime.fromtimestamp(human_time / 1000, tz=timezone.utc)
222
+
223
+ if isinstance(human_time, float):
224
+ return datetime.fromtimestamp(human_time / 1000, tz=timezone.utc)
225
+
226
+ if isinstance(human_time, datetime):
227
+ return human_time.astimezone(timezone.utc)
228
+
229
+ if not isinstance(human_time, str):
230
+ raise ValueError("Invalid input type: " + type(human_time).__name__)
231
+
232
+ human_time = human_time.strip()
233
+ original_input = human_time # Keep original for error message
234
+ human_time = human_time.upper() # Convert to uppercase for simpler regex matching
235
+
236
+ if human_time == "NOW":
237
+ return datetime.now(timezone.utc)
238
+
239
+ # Handle relative times (+/-)
240
+ if human_time.startswith("-") or human_time.startswith("+"):
241
+ sign = -1 if human_time.startswith("-") else 1
242
+
243
+ # Remove leading +/- and split into components
244
+ time_str = human_time[1:]
245
+ time_delta = timedelta(seconds=duration(time_str))
246
+ return datetime.now(timezone.utc) + time_delta * sign
247
+
248
+ # Try each format pattern
249
+ for pattern, fmt in _COMPILED_PATTERNS:
250
+ if re.match(pattern, human_time):
251
+ try:
252
+ if callable(fmt):
253
+ dt: datetime = fmt(human_time)
254
+ if not isinstance(dt, datetime):
255
+ raise ValueError(f"Expected datetime, got {type(dt).__name__}")
256
+ return dt
257
+ if isinstance(fmt, list):
258
+ # Try multiple formats
259
+ for f in fmt:
260
+ try:
261
+ dt = datetime.strptime(human_time, f)
262
+ if dt.tzinfo is None:
263
+ return dt.replace(tzinfo=timezone.utc) # Treat naive times as UTC
264
+ else:
265
+ return dt.astimezone(timezone.utc)
266
+ except ValueError:
267
+ continue
268
+ dt = datetime.strptime(human_time, str(fmt))
269
+ if dt.tzinfo is None:
270
+ return dt.replace(tzinfo=timezone.utc)
271
+ else:
272
+ return dt.astimezone(timezone.utc)
273
+ except ValueError:
274
+ continue
275
+
276
+ raise ValueError("Unsupported time format: " + original_input)
277
+
278
+
279
+ def duration(human_time: str) -> int:
280
+ """
281
+ Parse a human-friendly duration string into seconds, either ISO-8601 format or human format.
282
+ Less than 1 second is rounded to 0.
283
+
284
+ Args:
285
+ human_time (str): Duration string in:
286
+ ISO-8601 format (e.g. 'PT5M', 'P1DT2H'),
287
+ human format (e.g. '1h30m', '1w2d', '7d12h30m')
288
+
289
+ Returns:
290
+ timedelta: The parsed duration as a timedelta object
291
+
292
+ Raises:
293
+ ValueError: If the duration string format is not supported
294
+
295
+ Examples:
296
+ >>> duration('PT5S') # 5 seconds
297
+ >>> duration('PT9M15S') # 9 minutes 15 seconds
298
+ >>> duration('PT9H15M') # 9 hours 15 minutes
299
+ >>> duration('P1DT2H') # 1 day 2 hours
300
+ >>> duration('30m') # 30 minutes
301
+ >>> duration('1h30m') # 1 hour 30 minutes
302
+ >>> duration('1w2d') # 1 week 2 days
303
+ >>> duration('7d12h30m') # 7 days 12 hours 30 minutes
304
+ """
305
+
306
+ if not human_time:
307
+ raise ValueError(f"Invalid duration format: {human_time}")
308
+
309
+ human_time = human_time.upper()
310
+
311
+ # First try ISO-8601 format
312
+ iso_pattern = r"^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$"
313
+ iso_match = re.match(iso_pattern, human_time)
314
+
315
+ if iso_match:
316
+ # PT with no values specified is invalid - must have at least one value
317
+ if human_time == "PT" or human_time == "P" or human_time == "":
318
+ raise ValueError(f"Invalid duration format: {human_time}")
319
+
320
+ weeks = int(iso_match.group(1) or 0)
321
+ days = int(iso_match.group(2) or 0)
322
+ hours = int(iso_match.group(3) or 0)
323
+ minutes = int(iso_match.group(4) or 0)
324
+ seconds = float(iso_match.group(5) or 0)
325
+
326
+ return int(timedelta(weeks=weeks, days=days, hours=hours, minutes=minutes, seconds=seconds).total_seconds())
327
+
328
+ # Try human readable format (e.g. 1h30m, 1w2d)
329
+ human_pattern = r"^(?:(\d+)Y)?(?:(\d+)W)?(?:(\d+)D)?(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$"
330
+ human_match = re.match(human_pattern, human_time)
331
+
332
+ if human_match:
333
+ years = int(human_match.group(1) or 0)
334
+ weeks = int(human_match.group(2) or 0)
335
+ days = int(human_match.group(3) or 0)
336
+ hours = int(human_match.group(4) or 0)
337
+ minutes = int(human_match.group(5) or 0)
338
+ seconds = int(human_match.group(6) or 0)
339
+
340
+ # Convert years to days (assuming 365 days per year)
341
+ days_from_years = years * 365
342
+
343
+ return int(
344
+ timedelta(
345
+ weeks=weeks,
346
+ days=days + days_from_years,
347
+ hours=hours,
348
+ minutes=minutes,
349
+ seconds=seconds,
350
+ ).total_seconds()
351
+ )
352
+
353
+ raise ValueError(f"Unsupported duration format: {human_time}")