pyutilkit 0.5.0__py3-none-any.whl

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.

Potentially problematic release.


This version of pyutilkit might be problematic. Click here for more details.

pyutilkit/__init__.py ADDED
File without changes
pyutilkit/classes.py ADDED
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class Singleton(type):
7
+ instance: type[Singleton] | None
8
+
9
+ def __init__(
10
+ cls, name: str, bases: tuple[type[Any], ...], namespace: dict[str, Any]
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 cls.instance
File without changes
@@ -0,0 +1,195 @@
1
+ DEPRECATED_TIMEZONES = {
2
+ "Africa/Asmera",
3
+ "Africa/Timbuktu",
4
+ "America/Argentina/ComodRivadavia",
5
+ "America/Atka",
6
+ "America/Buenos_Aires",
7
+ "America/Catamarca",
8
+ "America/Coral_Harbour",
9
+ "America/Cordoba",
10
+ "America/Ensenada",
11
+ "America/Fort_Wayne",
12
+ "America/Godthab",
13
+ "America/Indianapolis",
14
+ "America/Jujuy",
15
+ "America/Knox_IN",
16
+ "America/Louisville",
17
+ "America/Mendoza",
18
+ "America/Montreal",
19
+ "America/Porto_Acre",
20
+ "America/Rosario",
21
+ "America/Santa_Isabel",
22
+ "America/Shiprock",
23
+ "America/Virgin",
24
+ "Antarctica/South_Pole",
25
+ "Asia/Ashkhabad",
26
+ "Asia/Calcutta",
27
+ "Asia/Chongqing",
28
+ "Asia/Chungking",
29
+ "Asia/Dacca",
30
+ "Asia/Harbin",
31
+ "Asia/Istanbul",
32
+ "Asia/Kashgar",
33
+ "Asia/Katmandu",
34
+ "Asia/Macao",
35
+ "Asia/Rangoon",
36
+ "Asia/Saigon",
37
+ "Asia/Tel_Aviv",
38
+ "Asia/Thimbu",
39
+ "Asia/Ujung_Pandang",
40
+ "Asia/Ulan_Bator",
41
+ "Atlantic/Faeroe",
42
+ "Atlantic/Jan_Mayen",
43
+ "Australia/ACT",
44
+ "Australia/Canberra",
45
+ "Australia/Currie",
46
+ "Australia/LHI",
47
+ "Australia/NSW",
48
+ "Australia/North",
49
+ "Australia/Queensland",
50
+ "Australia/South",
51
+ "Australia/Tasmania",
52
+ "Australia/Victoria",
53
+ "Australia/West",
54
+ "Australia/Yancowinna",
55
+ "Brazil/Acre",
56
+ "Brazil/DeNoronha",
57
+ "Brazil/East",
58
+ "Brazil/West",
59
+ "CET",
60
+ "CST6CDT",
61
+ "Canada/Atlantic",
62
+ "Canada/Central",
63
+ "Canada/Eastern",
64
+ "Canada/Mountain",
65
+ "Canada/Newfoundland",
66
+ "Canada/Pacific",
67
+ "Canada/Saskatchewan",
68
+ "Canada/Yukon",
69
+ "Chile/Continental",
70
+ "Chile/EasterIsland",
71
+ "Cuba",
72
+ "EET",
73
+ "EST",
74
+ "EST5EDT",
75
+ "Egypt",
76
+ "Eire",
77
+ "Etc/GMT",
78
+ "Etc/GMT+0",
79
+ "Etc/GMT+1",
80
+ "Etc/GMT+10",
81
+ "Etc/GMT+11",
82
+ "Etc/GMT+12",
83
+ "Etc/GMT+2",
84
+ "Etc/GMT+3",
85
+ "Etc/GMT+4",
86
+ "Etc/GMT+5",
87
+ "Etc/GMT+6",
88
+ "Etc/GMT+7",
89
+ "Etc/GMT+8",
90
+ "Etc/GMT+9",
91
+ "Etc/GMT-0",
92
+ "Etc/GMT-1",
93
+ "Etc/GMT-10",
94
+ "Etc/GMT-11",
95
+ "Etc/GMT-12",
96
+ "Etc/GMT-13",
97
+ "Etc/GMT-14",
98
+ "Etc/GMT-2",
99
+ "Etc/GMT-3",
100
+ "Etc/GMT-4",
101
+ "Etc/GMT-5",
102
+ "Etc/GMT-6",
103
+ "Etc/GMT-7",
104
+ "Etc/GMT-8",
105
+ "Etc/GMT-9",
106
+ "Etc/GMT0",
107
+ "Etc/Greenwich",
108
+ "Etc/UCT",
109
+ "Etc/UTC",
110
+ "Etc/Universal",
111
+ "Etc/Zulu",
112
+ "Europe/Belfast",
113
+ "Europe/Nicosia",
114
+ "Europe/Tiraspol",
115
+ "Factory",
116
+ "GB",
117
+ "GB-Eire",
118
+ "GMT",
119
+ "GMT+0",
120
+ "GMT-0",
121
+ "GMT0",
122
+ "Greenwich",
123
+ "HST",
124
+ "Hongkong",
125
+ "Iceland",
126
+ "Iran",
127
+ "Israel",
128
+ "Jamaica",
129
+ "Japan",
130
+ "Kwajalein",
131
+ "Libya",
132
+ "MET",
133
+ "MST",
134
+ "MST7MDT",
135
+ "Mexico/BajaNorte",
136
+ "Mexico/BajaSur",
137
+ "Mexico/General",
138
+ "NZ",
139
+ "NZ-CHAT",
140
+ "Navajo",
141
+ "PRC",
142
+ "PST8PDT",
143
+ "Pacific/Johnston",
144
+ "Pacific/Ponape",
145
+ "Pacific/Samoa",
146
+ "Pacific/Truk",
147
+ "Pacific/Yap",
148
+ "Poland",
149
+ "Portugal",
150
+ "ROC",
151
+ "ROK",
152
+ "Singapore",
153
+ "Turkey",
154
+ "UCT",
155
+ "US/Alaska",
156
+ "US/Aleutian",
157
+ "US/Arizona",
158
+ "US/Central",
159
+ "US/East-Indiana",
160
+ "US/Eastern",
161
+ "US/Hawaii",
162
+ "US/Indiana-Starke",
163
+ "US/Michigan",
164
+ "US/Mountain",
165
+ "US/Pacific",
166
+ "US/Samoa",
167
+ "Universal",
168
+ "W-SU",
169
+ "WET",
170
+ "Zulu",
171
+ }
172
+ # Timezones that are removed from the IANA db, but still appear in some systems
173
+ NON_IANA_TIMEZONES = {
174
+ "SystemV/AST4",
175
+ "SystemV/AST4ADT",
176
+ "SystemV/CST6",
177
+ "SystemV/CST6CDT",
178
+ "SystemV/EST5",
179
+ "SystemV/EST5EDT",
180
+ "SystemV/HST10",
181
+ "SystemV/MST7",
182
+ "SystemV/MST7MDT",
183
+ "SystemV/PST8",
184
+ "SystemV/PST8PDT",
185
+ "SystemV/YST9",
186
+ "SystemV/YST9YDT",
187
+ "localtime",
188
+ }
189
+ # Timezones that don't appear in all systems yet
190
+ PROPOSED_TIMEZONES = {"Pacific/Kanton"}
191
+
192
+ # All unavailable timezones
193
+ UNAVAILABLE_TIMEZONES = set.union(
194
+ DEPRECATED_TIMEZONES, NON_IANA_TIMEZONES, PROPOSED_TIMEZONES
195
+ )
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, tzinfo
4
+ from zoneinfo import ZoneInfo, available_timezones
5
+
6
+ from pyutilkit.data.timezones import UNAVAILABLE_TIMEZONES
7
+
8
+ UTC = ZoneInfo("UTC")
9
+
10
+
11
+ def get_timezones() -> set[str]:
12
+ """
13
+ Get all the available timezones
14
+
15
+ This takes into accounts timezones that might not be present in
16
+ all systems.
17
+ """
18
+ return available_timezones() - UNAVAILABLE_TIMEZONES
19
+
20
+
21
+ def now(tz_info: ZoneInfo = UTC) -> datetime:
22
+ return datetime.now(UTC).astimezone(tz_info)
23
+
24
+
25
+ def from_iso(date_string: str, tz_info: tzinfo = UTC) -> datetime:
26
+ """
27
+ Get datetime from an iso string
28
+
29
+ This to allow the Zulu timezone, which is a valid ISO timezone.
30
+ """
31
+ date_string = date_string.replace("Z", "+00:00")
32
+ dt = datetime.fromisoformat(date_string)
33
+ try:
34
+ return add_timezone(dt, tz_info)
35
+ except ValueError:
36
+ return convert_timezone(dt, tz_info)
37
+
38
+
39
+ def from_timestamp(timestamp: float, tz_info: tzinfo = UTC) -> datetime:
40
+ """
41
+ Get a datetime tz-aware time object from a timestamp
42
+ """
43
+ utc_dt = datetime.fromtimestamp(timestamp, tz=UTC)
44
+ return convert_timezone(utc_dt, tz_info)
45
+
46
+
47
+ def add_timezone(dt: datetime, tz_info: tzinfo = UTC) -> datetime:
48
+ """
49
+ Add a timezone to a naive datetime
50
+
51
+ Raise an error in case of a tz-aware datetime
52
+ """
53
+ if dt.tzinfo is not None:
54
+ msg = f"{dt} is already tz-aware"
55
+ raise ValueError(msg)
56
+ return dt.replace(tzinfo=tz_info)
57
+
58
+
59
+ def convert_timezone(dt: datetime, tz_info: tzinfo = UTC) -> datetime:
60
+ """
61
+ Change the timezone of a tz-aware datetime
62
+
63
+ Raise an error in case of a naive datetime
64
+ """
65
+ if dt.tzinfo is None:
66
+ msg = f"{dt} is a naive datetime"
67
+ raise ValueError(msg)
68
+ return dt.astimezone(tz_info)
pyutilkit/files.py ADDED
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import logging
5
+ from functools import wraps
6
+ from typing import TYPE_CHECKING, TypeVar
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Callable
10
+ from pathlib import Path
11
+
12
+ from typing_extensions import ParamSpec # py3.9: import from typing
13
+
14
+ P = ParamSpec("P")
15
+
16
+ logger = logging.getLogger(__name__)
17
+ INGEST_ERROR = "Function `%s` threw `%s` when called with args=%s and kwargs=%s"
18
+ R_co = TypeVar("R_co", covariant=True)
19
+
20
+
21
+ def handle_exceptions(
22
+ *,
23
+ exceptions: tuple[type[Exception], ...] = (Exception,),
24
+ default: R_co | None = None,
25
+ log_level: str = "info",
26
+ ) -> Callable[[Callable[P, R_co]], Callable[P, R_co | None]]:
27
+ def decorator(func: Callable[P, R_co]) -> Callable[P, R_co | None]:
28
+ @wraps(func)
29
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R_co | None:
30
+ try:
31
+ return func(*args, **kwargs)
32
+ except exceptions as exc:
33
+ getattr(logger, log_level)(
34
+ INGEST_ERROR,
35
+ func.__name__,
36
+ exc.__class__.__name__,
37
+ args,
38
+ kwargs,
39
+ exc_info=True,
40
+ )
41
+ return default
42
+
43
+ return wrapper
44
+
45
+ return decorator
46
+
47
+
48
+ def hash_file(path: Path, buffer_size: int = 2**16) -> str:
49
+ sha256 = hashlib.sha256()
50
+
51
+ with path.open("rb") as f:
52
+ while data := f.read(buffer_size):
53
+ sha256.update(data)
54
+
55
+ return sha256.hexdigest()
pyutilkit/py.typed ADDED
File without changes
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from dataclasses import dataclass
5
+ from subprocess import PIPE, Popen
6
+ from typing import Any
7
+
8
+ from pyutilkit.timing import Stopwatch, Timing
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class ProcessOutput:
13
+ stdout: bytes
14
+ stderr: bytes
15
+ pid: int
16
+ returncode: int
17
+ elapsed: Timing
18
+
19
+
20
+ def run_command(command: str | list[str], **kwargs: Any) -> ProcessOutput:
21
+ if kwargs.setdefault("stdout", PIPE) != PIPE:
22
+ msg = "stdout must be set to PIPE"
23
+ raise ValueError(msg)
24
+ if kwargs.setdefault("stderr", PIPE) != PIPE:
25
+ msg = "stderr must be set to PIPE"
26
+ raise ValueError(msg)
27
+
28
+ stdout = []
29
+ stderr = []
30
+ stopwatch = Stopwatch()
31
+ with stopwatch:
32
+ process = Popen(command, **kwargs) # noqa: S603
33
+
34
+ for line in process.stdout or []:
35
+ sys.stdout.buffer.write(line)
36
+ sys.stdout.flush()
37
+ stdout.append(line)
38
+
39
+ for line in process.stderr or []:
40
+ sys.stderr.buffer.write(line)
41
+ sys.stderr.flush()
42
+ stderr.append(line)
43
+
44
+ with stopwatch:
45
+ process.wait()
46
+
47
+ return ProcessOutput(
48
+ stdout=b"".join(stdout),
49
+ stderr=b"".join(stderr),
50
+ pid=process.pid,
51
+ returncode=process.returncode,
52
+ elapsed=stopwatch.elapsed,
53
+ )
pyutilkit/term.py ADDED
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from enum import IntEnum, unique
5
+ from math import ceil, floor
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Iterable
10
+
11
+ from typing_extensions import Self # py3.10: import from typing
12
+
13
+
14
+ @unique
15
+ class SGRCodes(IntEnum):
16
+ RESET = 0
17
+
18
+ BOLD = 1
19
+ ITALIC = 3
20
+ UNDERLINE = 4
21
+ BLINK = 5
22
+ REVERSE = 7
23
+ CONCEAL = 8
24
+
25
+ BLACK = 30
26
+ RED = 31
27
+ GREEN = 32
28
+ YELLOW = 33
29
+ BLUE = 34
30
+ MAGENTA = 35
31
+ CYAN = 36
32
+ GREY = 37
33
+
34
+ BG_BLACK = 40
35
+ BG_RED = 41
36
+ BG_GREEN = 42
37
+ BG_YELLOW = 43
38
+ BG_BLUE = 44
39
+ BG_MAGENTA = 45
40
+ BG_CYAN = 46
41
+ BG_GREY = 47
42
+
43
+ BLACK_BRIGHT = 90
44
+ RED_BRIGHT = 91
45
+ GREEN_BRIGHT = 92
46
+ YELLOW_BRIGHT = 93
47
+ BLUE_BRIGHT = 94
48
+ MAGENTA_BRIGHT = 95
49
+ CYAN_BRIGHT = 96
50
+ WHITE_BRIGHT = 97
51
+
52
+ BG_BLACK_BRIGHT = 100
53
+ BG_RED_BRIGHT = 101
54
+ BG_GREEN_BRIGHT = 102
55
+ BG_YELLOW_BRIGHT = 103
56
+ BG_BLUE_BRIGHT = 104
57
+ BG_MAGENTA_BRIGHT = 105
58
+ BG_CYAN_BRIGHT = 106
59
+ BG_WHITE_BRIGHT = 107
60
+
61
+ @property
62
+ def sequence(self) -> str:
63
+ return f"\033[{self.value}m"
64
+
65
+
66
+ class SGRString(str):
67
+ _sgr: tuple[SGRCodes, ...]
68
+ _string: str
69
+ __slots__ = ("_sgr", "_string")
70
+
71
+ def __new__(cls, obj: Any, *, params: Iterable[SGRCodes] = ()) -> Self:
72
+ string = super().__new__(cls, obj)
73
+ object.__setattr__(string, "_string", str(obj))
74
+ object.__setattr__(string, "_sgr", tuple(params))
75
+ return string
76
+
77
+ def __setattr__(self, name: str, value: Any) -> None:
78
+ msg = "SGRString is immutable"
79
+ raise AttributeError(msg)
80
+
81
+ def __delattr__(self, name: str) -> None:
82
+ msg = "SGRString is immutable"
83
+ raise AttributeError(msg)
84
+
85
+ def __str__(self) -> str:
86
+ if not self._sgr:
87
+ return self._string
88
+ prefix = "".join(code.sequence for code in self._sgr)
89
+ return f"{prefix}{self._string}{SGRCodes.RESET.sequence}"
90
+
91
+ def __mul__(self, other: Any) -> Self:
92
+ if not isinstance(other, int):
93
+ return NotImplemented
94
+ return type(self)(self._string * other, params=self._sgr)
95
+
96
+ def __rmul__(self, other: Any) -> Self:
97
+ if not isinstance(other, int):
98
+ return NotImplemented
99
+ return type(self)(self._string * other, params=self._sgr)
100
+
101
+ def header(
102
+ self,
103
+ *,
104
+ padding: str = " ",
105
+ left_spaces: int = 1,
106
+ right_spaces: int = 1,
107
+ space: str = " ",
108
+ ) -> None:
109
+ columns = os.get_terminal_size().columns
110
+ text = f"{space * left_spaces}{self}{space * right_spaces}"
111
+ title_length = left_spaces + len(self) + right_spaces
112
+ if title_length >= columns:
113
+ print(text.strip())
114
+ return
115
+
116
+ half = (columns - title_length) / 2
117
+ print(f"{padding * ceil(half)}{text}{padding * floor(half)}")
pyutilkit/timing.py ADDED
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations # py3.9: remove this line
2
+
3
+ from dataclasses import dataclass
4
+ from time import perf_counter_ns
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from types import TracebackType
9
+
10
+ from typing_extensions import Self # py3.10: import Self from typing
11
+
12
+
13
+ @dataclass(frozen=True, order=True)
14
+ class Timing:
15
+ __slots__ = ("nanoseconds",) # py3.9: remove this line
16
+
17
+ nanoseconds: int
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ days: int = 0,
23
+ seconds: int = 0,
24
+ milliseconds: int = 0,
25
+ microseconds: int = 0,
26
+ nanoseconds: int = 0,
27
+ ) -> None:
28
+ total_nanoseconds = (
29
+ nanoseconds
30
+ + 1000 * microseconds
31
+ + 1_000_000 * milliseconds
32
+ + 1_000_000_000 * seconds
33
+ + 86_400_000_000_000 * days
34
+ )
35
+ object.__setattr__(self, "nanoseconds", total_nanoseconds)
36
+
37
+ def __str__(self) -> str:
38
+ if self.nanoseconds < 1000:
39
+ return f"{self.nanoseconds}ns"
40
+ microseconds = self.nanoseconds / 1000
41
+ if microseconds < 1000:
42
+ return f"{microseconds:.1f}µs"
43
+ milliseconds = microseconds / 1000
44
+ if milliseconds < 1000:
45
+ return f"{milliseconds:.1f}ms"
46
+ seconds = milliseconds / 1000
47
+ if seconds < 60:
48
+ return f"{seconds:.2f}s"
49
+ round_seconds = int(seconds)
50
+ minutes, seconds = divmod(round_seconds, 60)
51
+ hours, minutes = divmod(minutes, 60)
52
+ if hours < 24:
53
+ return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
54
+ days, hours = divmod(hours, 24)
55
+ return f"{days:,}d {hours:02d}:{minutes:02d}:{seconds:02d}"
56
+
57
+ def __bool__(self) -> bool:
58
+ return bool(self.nanoseconds)
59
+
60
+ def __add__(self, other: Any) -> Timing:
61
+ if not isinstance(other, Timing):
62
+ return NotImplemented
63
+ return Timing(nanoseconds=self.nanoseconds + other.nanoseconds)
64
+
65
+ def __radd__(self, other: Any) -> Timing:
66
+ if not isinstance(other, Timing):
67
+ return NotImplemented
68
+ return Timing(nanoseconds=self.nanoseconds + other.nanoseconds)
69
+
70
+ def __mul__(self, other: Any) -> Timing:
71
+ if not isinstance(other, int):
72
+ return NotImplemented
73
+ return Timing(nanoseconds=self.nanoseconds * other)
74
+
75
+ def __rmul__(self, other: Any) -> Timing:
76
+ if not isinstance(other, int):
77
+ return NotImplemented
78
+ return Timing(nanoseconds=self.nanoseconds * other)
79
+
80
+ def __floordiv__(self, other: Any) -> Timing:
81
+ if not isinstance(other, int):
82
+ return NotImplemented
83
+ return Timing(nanoseconds=self.nanoseconds // other)
84
+
85
+ def __truediv__(self, other: Any) -> Timing:
86
+ if not isinstance(other, int):
87
+ return NotImplemented
88
+ return Timing(nanoseconds=self.nanoseconds // other)
89
+
90
+
91
+ class Stopwatch:
92
+ _start: int
93
+ laps: list[Timing]
94
+
95
+ def __init__(self) -> None:
96
+ self._start = 0
97
+ self.laps: list[Timing] = []
98
+
99
+ def __enter__(self) -> Self:
100
+ self._start = perf_counter_ns()
101
+ return self
102
+
103
+ def __exit__(
104
+ self,
105
+ exc_type: type[Exception] | None,
106
+ exc_value: Exception | None,
107
+ traceback: TracebackType | None,
108
+ ) -> None:
109
+ _end = perf_counter_ns()
110
+ self.laps.append(Timing(nanoseconds=_end - self._start))
111
+
112
+ def __bool__(self) -> bool:
113
+ return bool(self.elapsed)
114
+
115
+ @property
116
+ def elapsed(self) -> Timing:
117
+ return sum(self.laps, Timing())
118
+
119
+ @property
120
+ def average(self) -> Timing:
121
+ if not self.laps:
122
+ msg = "No laps recorded"
123
+ raise ZeroDivisionError(msg)
124
+ return self.elapsed // len(self.laps)
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.3
2
+ Name: pyutilkit
3
+ Version: 0.5.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: Operating System :: OS Independent
13
+ Requires-Dist: pyutilkit (~=0.4)
14
+ Project-URL: Documentation, https://pyutilkit.readthedocs.io/en/stable/
15
+ Project-URL: Repository, https://github.com/spapanik/pyutilkit
16
+ Description-Content-Type: text/markdown
17
+
18
+ # pyutilkit: python's missing batteries
19
+
20
+ [![tests][test_badge]][test_url]
21
+ [![license][licence_badge]][licence_url]
22
+ [![pypi][pypi_badge]][pypi_url]
23
+ [![downloads][pepy_badge]][pepy_url]
24
+ [![code style: black][black_badge]][black_url]
25
+ [![build automation: yam][yam_badge]][yam_url]
26
+ [![Lint: ruff][ruff_badge]][ruff_url]
27
+
28
+ The Python has long maintained the philosophy of "batteries included", giving the user
29
+ a rich standard library, avoiding the need for third party tools for most work. Some packages
30
+ are so common, that the have a similar status to the standard library. Still, some code seems
31
+ to be written time and again, with every project. This small library, with minimal requirements,
32
+ hopes to stop this repetition.
33
+
34
+ ## Links
35
+
36
+ - [Documentation]
37
+ - [Changelog]
38
+
39
+ [test_badge]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml/badge.svg
40
+ [test_url]: https://github.com/spapanik/pyutilkit/actions/workflows/tests.yml
41
+ [licence_badge]: https://img.shields.io/pypi/l/pyutilkit
42
+ [licence_url]: https://github.com/spapanik/pyutilkit/blob/main/docs/LICENSE.md
43
+ [pypi_badge]: https://img.shields.io/pypi/v/pyutilkit
44
+ [pypi_url]: https://pypi.org/project/pyutilkit
45
+ [pepy_badge]: https://pepy.tech/badge/pyutilkit
46
+ [pepy_url]: https://pepy.tech/project/pyutilkit
47
+ [black_badge]: https://img.shields.io/badge/code%20style-black-000000.svg
48
+ [black_url]: https://github.com/psf/black
49
+ [yam_badge]: https://img.shields.io/badge/build%20automation-yamk-success
50
+ [yam_url]: https://github.com/spapanik/yamk
51
+ [ruff_badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json
52
+ [ruff_url]: https://github.com/charliermarsh/ruff
53
+ [Documentation]: https://pyutilkit.readthedocs.io/en/stable/
54
+ [Changelog]: https://github.com/spapanik/pyutilkit/blob/main/docs/CHANGELOG.md
@@ -0,0 +1,13 @@
1
+ pyutilkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ pyutilkit/classes.py,sha256=DKGhYjpXXEr9BU8HFRygLJ4TPTIu0zp1bwL4TBTE_L4,462
3
+ pyutilkit/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ pyutilkit/data/timezones.py,sha256=p67f7VXrKCWNoKv8NQ4yEHOFI_zV9zpViEBsiC_IE-U,3866
5
+ pyutilkit/date_utils.py,sha256=1E7Bq_N53GO6HA3ukgsWh0jhy2sxgJ-k52I-ub2BSU8,1820
6
+ pyutilkit/files.py,sha256=UjlZcDVHm0hfT3nDAnWVvuhjJUuROcScUuBf2k_b_Ls,1532
7
+ pyutilkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ pyutilkit/subprocess.py,sha256=NCx0bK8lcHojt9PaZ4GQ7c822UMA1Rb0YDWgOw0S0-k,1322
9
+ pyutilkit/term.py,sha256=JkfFqGD0wM5pjpIafNE_3FH4nMGgOo7OxF1Atl7DpkI,2900
10
+ pyutilkit/timing.py,sha256=bgtstiXSnJpfZj87e7Lz5DllSw-b07nVnvnBF9BWhz4,3746
11
+ pyutilkit-0.5.0.dist-info/METADATA,sha256=ZsRUuK_ThYjORlJMBBIRgRw78NGUHfEbSd1PvvX-FkE,2400
12
+ pyutilkit-0.5.0.dist-info/WHEEL,sha256=k5X_cv0nlsobEJ2RWm08o6BGpyHHSLnOdq47ItFx1Pg,87
13
+ pyutilkit-0.5.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: phosphorus 0.4.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any