pytility 1.0.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.
- pytility/__init__.py +30 -0
- pytility/files.py +54 -0
- pytility/iterables.py +64 -0
- pytility/parsers.py +104 -0
- pytility/py.typed +0 -0
- pytility/strings.py +65 -0
- pytility-1.0.0.dist-info/METADATA +185 -0
- pytility-1.0.0.dist-info/RECORD +10 -0
- pytility-1.0.0.dist-info/WHEEL +4 -0
- pytility-1.0.0.dist-info/licenses/LICENSE +21 -0
pytility/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Initialisations."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version as _version
|
|
4
|
+
|
|
5
|
+
from .files import concat_files
|
|
6
|
+
from .iterables import arg_to_iter, batchify, clear_list, flatten, take_first, window
|
|
7
|
+
from .parsers import parse_bool, parse_date, parse_float, parse_int
|
|
8
|
+
from .strings import normalize_space, to_str, truncate
|
|
9
|
+
|
|
10
|
+
__version__ = _version("pytility")
|
|
11
|
+
VERSION = tuple(int(part) for part in __version__.split("."))
|
|
12
|
+
|
|
13
|
+
__all__ = (
|
|
14
|
+
"VERSION",
|
|
15
|
+
"__version__",
|
|
16
|
+
"arg_to_iter",
|
|
17
|
+
"batchify",
|
|
18
|
+
"clear_list",
|
|
19
|
+
"concat_files",
|
|
20
|
+
"flatten",
|
|
21
|
+
"normalize_space",
|
|
22
|
+
"parse_bool",
|
|
23
|
+
"parse_date",
|
|
24
|
+
"parse_float",
|
|
25
|
+
"parse_int",
|
|
26
|
+
"take_first",
|
|
27
|
+
"to_str",
|
|
28
|
+
"truncate",
|
|
29
|
+
"window",
|
|
30
|
+
)
|
pytility/files.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""File utilities."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from typing import TextIO
|
|
8
|
+
|
|
9
|
+
LOGGER = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
FilePath = str | bytes | os.PathLike | TextIO
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _copy_file(src: FilePath, dst: TextIO, ensure_newline: bool) -> int:
|
|
15
|
+
if isinstance(src, (str, bytes, os.PathLike)):
|
|
16
|
+
LOGGER.debug("copy data from <%s>", src)
|
|
17
|
+
with open(src) as in_file:
|
|
18
|
+
return _copy_file(in_file, dst, ensure_newline)
|
|
19
|
+
|
|
20
|
+
shutil.copyfileobj(src, dst)
|
|
21
|
+
|
|
22
|
+
if not src.tell():
|
|
23
|
+
return 0
|
|
24
|
+
|
|
25
|
+
copied = src.tell()
|
|
26
|
+
|
|
27
|
+
if ensure_newline:
|
|
28
|
+
src.seek(src.tell() - 1)
|
|
29
|
+
|
|
30
|
+
if src.read(1) != "\n":
|
|
31
|
+
dst.write("\n")
|
|
32
|
+
copied += 1
|
|
33
|
+
|
|
34
|
+
return copied
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def concat_files(
|
|
38
|
+
dst: FilePath, srcs: Iterable[FilePath], ensure_newline: bool = False
|
|
39
|
+
) -> int:
|
|
40
|
+
"""Concatenate files, returning the total number of bytes copied."""
|
|
41
|
+
|
|
42
|
+
if isinstance(dst, (str, bytes, os.PathLike)):
|
|
43
|
+
LOGGER.info("concatenating files into <%s>", dst)
|
|
44
|
+
with open(dst, "w") as out_file:
|
|
45
|
+
return concat_files(out_file, srcs, ensure_newline)
|
|
46
|
+
|
|
47
|
+
total = 0
|
|
48
|
+
|
|
49
|
+
for src in srcs:
|
|
50
|
+
total += _copy_file(src, dst, ensure_newline)
|
|
51
|
+
|
|
52
|
+
LOGGER.info("done concatenating, %d bytes in total", total)
|
|
53
|
+
|
|
54
|
+
return total
|
pytility/iterables.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Iterable utilities."""
|
|
2
|
+
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from itertools import groupby, tee
|
|
6
|
+
from typing import Any, TypeVar
|
|
7
|
+
|
|
8
|
+
ITERABLE_SINGLE_VALUES = (dict, str, bytes)
|
|
9
|
+
Typed = TypeVar("Typed")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def clear_list(items: Iterable[Typed | None]) -> list[Typed]:
|
|
13
|
+
"""Return unique items in order of first ocurrence."""
|
|
14
|
+
return list(OrderedDict.fromkeys(filter(None, items)))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def flatten(*args: Any) -> Iterable:
|
|
18
|
+
"""Flatten iterables of iterables recursively."""
|
|
19
|
+
for arg in args:
|
|
20
|
+
if not isinstance(arg, ITERABLE_SINGLE_VALUES) and hasattr(arg, "__iter__"):
|
|
21
|
+
for item in arg:
|
|
22
|
+
yield from flatten(item)
|
|
23
|
+
else:
|
|
24
|
+
yield arg
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def arg_to_iter(arg: Any) -> Iterable:
|
|
28
|
+
"""Wraps arg into tuple if not an iterable."""
|
|
29
|
+
|
|
30
|
+
if arg is None:
|
|
31
|
+
return ()
|
|
32
|
+
|
|
33
|
+
if not isinstance(arg, ITERABLE_SINGLE_VALUES) and hasattr(arg, "__iter__"):
|
|
34
|
+
return arg
|
|
35
|
+
|
|
36
|
+
return (arg,)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def take_first(items):
|
|
40
|
+
"""Take first item that is not None or zero-length str."""
|
|
41
|
+
|
|
42
|
+
for item in arg_to_iter(items):
|
|
43
|
+
if item is not None and item != "":
|
|
44
|
+
return item
|
|
45
|
+
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def batchify(iterable, size):
|
|
50
|
+
"""Make batches of given size."""
|
|
51
|
+
for _, group in groupby(enumerate(iterable), key=lambda x: x[0] // size):
|
|
52
|
+
yield (x[1] for x in group)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def window(iterable: Iterable[Typed], size: int = 2) -> Iterable[tuple[Typed, ...]]:
|
|
56
|
+
"""Sliding window of an iterator."""
|
|
57
|
+
|
|
58
|
+
iterables = tee(iterable, size)
|
|
59
|
+
|
|
60
|
+
for num, itb in enumerate(iterables):
|
|
61
|
+
for _ in range(num):
|
|
62
|
+
next(itb, None)
|
|
63
|
+
|
|
64
|
+
return zip(*iterables, strict=False)
|
pytility/parsers.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Parsers."""
|
|
2
|
+
|
|
3
|
+
from datetime import date as date_cls
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_int(string: Any, base: int = 10) -> int | None:
|
|
9
|
+
"""Safely convert an object to int if possible, else return None."""
|
|
10
|
+
|
|
11
|
+
if isinstance(string, int):
|
|
12
|
+
return string
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
return int(string, base=base)
|
|
16
|
+
except Exception:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
return int(string)
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_float(number: Any) -> float | None:
|
|
28
|
+
"""Safely convert an object to float if possible, else return None."""
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
return float(number)
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_bool(item: Any) -> bool:
|
|
38
|
+
"""Parses an item and converts it to a boolean."""
|
|
39
|
+
|
|
40
|
+
if isinstance(item, int):
|
|
41
|
+
return bool(item)
|
|
42
|
+
if item in ("True", "true", "Yes", "yes"):
|
|
43
|
+
return True
|
|
44
|
+
integer = parse_int(item)
|
|
45
|
+
if integer is not None:
|
|
46
|
+
return bool(integer)
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _add_tz(date: datetime | None, tzinfo: timezone | None = None) -> datetime | None:
|
|
51
|
+
return (
|
|
52
|
+
date if not tzinfo or not date or date.tzinfo else date.replace(tzinfo=tzinfo)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def parse_date(
|
|
57
|
+
date: Any, tzinfo: timezone | None = None, format_str: str | None = None
|
|
58
|
+
) -> datetime | None:
|
|
59
|
+
"""Try to turn input into a datetime object."""
|
|
60
|
+
|
|
61
|
+
if not date:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
# already a datetime
|
|
65
|
+
if isinstance(date, datetime):
|
|
66
|
+
return _add_tz(date, tzinfo)
|
|
67
|
+
|
|
68
|
+
# date without time
|
|
69
|
+
if isinstance(date, date_cls):
|
|
70
|
+
return _add_tz(datetime(date.year, date.month, date.day), tzinfo=tzinfo)
|
|
71
|
+
|
|
72
|
+
# parse as epoch time
|
|
73
|
+
timestamp = parse_float(date)
|
|
74
|
+
if timestamp is not None:
|
|
75
|
+
return datetime.fromtimestamp(timestamp, tzinfo or timezone.utc)
|
|
76
|
+
|
|
77
|
+
if format_str:
|
|
78
|
+
try:
|
|
79
|
+
# parse as string in given format
|
|
80
|
+
return _add_tz(datetime.strptime(date, format_str), tzinfo)
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
import dateutil.parser
|
|
86
|
+
|
|
87
|
+
# parse as string
|
|
88
|
+
return _add_tz(dateutil.parser.parse(date), tzinfo)
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
# parse as (year, month, day, hour, minute, second, microsecond, tzinfo)
|
|
94
|
+
return datetime(*date)
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
# parse as time.struct_time
|
|
100
|
+
return datetime(*date[:6], tzinfo=tzinfo or timezone.utc)
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
return None
|
pytility/py.typed
ADDED
|
File without changes
|
pytility/strings.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""String utilities."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import string as string_lib
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
PRINTABLE_SET = frozenset(string_lib.printable)
|
|
8
|
+
NON_PRINTABLE_SET = frozenset(chr(i) for i in range(128)) - PRINTABLE_SET
|
|
9
|
+
NON_PRINTABLE_TRANSLATE = {ord(character): None for character in NON_PRINTABLE_SET}
|
|
10
|
+
REGEX_WORD = re.compile(r"^\w+")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def to_str(string: Any, encoding: str = "utf-8") -> str | None:
|
|
14
|
+
"""Safely returns either string or None."""
|
|
15
|
+
|
|
16
|
+
string = (
|
|
17
|
+
string
|
|
18
|
+
if isinstance(string, str)
|
|
19
|
+
else string.decode(encoding)
|
|
20
|
+
if isinstance(string, bytes)
|
|
21
|
+
else None
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
return string.translate(NON_PRINTABLE_TRANSLATE) if string is not None else None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def normalize_space(item: Any, preserve_newline: bool = False) -> str:
|
|
28
|
+
"""Normalize space in a string."""
|
|
29
|
+
|
|
30
|
+
item = to_str(item)
|
|
31
|
+
|
|
32
|
+
if not item:
|
|
33
|
+
return ""
|
|
34
|
+
|
|
35
|
+
if preserve_newline:
|
|
36
|
+
return "\n".join(normalize_space(line) for line in item.splitlines()).strip()
|
|
37
|
+
|
|
38
|
+
return " ".join(item.split())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def truncate(
|
|
42
|
+
string: str,
|
|
43
|
+
length: int,
|
|
44
|
+
ellipsis: str = "[…]",
|
|
45
|
+
respect_word: bool = False,
|
|
46
|
+
) -> str:
|
|
47
|
+
"""Truncates a string at given length."""
|
|
48
|
+
|
|
49
|
+
if length < 0 or len(string) <= length + len(ellipsis):
|
|
50
|
+
return string
|
|
51
|
+
|
|
52
|
+
string_trunc = string[:length]
|
|
53
|
+
|
|
54
|
+
if respect_word:
|
|
55
|
+
match = REGEX_WORD.match(string[length:])
|
|
56
|
+
if match:
|
|
57
|
+
string_trunc += match.group(0)
|
|
58
|
+
|
|
59
|
+
if string_trunc.rstrip().endswith(ellipsis):
|
|
60
|
+
return string_trunc.rstrip()
|
|
61
|
+
|
|
62
|
+
if not respect_word or not string_trunc or string_trunc[-1].isspace():
|
|
63
|
+
return string_trunc + ellipsis
|
|
64
|
+
|
|
65
|
+
return f"{string_trunc} {ellipsis}"
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pytility
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A lean collection of Python utilities
|
|
5
|
+
Project-URL: Homepage, https://gitlab.com/mshepherd/pytility
|
|
6
|
+
Project-URL: Documentation, https://gitlab.com/mshepherd/pytility/blob/master/README.md
|
|
7
|
+
Project-URL: Source, https://gitlab.com/mshepherd/pytility
|
|
8
|
+
Project-URL: Tracker, https://gitlab.com/mshepherd/pytility/issues
|
|
9
|
+
Author-email: Markus Shepherd <markus.r.shepherd@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: pytility,utilities,utility
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Provides-Extra: dates
|
|
21
|
+
Requires-Dist: python-dateutil; extra == 'dates'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# 🛠 Pytility 🛠
|
|
25
|
+
|
|
26
|
+
[](https://pypi.python.org/pypi/pytility/)
|
|
27
|
+
[](https://pypi.python.org/pypi/pytility/)
|
|
28
|
+
[](https://pypi.python.org/pypi/pytility/)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
**Source Code**: [https://gitlab.com/mshepherd/pytility](https://gitlab.com/mshepherd/pytility)
|
|
33
|
+
|
|
34
|
+
**PyPI**: [https://pypi.org/project/pytility/](https://pypi.org/project/pytility/)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
A lean collection of Python utilities — small, dependency-free helpers for
|
|
39
|
+
strings, iterables, parsing, and files, with no runtime dependencies unless
|
|
40
|
+
you opt into the `dates` extra.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
pip install pytility
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Date parsing via [`python-dateutil`](https://pypi.org/project/python-dateutil/)
|
|
49
|
+
is an optional extra:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
pip install pytility[dates]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import pytility
|
|
59
|
+
|
|
60
|
+
pytility.normalize_space(" too much space ") # "too much space"
|
|
61
|
+
pytility.truncate("a long string", 6) # "a long[…]"
|
|
62
|
+
pytility.parse_bool("yes") # True
|
|
63
|
+
pytility.parse_int("2a", base=16) # 42
|
|
64
|
+
pytility.clear_list([1, 1, None, 2, "", 3]) # [1, 2, 3]
|
|
65
|
+
list(pytility.flatten([1, [2, [3, 4]], 5])) # [1, 2, 3, 4, 5]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### API reference
|
|
69
|
+
|
|
70
|
+
#### Strings (`pytility.strings`)
|
|
71
|
+
|
|
72
|
+
* `to_str(string, encoding="utf-8") -> str | None` — coerces `str` or
|
|
73
|
+
`bytes` to `str`, stripping non-printable characters; anything else
|
|
74
|
+
returns `None`.
|
|
75
|
+
* `normalize_space(item, preserve_newline=False) -> str` — collapses runs of
|
|
76
|
+
whitespace to single spaces; with `preserve_newline=True`, newlines are
|
|
77
|
+
kept but whitespace is normalized within each line.
|
|
78
|
+
* `truncate(string, length, ellipsis="[…]", respect_word=False) -> str` —
|
|
79
|
+
truncates `string` to `length` characters plus an ellipsis; with
|
|
80
|
+
`respect_word=True`, extends the cut to the end of the word it lands in
|
|
81
|
+
rather than splitting it.
|
|
82
|
+
|
|
83
|
+
#### Iterables (`pytility.iterables`)
|
|
84
|
+
|
|
85
|
+
* `arg_to_iter(arg) -> Iterable` — wraps a non-iterable (or a `str`/`bytes`/
|
|
86
|
+
`dict`, which are treated as scalars) in a 1-tuple; `None` becomes `()`;
|
|
87
|
+
anything else iterable is returned as-is.
|
|
88
|
+
* `clear_list(items) -> list` — unique items in order of first occurrence,
|
|
89
|
+
with falsy values (`None`, `""`, `0`, …) dropped.
|
|
90
|
+
* `flatten(*args) -> Iterable` — recursively flattens nested iterables;
|
|
91
|
+
`str`/`bytes`/`dict` are treated as scalars, not flattened further.
|
|
92
|
+
* `take_first(items) -> Any | None` — the first item that isn't `None` or
|
|
93
|
+
`""`, or `None` if there isn't one.
|
|
94
|
+
* `batchify(iterable, size) -> Iterable[Iterable]` — splits `iterable` into
|
|
95
|
+
chunks of at most `size` items.
|
|
96
|
+
* `window(iterable, size=2) -> Iterable[tuple]` — a sliding window of
|
|
97
|
+
`size` consecutive items over `iterable`.
|
|
98
|
+
|
|
99
|
+
#### Parsers (`pytility.parsers`)
|
|
100
|
+
|
|
101
|
+
* `parse_int(string, base=10) -> int | None` — safely converts to `int`,
|
|
102
|
+
or `None` on failure.
|
|
103
|
+
* `parse_float(number) -> float | None` — safely converts to `float`, or
|
|
104
|
+
`None` on failure.
|
|
105
|
+
* `parse_bool(item) -> bool` — recognizes `int`s, the strings `"True"`,
|
|
106
|
+
`"true"`, `"Yes"`, `"yes"`, and anything else `parse_int` can parse as
|
|
107
|
+
non-zero; everything else is `False`.
|
|
108
|
+
* `parse_date(date, tzinfo=None, format_str=None) -> datetime | None` —
|
|
109
|
+
parses `datetime`/`date` objects, epoch timestamps, a given
|
|
110
|
+
`format_str`, free-form strings (via `python-dateutil`, if installed),
|
|
111
|
+
or `(year, month, day, ...)` tuples / `time.struct_time`. Falsy input
|
|
112
|
+
(including `0`) returns `None` before any parsing is attempted.
|
|
113
|
+
|
|
114
|
+
#### Files (`pytility.files`)
|
|
115
|
+
|
|
116
|
+
* `concat_files(dst, srcs, ensure_newline=False) -> int` — concatenates
|
|
117
|
+
`srcs` into `dst` (paths or open file objects), returning the total
|
|
118
|
+
bytes written. With `ensure_newline=True`, a newline is appended after
|
|
119
|
+
every source that doesn't already end in one — including the last.
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
* Clone this repository
|
|
124
|
+
* Requirements:
|
|
125
|
+
* [uv](https://docs.astral.sh/uv/)
|
|
126
|
+
* Python 3.10+
|
|
127
|
+
* Create a virtual environment and install the dependencies
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
uv sync
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Testing
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
uv run pytest
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Pre-commit
|
|
140
|
+
|
|
141
|
+
Pre-commit hooks run the auto-formatter and linter (`ruff`), the type
|
|
142
|
+
checker (`ty`), and other housekeeping checks so the changeset is in good
|
|
143
|
+
shape before a commit happens.
|
|
144
|
+
|
|
145
|
+
Install the hooks with (runs on every commit):
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
uv run pre-commit install
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Or run all checks manually against every file:
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
uv run pre-commit run --all-files
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
CI (`.gitlab-ci.yml`) runs the same lint hooks, the test suite across
|
|
158
|
+
Python 3.10–3.14, and a package build on every push.
|
|
159
|
+
|
|
160
|
+
### Releasing
|
|
161
|
+
|
|
162
|
+
This project is published to PyPI manually — there's no CI publish job or
|
|
163
|
+
GitHub-style draft release here, just these steps run locally:
|
|
164
|
+
|
|
165
|
+
```sh
|
|
166
|
+
# 1. Bump version (major|minor|patch|stable|alpha|beta|rc|post|dev, or pass an explicit value)
|
|
167
|
+
uv version --bump patch
|
|
168
|
+
VERSION=$(uv version --short)
|
|
169
|
+
|
|
170
|
+
# 2. Commit the version bump
|
|
171
|
+
git add pyproject.toml uv.lock
|
|
172
|
+
git commit -m "Release $VERSION"
|
|
173
|
+
|
|
174
|
+
# 3. Tag and push to GitLab (the only remote this repo has)
|
|
175
|
+
git tag "v$VERSION"
|
|
176
|
+
git push gitlab master
|
|
177
|
+
git push gitlab "v$VERSION"
|
|
178
|
+
|
|
179
|
+
# 4. Build and publish to PyPI (needs a token, e.g. via a UV_PUBLISH_TOKEN env var)
|
|
180
|
+
uv build
|
|
181
|
+
uv publish
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Note that `uv version` only edits `pyproject.toml`; `uv.lock` is re-synced
|
|
185
|
+
alongside it since it also records the project's own version.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pytility/__init__.py,sha256=u_fOHyunMkqfgMDa1Dr0QnYf2rD40QqabhURJMEoa9k,709
|
|
2
|
+
pytility/files.py,sha256=SPKRn60zMcuUlVIEVhOsKhCM_iGmjf3gsJNBK6Uk3LA,1307
|
|
3
|
+
pytility/iterables.py,sha256=uQPC9X_WoHgJU6HzBm30MypaCj8LjtJtbF_WZiRnz4A,1676
|
|
4
|
+
pytility/parsers.py,sha256=YEiGDZ2PUPI783A7flyZUmR6xrSjauQ8I0-uSj2tdSg,2493
|
|
5
|
+
pytility/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
pytility/strings.py,sha256=DWbK9XlAII43ued2HUj670rBMVBaI7hoUuLq-ke8ScI,1672
|
|
7
|
+
pytility-1.0.0.dist-info/METADATA,sha256=G2y9RQMc1LIzX4ewrdiMinFNP4ci02dBkd62dP95M8M,6381
|
|
8
|
+
pytility-1.0.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
9
|
+
pytility-1.0.0.dist-info/licenses/LICENSE,sha256=WcYkIFKvE2InORAE70AfXkBoQqXLLRsXETBDkNkwxNc,1072
|
|
10
|
+
pytility-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Markus Shepherd
|
|
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.
|