Metadata-Version: 2.5
Name: pytility
Version: 1.0.0
Summary: A lean collection of Python utilities
Project-URL: Homepage, https://gitlab.com/mshepherd/pytility
Project-URL: Documentation, https://gitlab.com/mshepherd/pytility/blob/master/README.md
Project-URL: Source, https://gitlab.com/mshepherd/pytility
Project-URL: Tracker, https://gitlab.com/mshepherd/pytility/issues
Author-email: Markus Shepherd <markus.r.shepherd@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: pytility,utilities,utility
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.10
Provides-Extra: dates
Requires-Dist: python-dateutil; extra == 'dates'
Description-Content-Type: text/markdown

# 🛠 Pytility 🛠

[![PyPI](https://img.shields.io/pypi/v/pytility?style=flat-square)](https://pypi.python.org/pypi/pytility/)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pytility?style=flat-square)](https://pypi.python.org/pypi/pytility/)
[![PyPI - License](https://img.shields.io/pypi/l/pytility?style=flat-square)](https://pypi.python.org/pypi/pytility/)

---

**Source Code**: [https://gitlab.com/mshepherd/pytility](https://gitlab.com/mshepherd/pytility)

**PyPI**: [https://pypi.org/project/pytility/](https://pypi.org/project/pytility/)

---

A lean collection of Python utilities — small, dependency-free helpers for
strings, iterables, parsing, and files, with no runtime dependencies unless
you opt into the `dates` extra.

## Installation

```sh
pip install pytility
```

Date parsing via [`python-dateutil`](https://pypi.org/project/python-dateutil/)
is an optional extra:

```sh
pip install pytility[dates]
```

## Usage

```python
import pytility

pytility.normalize_space("  too   much   space  ")  # "too much space"
pytility.truncate("a long string", 6)  # "a long[…]"
pytility.parse_bool("yes")  # True
pytility.parse_int("2a", base=16)  # 42
pytility.clear_list([1, 1, None, 2, "", 3])  # [1, 2, 3]
list(pytility.flatten([1, [2, [3, 4]], 5]))  # [1, 2, 3, 4, 5]
```

### API reference

#### Strings (`pytility.strings`)

* `to_str(string, encoding="utf-8") -> str | None` — coerces `str` or
  `bytes` to `str`, stripping non-printable characters; anything else
  returns `None`.
* `normalize_space(item, preserve_newline=False) -> str` — collapses runs of
  whitespace to single spaces; with `preserve_newline=True`, newlines are
  kept but whitespace is normalized within each line.
* `truncate(string, length, ellipsis="[…]", respect_word=False) -> str` —
  truncates `string` to `length` characters plus an ellipsis; with
  `respect_word=True`, extends the cut to the end of the word it lands in
  rather than splitting it.

#### Iterables (`pytility.iterables`)

* `arg_to_iter(arg) -> Iterable` — wraps a non-iterable (or a `str`/`bytes`/
  `dict`, which are treated as scalars) in a 1-tuple; `None` becomes `()`;
  anything else iterable is returned as-is.
* `clear_list(items) -> list` — unique items in order of first occurrence,
  with falsy values (`None`, `""`, `0`, …) dropped.
* `flatten(*args) -> Iterable` — recursively flattens nested iterables;
  `str`/`bytes`/`dict` are treated as scalars, not flattened further.
* `take_first(items) -> Any | None` — the first item that isn't `None` or
  `""`, or `None` if there isn't one.
* `batchify(iterable, size) -> Iterable[Iterable]` — splits `iterable` into
  chunks of at most `size` items.
* `window(iterable, size=2) -> Iterable[tuple]` — a sliding window of
  `size` consecutive items over `iterable`.

#### Parsers (`pytility.parsers`)

* `parse_int(string, base=10) -> int | None` — safely converts to `int`,
  or `None` on failure.
* `parse_float(number) -> float | None` — safely converts to `float`, or
  `None` on failure.
* `parse_bool(item) -> bool` — recognizes `int`s, the strings `"True"`,
  `"true"`, `"Yes"`, `"yes"`, and anything else `parse_int` can parse as
  non-zero; everything else is `False`.
* `parse_date(date, tzinfo=None, format_str=None) -> datetime | None` —
  parses `datetime`/`date` objects, epoch timestamps, a given
  `format_str`, free-form strings (via `python-dateutil`, if installed),
  or `(year, month, day, ...)` tuples / `time.struct_time`. Falsy input
  (including `0`) returns `None` before any parsing is attempted.

#### Files (`pytility.files`)

* `concat_files(dst, srcs, ensure_newline=False) -> int` — concatenates
  `srcs` into `dst` (paths or open file objects), returning the total
  bytes written. With `ensure_newline=True`, a newline is appended after
  every source that doesn't already end in one — including the last.

## Development

* Clone this repository
* Requirements:
  * [uv](https://docs.astral.sh/uv/)
  * Python 3.10+
* Create a virtual environment and install the dependencies

```sh
uv sync
```

### Testing

```sh
uv run pytest
```

### Pre-commit

Pre-commit hooks run the auto-formatter and linter (`ruff`), the type
checker (`ty`), and other housekeeping checks so the changeset is in good
shape before a commit happens.

Install the hooks with (runs on every commit):

```sh
uv run pre-commit install
```

Or run all checks manually against every file:

```sh
uv run pre-commit run --all-files
```

CI (`.gitlab-ci.yml`) runs the same lint hooks, the test suite across
Python 3.10–3.14, and a package build on every push.

### Releasing

This project is published to PyPI manually — there's no CI publish job or
GitHub-style draft release here, just these steps run locally:

```sh
# 1. Bump version (major|minor|patch|stable|alpha|beta|rc|post|dev, or pass an explicit value)
uv version --bump patch
VERSION=$(uv version --short)

# 2. Commit the version bump
git add pyproject.toml uv.lock
git commit -m "Release $VERSION"

# 3. Tag and push to GitLab (the only remote this repo has)
git tag "v$VERSION"
git push gitlab master
git push gitlab "v$VERSION"

# 4. Build and publish to PyPI (needs a token, e.g. via a UV_PUBLISH_TOKEN env var)
uv build
uv publish
```

Note that `uv version` only edits `pyproject.toml`; `uv.lock` is re-synced
alongside it since it also records the project's own version.
