pyutilkit 0.10.0__tar.gz → 0.11.0.post1__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.
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.3
2
+ Name: pyutilkit
3
+ Version: 0.11.0.post1
4
+ Summary: python's missing batteries
5
+ Keywords: utils
6
+ Author: Stephanos Kuma
7
+ Author-email: Stephanos Kuma <stephanos@kuma.ai>
8
+ License: BSD-3-Clause
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Intended Audience :: Developers
13
+ Requires-Dist: tzdata ; os_name == 'nt'
14
+ Requires-Python: >=3.10
15
+ Project-URL: homepage, https://pyutilkit.readthedocs.io/en/stable/
16
+ Project-URL: repository, https://github.com/spapanik/pyutilkit
17
+ Project-URL: documentation, https://pyutilkit.readthedocs.io/en/stable/
18
+ Description-Content-Type: text/markdown
19
+
20
+ # pyutilkit: python's missing batteries
21
+
22
+ [![build][build_badge]][build_url]
23
+ [![lint][lint_badge]][lint_url]
24
+ [![tests][tests_badge]][tests_url]
25
+ [![license][licence_badge]][licence_url]
26
+ [![codecov][codecov_badge]][codecov_url]
27
+ [![readthedocs][readthedocs_badge]][readthedocs_url]
28
+ [![pypi][pypi_badge]][pypi_url]
29
+ [![downloads][pepy_badge]][pepy_url]
30
+ [![build automation: yam][yam_badge]][yam_url]
31
+ [![Lint: ruff][ruff_badge]][ruff_url]
32
+
33
+ Python has long maintained the philosophy of "batteries included", providing users with a rich
34
+ standard library that avoids the need for third-party tools for most tasks. Some packages are so
35
+ common they have achieved similar status to the standard library. Yet, certain utilities are
36
+ reimplemented across countless projects. This lightweight library, with minimal dependencies,
37
+ aims to eliminate that repetition.
38
+
39
+ ## Quick Start
40
+
41
+ Install pyutilkit:
42
+
43
+ ```bash
44
+ pip install pyutilkit
45
+ ```
46
+
47
+ Or with uv (recommended for speed):
48
+
49
+ ```bash
50
+ uv pip install pyutilkit
51
+ ```
52
+
53
+ Basic usage example:
54
+
55
+ ```python
56
+ from pyutilkit.date_utils import now
57
+ from pyutilkit.timing import Stopwatch
58
+ from pyutilkit.term import SGRString, SGRCodes
59
+
60
+ # Get current time in any timezone
61
+ from zoneinfo import ZoneInfo
62
+ tokyo_time = now(ZoneInfo("Asia/Tokyo"))
63
+ print(f"Current time in Tokyo: {tokyo_time}")
64
+
65
+ # Measure execution time
66
+ stopwatch = Stopwatch()
67
+ with stopwatch:
68
+ # Your code here
69
+ result = sum(range(1000000))
70
+ print(f"Computation took: {stopwatch.elapsed}")
71
+
72
+ # Colorful terminal output
73
+ success = SGRString("✓ Task completed", params=[SGRCodes.GREEN, SGRCodes.BOLD])
74
+ success.print()
75
+ ```
76
+
77
+ ## Key Features
78
+
79
+ - **🕐 Timezone Utilities**: Robust datetime handling with cross-platform timezone support, ISO parsing (including Zulu timezone), and seamless timezone conversion
80
+ - **⏱️ High-Precision Timing**: Nanosecond-precision timing with human-readable formatting, lap tracking, and stopwatch functionality
81
+ - **🎨 Terminal Formatting**: ANSI color codes with smart TTY detection, automatic style stripping for piped output, and convenient header formatting
82
+ - **🛡️ Error Handling**: Elegant exception handling decorator that logs errors and returns defaults instead of raising exceptions
83
+ - **📁 File Utilities**: Efficient SHA-256 file hashing with buffered reading for large files
84
+ - **🚀 Subprocess Enhancement**: Run shell commands with real-time output streaming, automatic timing, and structured results
85
+ - **🔧 Design Patterns**: Thread-safe Singleton metaclass implementation
86
+ - **✨ Zero Dependencies**: Pure Python using only standard library (except optional tzdata on Windows)
87
+ - **🧪 Fully Tested**: 100% test coverage with comprehensive type annotations
88
+
89
+ ## Documentation
90
+
91
+ - **[Installation Guide](installation.md)** - Setup instructions and requirements
92
+ - **[Usage Guide](usage/index.md)** - Comprehensive examples and tutorials for all modules
93
+ - [Classes](usage/classes.md) - Singleton pattern implementation
94
+ - [Date Utilities](usage/date_utils.md) - Timezone-aware datetime operations
95
+ - [File Utilities](usage/files.md) - Exception handling and file hashing
96
+ - [Subprocess](usage/subprocess.md) - Enhanced command execution
97
+ - [Terminal](usage/term.md) - Terminal formatting and colors
98
+ - [Timing](usage/timing.md) - Performance measurement and benchmarking
99
+ - **[Changelog](CHANGELOG.md)** - Version history and changes
100
+ - **[Code of Conduct](CODE_OF_CONDUCT.md)** - Community guidelines
101
+
102
+ ## Requirements
103
+
104
+ - Python 3.10 or higher
105
+ - No external dependencies (tzdata is automatically installed on Windows)
106
+
107
+ ## Links
108
+
109
+ - [Full Documentation](https://pyutilkit.readthedocs.io/en/stable/)
110
+ - [PyPI Package](https://pypi.org/project/pyutilkit)
111
+ - [GitHub Repository](https://github.com/spapanik/pyutilkit)
112
+ - [Changelog](CHANGELOG.md)
113
+ - [Report Issues](https://github.com/spapanik/pyutilkit/issues)
114
+
115
+ [build_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml/badge.svg
116
+ [build_url]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml
117
+ [lint_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml/badge.svg
118
+ [lint_url]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml
119
+ [tests_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml/badge.svg
120
+ [tests_url]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml
121
+ [licence_badge]: https://img.shields.io/pypi/l/pyutilkit
122
+ [licence_url]: https://pyutilkit.readthedocs.io/en/stable/LICENSE/
123
+ [codecov_badge]: https://codecov.io/github/spapanik/pyutilkit/graph/badge.svg?token=Q20F84BW72
124
+ [codecov_url]: https://codecov.io/github/spapanik/pyutilkit
125
+ [readthedocs_badge]: https://readthedocs.org/projects/pyutilkit/badge/?version=latest
126
+ [readthedocs_url]: https://pyutilkit.readthedocs.io/en/latest/
127
+ [pypi_badge]: https://img.shields.io/pypi/v/pyutilkit
128
+ [pypi_url]: https://pypi.org/project/pyutilkit
129
+ [pepy_badge]: https://pepy.tech/badge/pyutilkit
130
+ [pepy_url]: https://pepy.tech/project/pyutilkit
131
+ [yam_badge]: https://img.shields.io/badge/build%20automation-yamk-success
132
+ [yam_url]: https://github.com/spapanik/yamk
133
+ [ruff_badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json
134
+ [ruff_url]: https://github.com/charliermarsh/ruff
135
+ [Documentation]: https://pyutilkit.readthedocs.io/en/stable/
136
+ [Changelog]: https://pyutilkit.readthedocs.io/en/stable/CHANGELOG/
@@ -0,0 +1,117 @@
1
+ # pyutilkit: python's missing batteries
2
+
3
+ [![build][build_badge]][build_url]
4
+ [![lint][lint_badge]][lint_url]
5
+ [![tests][tests_badge]][tests_url]
6
+ [![license][licence_badge]][licence_url]
7
+ [![codecov][codecov_badge]][codecov_url]
8
+ [![readthedocs][readthedocs_badge]][readthedocs_url]
9
+ [![pypi][pypi_badge]][pypi_url]
10
+ [![downloads][pepy_badge]][pepy_url]
11
+ [![build automation: yam][yam_badge]][yam_url]
12
+ [![Lint: ruff][ruff_badge]][ruff_url]
13
+
14
+ Python has long maintained the philosophy of "batteries included", providing users with a rich
15
+ standard library that avoids the need for third-party tools for most tasks. Some packages are so
16
+ common they have achieved similar status to the standard library. Yet, certain utilities are
17
+ reimplemented across countless projects. This lightweight library, with minimal dependencies,
18
+ aims to eliminate that repetition.
19
+
20
+ ## Quick Start
21
+
22
+ Install pyutilkit:
23
+
24
+ ```bash
25
+ pip install pyutilkit
26
+ ```
27
+
28
+ Or with uv (recommended for speed):
29
+
30
+ ```bash
31
+ uv pip install pyutilkit
32
+ ```
33
+
34
+ Basic usage example:
35
+
36
+ ```python
37
+ from pyutilkit.date_utils import now
38
+ from pyutilkit.timing import Stopwatch
39
+ from pyutilkit.term import SGRString, SGRCodes
40
+
41
+ # Get current time in any timezone
42
+ from zoneinfo import ZoneInfo
43
+ tokyo_time = now(ZoneInfo("Asia/Tokyo"))
44
+ print(f"Current time in Tokyo: {tokyo_time}")
45
+
46
+ # Measure execution time
47
+ stopwatch = Stopwatch()
48
+ with stopwatch:
49
+ # Your code here
50
+ result = sum(range(1000000))
51
+ print(f"Computation took: {stopwatch.elapsed}")
52
+
53
+ # Colorful terminal output
54
+ success = SGRString("✓ Task completed", params=[SGRCodes.GREEN, SGRCodes.BOLD])
55
+ success.print()
56
+ ```
57
+
58
+ ## Key Features
59
+
60
+ - **🕐 Timezone Utilities**: Robust datetime handling with cross-platform timezone support, ISO parsing (including Zulu timezone), and seamless timezone conversion
61
+ - **⏱️ High-Precision Timing**: Nanosecond-precision timing with human-readable formatting, lap tracking, and stopwatch functionality
62
+ - **🎨 Terminal Formatting**: ANSI color codes with smart TTY detection, automatic style stripping for piped output, and convenient header formatting
63
+ - **🛡️ Error Handling**: Elegant exception handling decorator that logs errors and returns defaults instead of raising exceptions
64
+ - **📁 File Utilities**: Efficient SHA-256 file hashing with buffered reading for large files
65
+ - **🚀 Subprocess Enhancement**: Run shell commands with real-time output streaming, automatic timing, and structured results
66
+ - **🔧 Design Patterns**: Thread-safe Singleton metaclass implementation
67
+ - **✨ Zero Dependencies**: Pure Python using only standard library (except optional tzdata on Windows)
68
+ - **🧪 Fully Tested**: 100% test coverage with comprehensive type annotations
69
+
70
+ ## Documentation
71
+
72
+ - **[Installation Guide](installation.md)** - Setup instructions and requirements
73
+ - **[Usage Guide](usage/index.md)** - Comprehensive examples and tutorials for all modules
74
+ - [Classes](usage/classes.md) - Singleton pattern implementation
75
+ - [Date Utilities](usage/date_utils.md) - Timezone-aware datetime operations
76
+ - [File Utilities](usage/files.md) - Exception handling and file hashing
77
+ - [Subprocess](usage/subprocess.md) - Enhanced command execution
78
+ - [Terminal](usage/term.md) - Terminal formatting and colors
79
+ - [Timing](usage/timing.md) - Performance measurement and benchmarking
80
+ - **[Changelog](CHANGELOG.md)** - Version history and changes
81
+ - **[Code of Conduct](CODE_OF_CONDUCT.md)** - Community guidelines
82
+
83
+ ## Requirements
84
+
85
+ - Python 3.10 or higher
86
+ - No external dependencies (tzdata is automatically installed on Windows)
87
+
88
+ ## Links
89
+
90
+ - [Full Documentation](https://pyutilkit.readthedocs.io/en/stable/)
91
+ - [PyPI Package](https://pypi.org/project/pyutilkit)
92
+ - [GitHub Repository](https://github.com/spapanik/pyutilkit)
93
+ - [Changelog](CHANGELOG.md)
94
+ - [Report Issues](https://github.com/spapanik/pyutilkit/issues)
95
+
96
+ [build_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml/badge.svg
97
+ [build_url]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml
98
+ [lint_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml/badge.svg
99
+ [lint_url]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml
100
+ [tests_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml/badge.svg
101
+ [tests_url]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml
102
+ [licence_badge]: https://img.shields.io/pypi/l/pyutilkit
103
+ [licence_url]: https://pyutilkit.readthedocs.io/en/stable/LICENSE/
104
+ [codecov_badge]: https://codecov.io/github/spapanik/pyutilkit/graph/badge.svg?token=Q20F84BW72
105
+ [codecov_url]: https://codecov.io/github/spapanik/pyutilkit
106
+ [readthedocs_badge]: https://readthedocs.org/projects/pyutilkit/badge/?version=latest
107
+ [readthedocs_url]: https://pyutilkit.readthedocs.io/en/latest/
108
+ [pypi_badge]: https://img.shields.io/pypi/v/pyutilkit
109
+ [pypi_url]: https://pypi.org/project/pyutilkit
110
+ [pepy_badge]: https://pepy.tech/badge/pyutilkit
111
+ [pepy_url]: https://pepy.tech/project/pyutilkit
112
+ [yam_badge]: https://img.shields.io/badge/build%20automation-yamk-success
113
+ [yam_url]: https://github.com/spapanik/yamk
114
+ [ruff_badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json
115
+ [ruff_url]: https://github.com/charliermarsh/ruff
116
+ [Documentation]: https://pyutilkit.readthedocs.io/en/stable/
117
+ [Changelog]: https://pyutilkit.readthedocs.io/en/stable/CHANGELOG/
@@ -1,12 +1,12 @@
1
1
  [build-system]
2
2
  requires = [
3
- "phosphorus>=0.8",
3
+ "uv_build>=0.11.0,<0.12.0",
4
4
  ]
5
- build-backend = "phosphorus.construction.api"
5
+ build-backend = "uv_build"
6
6
 
7
7
  [project]
8
8
  name = "pyutilkit"
9
- version = "0.10.0"
9
+ version = "0.11.0.post1"
10
10
 
11
11
  authors = [
12
12
  { name = "Stephanos Kuma", email = "stephanos@kuma.ai" },
@@ -22,10 +22,10 @@ classifiers = [
22
22
  "Development Status :: 4 - Beta",
23
23
  "Operating System :: OS Independent",
24
24
  "Programming Language :: Python :: 3 :: Only",
25
- "License :: OSI Approved :: BSD License",
25
+ "Intended Audience :: Developers",
26
26
  ]
27
27
 
28
- requires-python = ">=3.9"
28
+ requires-python = ">=3.10"
29
29
  dependencies = [
30
30
  "tzdata; os_name == 'nt'",
31
31
  ]
@@ -35,32 +35,35 @@ homepage = "https://pyutilkit.readthedocs.io/en/stable/"
35
35
  repository = "https://github.com/spapanik/pyutilkit"
36
36
  documentation = "https://pyutilkit.readthedocs.io/en/stable/"
37
37
 
38
- [tool.phosphorus.dev-dependencies]
38
+ [dependency-groups]
39
39
  dev = [
40
40
  "ipdb~=0.13",
41
- "ipython~=8.18",
41
+ "ipython~=8.37",
42
+ "ptpython~=3.0",
43
+ { include-group = "lint" },
44
+ { include-group = "test" },
45
+ { include-group = "docs" },
42
46
  ]
43
47
  lint = [
44
- "black~=24.10",
45
- "mypy~=1.13",
46
- "ruff~=0.8",
48
+ "mypy~=1.19",
49
+ "ruff~=0.15",
50
+ "ty~=0.0.26",
51
+ { include-group = "test_core" },
47
52
  ]
48
53
  test = [
54
+ "pytest-cov~=7.0",
55
+ { include-group = "test_core" },
56
+ ]
57
+ test_core = [
49
58
  "freezegun~=1.5",
50
- "pytest~=8.3",
51
- "pytest-cov~=6.0",
59
+ "pytest~=9.0",
52
60
  ]
53
61
  docs = [
54
62
  "mkdocs~=1.6",
55
- "mkdocs-material~=9.5",
63
+ "mkdocs-material~=9.6",
56
64
  "mkdocs-material-extensions~=1.3",
57
- "pygments~=2.18",
58
- "pymdown-extensions~=10.12",
59
- ]
60
-
61
- [tool.black]
62
- target-version = [
63
- "py39",
65
+ "pygments~=2.19",
66
+ "pymdown-extensions~=10.15",
64
67
  ]
65
68
 
66
69
  [tool.mypy]
@@ -95,7 +98,7 @@ disallow_any_decorated = false # mock.MagicMock is Any
95
98
  src = [
96
99
  "src",
97
100
  ]
98
- target-version = "py39"
101
+ target-version = "py310"
99
102
 
100
103
  [tool.ruff.lint]
101
104
  select = [
@@ -103,11 +106,10 @@ select = [
103
106
  ]
104
107
  ignore = [
105
108
  "C901", # Adding a limit to complexity is too arbitrary
106
- "COM812", # Avoid magic trailing commas
109
+ "COM812", # Avoid conflicts with the formatter
107
110
  "D10", # Not everything needs a docstring
108
111
  "D203", # Prefer `no-blank-line-before-class` (D211)
109
112
  "D213", # Prefer `multi-line-summary-first-line` (D212)
110
- "E501", # Avoid clashes with black
111
113
  "PLR09", # Adding a limit to complexity is too arbitrary
112
114
  ]
113
115
 
@@ -116,6 +118,7 @@ ignore = [
116
118
  "FBT001", # Test arguments are handled by pytest
117
119
  "PLR2004", # Tests should contain magic number comparisons
118
120
  "S101", # Pytest needs assert statements
121
+ "SLF001", # Tests need to access private members for coverage
119
122
  ]
120
123
 
121
124
  [tool.ruff.lint.flake8-tidy-imports]
@@ -132,9 +135,16 @@ forced-separate = [
132
135
  ]
133
136
  split-on-trailing-comma = false
134
137
 
135
- [tool.pytest.ini_options]
136
- addopts = "-ra -v"
137
- testpaths = "tests"
138
+ [tool.pytest]
139
+ minversion = "9.0"
140
+ strict = true
141
+ addopts = [
142
+ "-ra",
143
+ "-v",
144
+ ]
145
+ testpaths = [
146
+ "tests",
147
+ ]
138
148
 
139
149
  [tool.coverage.run]
140
150
  branch = true
@@ -142,12 +152,17 @@ source = [
142
152
  "src/",
143
153
  ]
144
154
  data_file = ".cov_cache/coverage.dat"
155
+ omit = [
156
+ "src/**/type_defs.py",
157
+ ]
145
158
 
146
159
  [tool.coverage.report]
147
160
  exclude_also = [
148
161
  "if TYPE_CHECKING:",
162
+ "raise NotImplementedError",
163
+ "raise UnreachableCodeError",
149
164
  ]
150
- fail_under = 90
165
+ fail_under = 100
151
166
  precision = 2
152
167
  show_missing = true
153
168
  skip_covered = true
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("pyutilkit")
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from typing import TypeVar, cast
5
+
6
+ _T = TypeVar("_T")
7
+
8
+
9
+ class Singleton(type):
10
+ instance: object
11
+ _lock: threading.Lock
12
+
13
+ def __init__(
14
+ cls, name: str, bases: tuple[type[object], ...], namespace: dict[str, object]
15
+ ) -> None:
16
+ super().__init__(name, bases, namespace)
17
+ cls.instance = None
18
+ cls._lock = threading.Lock()
19
+
20
+ def __call__(cls: type[_T]) -> _T:
21
+ mcs = cast("Singleton", cls)
22
+ if mcs.instance is None:
23
+ with mcs._lock:
24
+ if mcs.instance is None:
25
+ mcs.instance = super(Singleton, mcs).__call__()
26
+ return cast("_T", mcs.instance)
@@ -17,7 +17,7 @@ def get_timezones() -> set[str]:
17
17
  return available_timezones() - UNAVAILABLE_TIMEZONES
18
18
 
19
19
 
20
- def now(tz_info: ZoneInfo = UTC) -> datetime:
20
+ def now(tz_info: tzinfo = UTC) -> datetime:
21
21
  return datetime.now(UTC).astimezone(tz_info)
22
22
 
23
23
 
@@ -3,19 +3,16 @@ from __future__ import annotations
3
3
  import hashlib
4
4
  import logging
5
5
  from functools import wraps
6
- from typing import TYPE_CHECKING, TypeVar
6
+ from typing import TYPE_CHECKING, ParamSpec, TypeVar
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from collections.abc import Callable
10
10
  from pathlib import Path
11
11
 
12
- from typing_extensions import ParamSpec # upgrade: py3.9: import from typing
13
-
14
- P = ParamSpec("P")
15
-
16
12
  logger = logging.getLogger(__name__)
17
13
  INGEST_ERROR = "Function `%s` threw `%s` when called with args=%s and kwargs=%s"
18
14
  R_co = TypeVar("R_co", covariant=True)
15
+ P = ParamSpec("P")
19
16
 
20
17
 
21
18
  def handle_exceptions(
@@ -32,7 +29,7 @@ def handle_exceptions(
32
29
  except exceptions as exc:
33
30
  getattr(logger, log_level)(
34
31
  INGEST_ERROR,
35
- func.__name__,
32
+ func.__name__, # ty: ignore[unresolved-attribute]
36
33
  exc.__class__.__name__,
37
34
  args,
38
35
  kwargs,
@@ -67,18 +67,8 @@ class SGRCodes(IntEnum):
67
67
  return f"\033[{self.value}m"
68
68
 
69
69
 
70
- @dataclass(frozen=True, order=True)
70
+ @dataclass(frozen=True, order=True, slots=True)
71
71
  class SGRString:
72
- __slots__ = ( # upgrade: py3.9: use __slots__ = True
73
- "_force_prefix",
74
- "_force_sgr",
75
- "_is_error",
76
- "_prefix",
77
- "_sgr",
78
- "_string",
79
- "_suffix",
80
- )
81
-
82
72
  _string: str
83
73
  _sgr: tuple[SGRCodes, ...]
84
74
  _prefix: str
@@ -215,14 +205,14 @@ class SGRString:
215
205
  type(self)(self._string, prefix=prefix, suffix=suffix, params=self._sgr).print()
216
206
 
217
207
 
218
- @dataclass(frozen=True, order=True)
208
+ @dataclass(frozen=True, order=True, slots=True)
219
209
  class SGROutput:
220
- __slots__ = ("_strings",) # upgrade: py3.9: use __slots__ = True
221
210
  _strings: tuple[SGRString, ...]
222
211
 
223
212
  def __init__(
224
213
  self,
225
214
  strings: Iterable[object],
215
+ *,
226
216
  force_prefix: bool | None = None,
227
217
  force_sgr: bool | None = None,
228
218
  is_error: bool | None = None,
@@ -242,6 +232,7 @@ class SGROutput:
242
232
  @staticmethod
243
233
  def _clean_string(
244
234
  string: object,
235
+ *,
245
236
  force_prefix: bool | None,
246
237
  force_sgr: bool | None,
247
238
  is_error: bool | None,
@@ -5,21 +5,20 @@ from time import perf_counter_ns
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  if TYPE_CHECKING:
8
+ from collections.abc import Iterator
8
9
  from types import TracebackType
9
10
 
10
11
  from typing_extensions import Self # upgrade: py3.10: import from typing
11
12
 
12
- METRIC_MULTIPLIER = 1000
13
+ METRIC_MULTIPLIER = 1_000
13
14
  SECONDS_PER_MINUTE = 60
14
15
  MINUTES_PER_HOUR = 60
15
16
  HOURS_PER_DAY = 24
16
17
  SECONDS_PER_DAY = SECONDS_PER_MINUTE * MINUTES_PER_HOUR * HOURS_PER_DAY
17
18
 
18
19
 
19
- @dataclass(frozen=True, order=True)
20
+ @dataclass(frozen=True, order=True, slots=True)
20
21
  class Timing:
21
- __slots__ = ("nanoseconds",) # upgrade: py3.9: use __slots__ = True
22
-
23
22
  nanoseconds: int
24
23
 
25
24
  def __init__(
@@ -41,28 +40,35 @@ class Timing:
41
40
  object.__setattr__(self, "nanoseconds", total_nanoseconds)
42
41
 
43
42
  def __str__(self) -> str:
44
- if self.nanoseconds < METRIC_MULTIPLIER:
45
- return f"{self.nanoseconds}ns"
46
- microseconds = self.nanoseconds / METRIC_MULTIPLIER
43
+ if self.nanoseconds == 0:
44
+ return "0ns"
45
+ sign = "" if self.nanoseconds > 0 else "-"
46
+ nanoseconds = abs(self.nanoseconds)
47
+ if nanoseconds < METRIC_MULTIPLIER:
48
+ return f"{sign}{nanoseconds}ns"
49
+ microseconds = nanoseconds / METRIC_MULTIPLIER
47
50
  if microseconds < METRIC_MULTIPLIER:
48
- return f"{microseconds:.1f}µs"
51
+ return f"{sign}{microseconds:.1f}µs"
49
52
  milliseconds = microseconds / METRIC_MULTIPLIER
50
53
  if milliseconds < METRIC_MULTIPLIER:
51
- return f"{milliseconds:.1f}ms"
54
+ return f"{sign}{milliseconds:.1f}ms"
52
55
  seconds = milliseconds / METRIC_MULTIPLIER
53
56
  if seconds < SECONDS_PER_MINUTE:
54
- return f"{seconds:.2f}s"
57
+ return f"{sign}{seconds:.2f}s"
55
58
  round_seconds = int(seconds)
56
59
  minutes, seconds = divmod(round_seconds, SECONDS_PER_MINUTE)
57
60
  hours, minutes = divmod(minutes, MINUTES_PER_HOUR)
58
61
  if hours < HOURS_PER_DAY:
59
- return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
62
+ return f"{sign}{hours:02d}:{minutes:02d}:{seconds:02d}"
60
63
  days, hours = divmod(hours, HOURS_PER_DAY)
61
- return f"{days:,}d {hours:02d}:{minutes:02d}:{seconds:02d}"
64
+ return f"{sign}{days:,}d {hours:02d}:{minutes:02d}:{seconds:02d}"
62
65
 
63
66
  def __bool__(self) -> bool:
64
67
  return bool(self.nanoseconds)
65
68
 
69
+ def __neg__(self) -> Self:
70
+ return self.__class__(nanoseconds=-self.nanoseconds)
71
+
66
72
  def __add__(self, other: object) -> Timing:
67
73
  if not isinstance(other, Timing):
68
74
  return NotImplemented
@@ -73,6 +79,16 @@ class Timing:
73
79
  return NotImplemented
74
80
  return Timing(nanoseconds=self.nanoseconds + other.nanoseconds)
75
81
 
82
+ def __sub__(self, other: object) -> Timing:
83
+ if not isinstance(other, Timing):
84
+ return NotImplemented
85
+ return Timing(nanoseconds=self.nanoseconds - other.nanoseconds)
86
+
87
+ def __rsub__(self, other: object) -> Timing:
88
+ if not isinstance(other, Timing):
89
+ return NotImplemented
90
+ return Timing(nanoseconds=other.nanoseconds - self.nanoseconds)
91
+
76
92
  def __mul__(self, other: object) -> Timing:
77
93
  if not isinstance(other, int):
78
94
  return NotImplemented
@@ -97,10 +113,11 @@ class Timing:
97
113
  class Stopwatch:
98
114
  _start: int
99
115
  laps: list[Timing]
116
+ __slots__ = ("_start", "_zero", "laps")
100
117
 
101
118
  def __init__(self) -> None:
102
- self._start = 0
103
119
  self.laps: list[Timing] = []
120
+ self._zero = Timing()
104
121
 
105
122
  def __enter__(self) -> Self:
106
123
  self._start = perf_counter_ns()
@@ -112,19 +129,37 @@ class Stopwatch:
112
129
  exc_value: BaseException | None,
113
130
  traceback: TracebackType | None,
114
131
  ) -> None:
115
- _end = perf_counter_ns()
116
- self.laps.append(Timing(nanoseconds=_end - self._start))
132
+ end = perf_counter_ns()
133
+ self.laps.append(Timing(nanoseconds=end - self._start))
134
+ del self._start
117
135
 
118
136
  def __bool__(self) -> bool:
119
137
  return bool(self.elapsed)
120
138
 
139
+ def __len__(self) -> int:
140
+ return len(self.laps)
141
+
142
+ def __iter__(self) -> Iterator[Timing]:
143
+ return iter(self.laps)
144
+
145
+ def reset(self) -> None:
146
+ self.laps.clear()
147
+
121
148
  @property
122
149
  def elapsed(self) -> Timing:
123
- return sum(self.laps, Timing())
150
+ return sum(self.laps, self._zero)
124
151
 
125
152
  @property
126
153
  def average(self) -> Timing:
127
154
  if not self.laps:
128
155
  msg = "No laps recorded"
129
156
  raise ZeroDivisionError(msg)
130
- return self.elapsed // len(self.laps)
157
+ return self.elapsed // len(self)
158
+
159
+ @property
160
+ def min(self) -> Timing:
161
+ return min(self.laps)
162
+
163
+ @property
164
+ def max(self) -> Timing:
165
+ return max(self.laps)
@@ -1,11 +0,0 @@
1
- # BSD 3-Clause License
2
-
3
- Copyright &copy; 2024 Stephanos Kuma.
4
-
5
- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
6
-
7
- 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8
- 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
- 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10
-
11
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
pyutilkit-0.10.0/PKG-INFO DELETED
@@ -1,62 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: pyutilkit
3
- Version: 0.10.0
4
- Summary: python's missing batteries
5
- Home-page: https://pyutilkit.readthedocs.io/en/stable/
6
- License: BSD-3-Clause
7
- Keywords: utils
8
- Author: Stephanos Kuma
9
- Author-email: "Stephanos Kuma" <stephanos@kuma.ai>
10
- Requires-Python: >=3.9
11
- Classifier: Development Status :: 4 - Beta
12
- Classifier: License :: OSI Approved :: BSD License
13
- Classifier: Operating System :: OS Independent
14
- Classifier: Programming Language :: Python :: 3 :: Only
15
- Requires-Dist: tzdata ; os_name == 'nt'
16
- Project-URL: Documentation, https://pyutilkit.readthedocs.io/en/stable/
17
- Project-URL: Repository, https://github.com/spapanik/pyutilkit
18
- Description-Content-Type: text/markdown
19
-
20
- # pyutilkit: python's missing batteries
21
-
22
- [![build][build_badge]][build_url]
23
- [![lint][lint_badge]][lint_url]
24
- [![tests][test_badge]][test_url]
25
- [![license][licence_badge]][licence_url]
26
- [![pypi][pypi_badge]][pypi_url]
27
- [![downloads][pepy_badge]][pepy_url]
28
- [![code style: black][black_badge]][black_url]
29
- [![build automation: yam][yam_badge]][yam_url]
30
- [![Lint: ruff][ruff_badge]][ruff_url]
31
-
32
- The Python has long maintained the philosophy of "batteries included", giving the user
33
- a rich standard library, avoiding the need for third party tools for most work. Some packages
34
- are so common, that the have a similar status to the standard library. Still, some code seems
35
- to be written time and again, with every project. This small library, with minimal requirements,
36
- hopes to stop this repetition.
37
-
38
- ## Links
39
-
40
- - [Documentation]
41
- - [Changelog]
42
-
43
- [build_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml/badge.svg
44
- [build_url]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml
45
- [lint_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml/badge.svg
46
- [lint_url]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml
47
- [test_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml/badge.svg
48
- [test_url]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml
49
- [licence_badge]: https://img.shields.io/pypi/l/pyutilkit
50
- [licence_url]: https://pyutilkit.readthedocs.io/en/stable/LICENSE/
51
- [pypi_badge]: https://img.shields.io/pypi/v/pyutilkit
52
- [pypi_url]: https://pypi.org/project/pyutilkit
53
- [pepy_badge]: https://pepy.tech/badge/pyutilkit
54
- [pepy_url]: https://pepy.tech/project/pyutilkit
55
- [black_badge]: https://img.shields.io/badge/code%20style-black-000000.svg
56
- [black_url]: https://github.com/psf/black
57
- [yam_badge]: https://img.shields.io/badge/build%20automation-yamk-success
58
- [yam_url]: https://github.com/spapanik/yamk
59
- [ruff_badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json
60
- [ruff_url]: https://github.com/charliermarsh/ruff
61
- [Documentation]: https://pyutilkit.readthedocs.io/en/stable/
62
- [Changelog]: https://pyutilkit.readthedocs.io/en/stable/CHANGELOG/
@@ -1,43 +0,0 @@
1
- # pyutilkit: python's missing batteries
2
-
3
- [![build][build_badge]][build_url]
4
- [![lint][lint_badge]][lint_url]
5
- [![tests][test_badge]][test_url]
6
- [![license][licence_badge]][licence_url]
7
- [![pypi][pypi_badge]][pypi_url]
8
- [![downloads][pepy_badge]][pepy_url]
9
- [![code style: black][black_badge]][black_url]
10
- [![build automation: yam][yam_badge]][yam_url]
11
- [![Lint: ruff][ruff_badge]][ruff_url]
12
-
13
- The Python has long maintained the philosophy of "batteries included", giving the user
14
- a rich standard library, avoiding the need for third party tools for most work. Some packages
15
- are so common, that the have a similar status to the standard library. Still, some code seems
16
- to be written time and again, with every project. This small library, with minimal requirements,
17
- hopes to stop this repetition.
18
-
19
- ## Links
20
-
21
- - [Documentation]
22
- - [Changelog]
23
-
24
- [build_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml/badge.svg
25
- [build_url]: https://github.com/spapanik/pyutilkit/actions/workflows/build.yml
26
- [lint_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml/badge.svg
27
- [lint_url]: https://github.com/spapanik/pyutilkit/actions/workflows/lint.yml
28
- [test_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml/badge.svg
29
- [test_url]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml
30
- [licence_badge]: https://img.shields.io/pypi/l/pyutilkit
31
- [licence_url]: https://pyutilkit.readthedocs.io/en/stable/LICENSE/
32
- [pypi_badge]: https://img.shields.io/pypi/v/pyutilkit
33
- [pypi_url]: https://pypi.org/project/pyutilkit
34
- [pepy_badge]: https://pepy.tech/badge/pyutilkit
35
- [pepy_url]: https://pepy.tech/project/pyutilkit
36
- [black_badge]: https://img.shields.io/badge/code%20style-black-000000.svg
37
- [black_url]: https://github.com/psf/black
38
- [yam_badge]: https://img.shields.io/badge/build%20automation-yamk-success
39
- [yam_url]: https://github.com/spapanik/yamk
40
- [ruff_badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json
41
- [ruff_url]: https://github.com/charliermarsh/ruff
42
- [Documentation]: https://pyutilkit.readthedocs.io/en/stable/
43
- [Changelog]: https://pyutilkit.readthedocs.io/en/stable/CHANGELOG/
@@ -1,18 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import cast
4
-
5
-
6
- class Singleton(type):
7
- instance: type[Singleton] | None
8
-
9
- def __init__(
10
- cls, name: str, bases: tuple[type[object], ...], namespace: dict[str, object]
11
- ) -> None:
12
- super().__init__(name, bases, namespace)
13
- cls.instance = None
14
-
15
- def __call__(cls) -> type[Singleton]:
16
- if cls.instance is None:
17
- cls.instance = super().__call__()
18
- return cast(type[Singleton], cls.instance)