bear-epoch-time 1.0.1__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 (29) hide show
  1. bear_epoch_time-1.0.1/.bumpversion.cfg +12 -0
  2. bear_epoch_time-1.0.1/.gitignore +211 -0
  3. bear_epoch_time-1.0.1/AGENTS.md +15 -0
  4. bear_epoch_time-1.0.1/PKG-INFO +14 -0
  5. bear_epoch_time-1.0.1/README.md +1 -0
  6. bear_epoch_time-1.0.1/TODO.md +16 -0
  7. bear_epoch_time-1.0.1/directory_structure.txt +34 -0
  8. bear_epoch_time-1.0.1/maskfile.md +58 -0
  9. bear_epoch_time-1.0.1/noxfile.py +70 -0
  10. bear_epoch_time-1.0.1/pyproject.toml +35 -0
  11. bear_epoch_time-1.0.1/src/bear_epoch_time/__init__.py +25 -0
  12. bear_epoch_time-1.0.1/src/bear_epoch_time/_helpers.py +92 -0
  13. bear_epoch_time-1.0.1/src/bear_epoch_time/_time_class.py +327 -0
  14. bear_epoch_time-1.0.1/src/bear_epoch_time/_time_class_legacy.py +315 -0
  15. bear_epoch_time-1.0.1/src/bear_epoch_time/_timer.py +77 -0
  16. bear_epoch_time-1.0.1/src/bear_epoch_time/_tools.py +17 -0
  17. bear_epoch_time-1.0.1/src/bear_epoch_time/_tz.py +172 -0
  18. bear_epoch_time-1.0.1/src/bear_epoch_time/basic_logger/__init__.py +3 -0
  19. bear_epoch_time-1.0.1/src/bear_epoch_time/basic_logger/logger.py +79 -0
  20. bear_epoch_time-1.0.1/src/bear_epoch_time/basic_logger/logger.pyi +19 -0
  21. bear_epoch_time-1.0.1/src/bear_epoch_time/constants/__init__.py +8 -0
  22. bear_epoch_time-1.0.1/src/bear_epoch_time/constants/date_related.py +32 -0
  23. bear_epoch_time-1.0.1/src/bear_epoch_time/constants/time_related.py +22 -0
  24. bear_epoch_time-1.0.1/src/bear_epoch_time/time_manager.py +238 -0
  25. bear_epoch_time-1.0.1/tests/__init__.py +0 -0
  26. bear_epoch_time-1.0.1/tests/test_basic_logger.py +93 -0
  27. bear_epoch_time-1.0.1/tests/test_epoch_timestamp.py +354 -0
  28. bear_epoch_time-1.0.1/tests/test_time_parsing.py +26 -0
  29. bear_epoch_time-1.0.1/uv.lock +572 -0
@@ -0,0 +1,12 @@
1
+ [bumpversion]
2
+ current_version = 1.0.1
3
+
4
+ [bumpversion:file:pyproject.toml]
5
+
6
+ [bumpversion:file:README.md]
7
+ search = # Bear Epoch Time v{current_version}
8
+ replace = # Bear Epoch Time v{new_version}
9
+
10
+ [bumpversion:file:src/bear_epoch_time/__init__.py]
11
+ search = __version__ = "{current_version}"
12
+ replace = __version__ = "{new_version}"
@@ -0,0 +1,211 @@
1
+ # Created by https://www.toptal.com/developers/gitignore/api/python,macos,shell
2
+ # Edit at https://www.toptal.com/developers/gitignore?templates=python,macos,shell
3
+
4
+ ### macOS ###
5
+ # General
6
+ .DS_Store
7
+ .AppleDouble
8
+ .LSOverride
9
+
10
+ # Icon must end with two \r
11
+ Icon
12
+
13
+
14
+ # Thumbnails
15
+ ._*
16
+
17
+ # Files that might appear in the root of a volume
18
+ .DocumentRevisions-V100
19
+ .fseventsd
20
+ .Spotlight-V100
21
+ .TemporaryItems
22
+ .Trashes
23
+ .VolumeIcon.icns
24
+ .com.apple.timemachine.donotpresent
25
+
26
+ # Directories potentially created on remote AFP share
27
+ .AppleDB
28
+ .AppleDesktop
29
+ Network Trash Folder
30
+ Temporary Items
31
+ .apdisk
32
+
33
+ ### macOS Patch ###
34
+ # iCloud generated files
35
+ *.icloud
36
+
37
+ ### Python ###
38
+ # Byte-compiled / optimized / DLL files
39
+ __pycache__/
40
+ *.py[cod]
41
+ *$py.class
42
+
43
+ # C extensions
44
+ *.so
45
+
46
+ # Distribution / packaging
47
+ .Python
48
+ build/
49
+ develop-eggs/
50
+ dist/
51
+ downloads/
52
+ eggs/
53
+ .eggs/
54
+ lib/
55
+ lib64/
56
+ parts/
57
+ sdist/
58
+ var/
59
+ wheels/
60
+ share/python-wheels/
61
+ *.egg-info/
62
+ .installed.cfg
63
+ *.egg
64
+ MANIFEST
65
+
66
+ # PyInstaller
67
+ # Usually these files are written by a python script from a template
68
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
69
+ *.manifest
70
+ *.spec
71
+
72
+ # Installer logs
73
+ pip-log.txt
74
+ pip-delete-this-directory.txt
75
+
76
+ # Unit test / coverage reports
77
+ htmlcov/
78
+ .tox/
79
+ .nox/
80
+ .coverage
81
+ .coverage.*
82
+ .cache
83
+ nosetests.xml
84
+ coverage.xml
85
+ *.cover
86
+ *.py,cover
87
+ .hypothesis/
88
+ .pytest_cache/
89
+ cover/
90
+
91
+ # Translations
92
+ *.mo
93
+ *.pot
94
+
95
+ # Django stuff:
96
+ *.log
97
+ local_settings.py
98
+ db.sqlite3
99
+ db.sqlite3-journal
100
+
101
+ # Flask stuff:
102
+ instance/
103
+ .webassets-cache
104
+
105
+ # Scrapy stuff:
106
+ .scrapy
107
+
108
+ # Sphinx documentation
109
+ docs/_build/
110
+
111
+ # PyBuilder
112
+ .pybuilder/
113
+ target/
114
+
115
+ # Jupyter Notebook
116
+ .ipynb_checkpoints
117
+
118
+ # IPython
119
+ profile_default/
120
+ ipython_config.py
121
+
122
+ # pyenv
123
+ # For a library or package, you might want to ignore these files since the code is
124
+ # intended to run in multiple environments; otherwise, check them in:
125
+ .python-version
126
+
127
+ # pipenv
128
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
129
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
130
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
131
+ # install all needed dependencies.
132
+ #Pipfile.lock
133
+
134
+ # poetry
135
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
136
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
137
+ # commonly ignored for libraries.
138
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
139
+ #poetry.lock
140
+
141
+ # pdm
142
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
143
+ #pdm.lock
144
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
145
+ # in version control.
146
+ # https://pdm.fming.dev/#use-with-ide
147
+ .pdm.toml
148
+
149
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
150
+ __pypackages__/
151
+
152
+ # Celery stuff
153
+ celerybeat-schedule
154
+ celerybeat.pid
155
+
156
+ # SageMath parsed files
157
+ *.sage.py
158
+
159
+ # Environments
160
+ .env
161
+ .venv
162
+ env/
163
+ venv/
164
+ ENV/
165
+ env.bak/
166
+ venv.bak/
167
+
168
+ # Spyder project settings
169
+ .spyderproject
170
+ .spyproject
171
+
172
+ # Rope project settings
173
+ .ropeproject
174
+
175
+ # mkdocs documentation
176
+ /site
177
+
178
+ # mypy
179
+ .mypy_cache/
180
+ .dmypy.json
181
+ dmypy.json
182
+
183
+ # Pyre type checker
184
+ .pyre/
185
+
186
+ # pytype static type analyzer
187
+ .pytype/
188
+
189
+ # Cython debug symbols
190
+ cython_debug/
191
+
192
+ # PyCharm
193
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
194
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
195
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
196
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
197
+ .idea/
198
+
199
+ ### Python Patch ###
200
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
201
+ poetry.toml
202
+
203
+ # ruff
204
+ .ruff_cache/
205
+
206
+ # LSP config files
207
+ pyrightconfig.json
208
+
209
+ .vscode/
210
+
211
+ *.db
@@ -0,0 +1,15 @@
1
+ # Contributor Guide for AI Agents
2
+
3
+ ## Dev Environment Guidelines
4
+
5
+ ### Claude Only
6
+
7
+ <antml:thinking_mode>interleaved</antml:thinking_mode><antml:max_thinking_length>16000</antml:max_thinking_length>
8
+
9
+ - Python Version and Typing: Assume Python 3.12 or higher is being used. Because of that we SHOULD use the latest typing features available in Python 3.12 and later. Never use `Optional`, `Union`, or `Dict, List, Tuple` from `typing` module. Instead, use the built-in types directly, e.g., `list`, `dict`, `tuple`, and use `|` for union types. This is not an exhaustive list, so be thoughtful about any potentially deprecated types you might use. If you are not sure about something, ask the user.
10
+ - Type Annotations: Use type annotations as often as possible. If a function has no return value, use `None` as the return type. If a function returns a value, use the appropriate type annotation but don't bend over backwards to make it perfect. If you are not sure about the type, use `Any` as a fallback but use this sparingly, see it as a failure and you should endeavor to avoid using `Any` as much as possible.
11
+ - Docstrings: Use docstrings for most (95%) public functions and classes. The docstring should describe the purpose of the function or class, its parameters, and its return value. Use the Google style for docstrings. Because of this it is highly preferred to not use comments in the code unless absolutely necessary. If you feel like you need to use a comment, consider if the code can be refactored to make it more readable instead.
12
+ - Imports: Use relative imports for everything (beyond the obvious imports for external packages) in this current package. Keep imports clean using `uv run isort src tests` and `uv run black src tests` to format the code in that order.
13
+ - Endeavor to use a TDD approach when writing new code. Write tests first, then write the code to make the tests pass and then refactor the code to make it cleaner and more readable.
14
+ - There is a `directory_structure.txt` file in the root of the repository that describes the directory structure of the project. Use this as a quick way to understand the project structure and where to place new files or modules.
15
+ - While projects might partially use Poetry, UV is generally the preferred tool for managing dependencies and virtual environments. It is taking me some time to get used to it, so please bear with me. If you are not sure about something, ask the user.
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: bear-epoch-time
3
+ Version: 1.0.1
4
+ Summary: Elegant epoch timestamp handling with timezone-aware operations, fluent API, and smart defaults for Python developers who hate datetime complexity.
5
+ Author-email: chaz <bright.lid5647@fastmail.com>
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: python-dateutil>=2.9.0.post0
8
+ Requires-Dist: pytz>=2025.2
9
+ Requires-Dist: rich-tree-cli>=0.3.14
10
+ Requires-Dist: rich>=14.0.0
11
+ Requires-Dist: tzlocal>=5.3.1
12
+ Description-Content-Type: text/markdown
13
+
14
+ # Bear Epoch Time v1.0.1
@@ -0,0 +1 @@
1
+ # Bear Epoch Time v1.0.1
@@ -0,0 +1,16 @@
1
+ ---
2
+ tags: ["todo", "python", "module", "time", "pypi"]
3
+ project_path: "/Users/chaz/Documents/repos/github/sicksubroutine/bear-epoch-time"
4
+ ---
5
+ # bear-epoch-time
6
+
7
+ ## Tasks
8
+
9
+ - [ ] Do a once over of everything with Codex (Hi! That is you! :D)
10
+ - [ ] Ensure there is good test coverage
11
+ - [ ] use Nox for ensure full Python Version coverage
12
+ - [/] Return Default Constants for timezone related things (Basically have it use specific defaults for `BEAR` (the dev) and check for local values for everybody else) ([/] means the task is in progress)
13
+
14
+ ## Notes
15
+
16
+
@@ -0,0 +1,34 @@
1
+  bear-epoch-time
2
+ ├──  .bumpversion.cfg (327 bytes) (12 lines)
3
+ ├──  .gitignore (3937 bytes) (211 lines)
4
+ ├──  .python-version (5 bytes) (1 lines)
5
+ ├──  AGENTS.md (2448 bytes) (15 lines)
6
+ ├──  directory_structure.txt (1794 bytes) (34 lines)
7
+ ├──  maskfile.md (819 bytes) (53 lines)
8
+ ├──  noxfile.py (1507 bytes) (70 lines)
9
+ ├──  pyproject.toml (855 bytes) (35 lines)
10
+ ├──  README.md (25 bytes) (1 lines)
11
+ ├──  TODO.md (554 bytes) (16 lines)
12
+ ├──  src
13
+ │ └──  bear_epoch_time
14
+ │ ├──  __init__.py (544 bytes) (25 lines)
15
+ │ ├──  _helpers.py (2868 bytes) (92 lines)
16
+ │ ├──  _time_class.py (11783 bytes) (327 lines)
17
+ │ ├──  _time_class_legacy.py (11504 bytes) (315 lines)
18
+ │ ├──  _timer.py (2349 bytes) (77 lines)
19
+ │ ├──  _tools.py (456 bytes) (17 lines)
20
+ │ ├──  _tz.py (4930 bytes) (172 lines)
21
+ │ ├──  time_manager.py (8518 bytes) (238 lines)
22
+ │ ├──  basic_logger
23
+ │ │ ├──  __init__.py (117 bytes) (3 lines)
24
+ │ │ ├──  logger.py (2459 bytes) (79 lines)
25
+ │ │ └──  logger.pyi (948 bytes) (19 lines)
26
+ │ └──  constants
27
+ │ ├──  __init__.py (298 bytes) (8 lines)
28
+ │ ├──  date_related.py (1003 bytes) (32 lines)
29
+ │ └──  time_related.py (638 bytes) (22 lines)
30
+ └──  tests
31
+ ├──  __init__.py (0 bytes) (0 lines)
32
+ ├──  test_basic_logger.py (3274 bytes) (93 lines)
33
+ ├──  test_epoch_timestamp.py (14011 bytes) (354 lines)
34
+ └──  test_time_parsing.py (992 bytes) (26 lines)
@@ -0,0 +1,58 @@
1
+ # Python Project Mask File
2
+
3
+ ## clean
4
+
5
+ > This command cleans the build artifacts
6
+
7
+ ```bash
8
+ rm -rf dist/
9
+ ```
10
+
11
+ ## bump (patch_version)
12
+
13
+ > Bump the version of the local project specifying the patch level: `minor`, `major`, `patch`
14
+
15
+ ```bash
16
+ bump2version ${patch_version} --allow-dirty
17
+ ```
18
+
19
+ ## build
20
+
21
+ > This command builds the project via uv
22
+
23
+ ```bash
24
+ uv build
25
+ ```
26
+
27
+ ## test
28
+
29
+ > This command runs the tests using pytest
30
+
31
+ ```bash
32
+ pytest -s
33
+ ```
34
+
35
+ ## publish
36
+
37
+ > This command publishes the package to PyPI officially, isn't that great?
38
+
39
+ ```bash
40
+ if [ "${publish_location}" = "local" ]; then
41
+ twine upload -r local dist/* # uploads to a local repository
42
+ else
43
+ export UV_PUBLISH_TOKEN=$(op read "op://Private/PyPI Prod/api_key")
44
+ uv publish --index pypi dist/* --token $UV_PUBLISH_TOKEN
45
+ fi
46
+ ```
47
+
48
+ ## full (patch_version) (publish_location)
49
+
50
+ > This command runs the full build and publish process
51
+
52
+ ```bash
53
+ $MASK clean
54
+ $MASK bump ${patch_version}
55
+ $MASK build
56
+ $MASK publish ${publish_location}
57
+ pytest -s
58
+ ```
@@ -0,0 +1,70 @@
1
+ import nox
2
+
3
+ VERSIONS = ["3.9", "3.10", "3.11", "3.12", "3.13"]
4
+
5
+
6
+ # @nox.session(venv_backend="uv", tags=["lint"])
7
+ # def flake8(session):
8
+ # """Run flake8 linting"""
9
+ # session.install("flake8")
10
+
11
+ # session.run(
12
+ # "flake8",
13
+ # ".",
14
+ # "--count",
15
+ # "--select=E9,F63,F7,F82",
16
+ # "--show-source",
17
+ # "--statistics",
18
+ # "--exclude=.venv,.nox",
19
+ # )
20
+
21
+ # session.run(
22
+ # "flake8",
23
+ # ".",
24
+ # "--count",
25
+ # "--exit-zero",
26
+ # "--max-complexity=10",
27
+ # "--max-line-length=127",
28
+ # "--statistics",
29
+ # "--exclude=.venv,.nox",
30
+ # )
31
+
32
+
33
+ # @nox.session(venv_backend="uv", tags=["lint"])
34
+ # def isort(session):
35
+ # """Check import sorting with isort"""
36
+ # session.install("isort")
37
+ # session.run(
38
+ # "isort",
39
+ # ".",
40
+ # "--check-only",
41
+ # "--diff",
42
+ # "--skip",
43
+ # ".venv",
44
+ # "--skip",
45
+ # ".nox",
46
+ # "--line-length",
47
+ # "120",
48
+ # )
49
+
50
+
51
+ # @nox.session(venv_backend="uv", tags=["lint"])
52
+ # def black(session):
53
+ # """Check formatting with black"""
54
+ # session.install("black")
55
+ # session.run(
56
+ # "black",
57
+ # ".",
58
+ # "--check",
59
+ # "--diff",
60
+ # "--extend-exclude=.venv|.nox",
61
+ # "-l",
62
+ # "120",
63
+ # )
64
+
65
+
66
+ @nox.session(python=VERSIONS, venv_backend="uv")
67
+ def test_all_tests(session):
68
+ session.install("-e", ".")
69
+ session.install("pytest")
70
+ session.run("pytest")
@@ -0,0 +1,35 @@
1
+ [project]
2
+ name = "bear-epoch-time"
3
+ version = "1.0.1"
4
+ description = "Elegant epoch timestamp handling with timezone-aware operations, fluent API, and smart defaults for Python developers who hate datetime complexity."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "chaz", email = "bright.lid5647@fastmail.com" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "python-dateutil>=2.9.0.post0",
12
+ "pytz>=2025.2",
13
+ "rich>=14.0.0",
14
+ "rich-tree-cli>=0.3.14",
15
+ "tzlocal>=5.3.1",
16
+ ]
17
+
18
+ [build-system]
19
+ requires = ["hatchling"]
20
+ build-backend = "hatchling.build"
21
+
22
+ [dependency-groups]
23
+ dev = [
24
+ "black>=25.1.0",
25
+ "bump2version>=1.0.1",
26
+ "flake8>=7.2.0",
27
+ "isort>=6.0.1",
28
+ "nox>=2025.5.1",
29
+ "pytest>=8.4.0",
30
+ ]
31
+
32
+ [tool.pytest.ini_options]
33
+ markers = [
34
+ "visual: marks tests as visual verification tests (deselect with '-m \"not visual\"')"
35
+ ]
@@ -0,0 +1,25 @@
1
+ __version__ = "1.0.1"
2
+
3
+ import sys
4
+
5
+ if sys.version_info < (3, 11):
6
+ from ._time_class_legacy import EpochTimestamp
7
+ else:
8
+ from ._time_class import EpochTimestamp
9
+
10
+ from ._timer import TimerData, create_timer, timer
11
+ from ._tools import add_ord_suffix
12
+ from .constants.date_related import DATE_FORMAT, DATE_TIME_FORMAT
13
+ from .time_manager import TimeTools
14
+
15
+ __all__ = [
16
+ "EpochTimestamp",
17
+ "TimerData",
18
+ "create_timer",
19
+ "timer",
20
+ "TimeTools",
21
+ "add_ord_suffix",
22
+ "DATE_FORMAT",
23
+ "DATE_TIME_FORMAT",
24
+ "__version__",
25
+ ]
@@ -0,0 +1,92 @@
1
+ import re
2
+ from datetime import timedelta
3
+
4
+ from .constants.time_related import (SECONDS_IN_DAY, SECONDS_IN_HOUR,
5
+ SECONDS_IN_MINUTE, SECONDS_IN_MONTH)
6
+
7
+
8
+ def convert_to_seconds(time_str: str) -> int:
9
+ """Convert a time string to seconds.
10
+
11
+ Examples
12
+ --------
13
+ >>> convert_to_seconds("1M 30m")
14
+ 2610000
15
+
16
+ Notes
17
+ -----
18
+ * ``M`` or ``mo`` denotes **months**.
19
+ * ``m`` denotes **minutes**.
20
+ """
21
+
22
+ time_parts: list[tuple[str, str]] = re.findall(r"(\d+)\s*(M|mo|[dhms])", time_str)
23
+ total_seconds = 0
24
+ for value, unit in time_parts:
25
+ if not value.isdigit():
26
+ raise ValueError(f"Invalid time value: {value}")
27
+ value = int(value)
28
+
29
+ if unit == "M" or unit.lower() == "mo":
30
+ total_seconds += value * SECONDS_IN_MONTH
31
+ elif unit == "d":
32
+ total_seconds += value * SECONDS_IN_DAY
33
+ elif unit == "h":
34
+ total_seconds += value * SECONDS_IN_HOUR
35
+ elif unit == "m":
36
+ total_seconds += value * SECONDS_IN_MINUTE
37
+ elif unit == "s":
38
+ total_seconds += value
39
+ else:
40
+ raise ValueError(f"Invalid time unit: {unit}")
41
+ return total_seconds
42
+
43
+
44
+ def timedelta_to_seconds(td: timedelta) -> int:
45
+ """Convert a timedelta object to seconds."""
46
+ if not isinstance(td, timedelta):
47
+ raise ValueError("Input must be a timedelta object")
48
+ return int(td.total_seconds())
49
+
50
+
51
+ def convert_to_milliseconds(time_str: str) -> int:
52
+ return convert_to_seconds(time_str) * 1000
53
+
54
+
55
+ def milliseconds_to_time(milliseconds: int) -> str:
56
+ """Convert milliseconds to a human-readable time string."""
57
+ if milliseconds < 0:
58
+ raise ValueError("Milliseconds cannot be negative")
59
+ seconds = milliseconds // 1000
60
+ return seconds_to_time(seconds)
61
+
62
+
63
+ def seconds_to_timedelta(seconds: int) -> timedelta:
64
+ """Convert seconds to a timedelta object."""
65
+ if seconds < 0:
66
+ raise ValueError("Seconds cannot be negative")
67
+ return timedelta(seconds=seconds)
68
+
69
+
70
+ def seconds_to_time(seconds: int) -> str:
71
+ """Convert seconds to a human-readable time string.
72
+
73
+ Months are represented with ``M`` while minutes use ``m``.
74
+ """
75
+ if seconds < 0:
76
+ raise ValueError("Seconds cannot be negative")
77
+ months, remainder = divmod(seconds, SECONDS_IN_MONTH)
78
+ days, remainder = divmod(remainder, SECONDS_IN_DAY)
79
+ hours, remainder = divmod(remainder, SECONDS_IN_HOUR)
80
+ minutes, seconds = divmod(remainder, SECONDS_IN_MINUTE)
81
+ time_parts = []
82
+ if months > 0:
83
+ time_parts.append(f"{months}M")
84
+ if days > 0:
85
+ time_parts.append(f"{days}d")
86
+ if hours > 0:
87
+ time_parts.append(f"{hours}h")
88
+ if minutes > 0:
89
+ time_parts.append(f"{minutes}m")
90
+ if seconds > 0:
91
+ time_parts.append(f"{seconds}s")
92
+ return " ".join(time_parts)