Metadata-Version: 2.4
Name: handyobj
Version: 0.3.0
Summary: Chainable list operations and recursive attribute access for dictionaries
Project-URL: Homepage, https://github.com/Senhaji-Rhazi-Hamza/handyobj
Project-URL: Repository, https://github.com/Senhaji-Rhazi-Hamza/handyobj
Project-URL: Issues, https://github.com/Senhaji-Rhazi-Hamza/handyobj/issues
Project-URL: Changelog, https://github.com/Senhaji-Rhazi-Hamza/handyobj/blob/master/CHANGELOG.md
Author-email: SENHAJI RHAZI Hamza <hamza.senhajirhazi@gmail.com>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: collections,dictionary,functional,list,utilities
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
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
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# handyobj

`handyobj` provides two small, typed collection helpers:

- `SmartList`, a `list` subclass with chainable map, filter, grouping, sorting,
  reduction, and threaded mapping operations.
- `ObjectDict`, a `dict` subclass that recursively exposes string keys through
  attribute access.

The package supports Python 3.10 and newer and has no runtime dependencies.

## Installation

```shell
python -m pip install handyobj
```

## Quick start

```python
from handyobj import ObjectDict, SmartList

people = SmartList(
    [
        ObjectDict(name="Ada", role="Engineer"),
        ObjectDict(name="Grace", role="Admiral"),
        ObjectDict(name="Margaret", role="Engineer"),
    ]
)

engineers = (
    people.filter_by_attribute_values(role="Engineer")
    .map(lambda person: person.name)
    .sorted()
)

assert engineers == ["Ada", "Margaret"]
```

## `SmartList`

Every non-mutating collection operation returns another `SmartList`, so calls can
be chained:

```python
numbers = SmartList([1, 2, 3, 4])

result = numbers.filter_by_predicates(lambda number: number % 2 == 0).map(
    lambda number: number * 10
)

assert result == [20, 40]
assert numbers == [1, 2, 3, 4]
```

Useful methods include:

- `map(function)` and `filter_by_predicates(*predicates)`
- `filter_by_attribute_values(**values)`
- `filter_by_matched_attributes(**patterns)`
- `group_by_labeled_predicates(*(label, predicate))`
- `reduce(reducer, initial)`
- `first()`, `last()`, `first_or_none()`, `last_or_none()`, and `one_or_none()`
- `sorted(key=..., reverse=...)`, which returns a sorted copy
- `threaded_map(function, max_workers=...)` for synchronous, I/O-bound work

`SmartList` retains the normal `list.sort()` contract: it sorts in place and returns
`None`. Use `sorted()` when building a chain.

Groups are independent, so an item can belong to multiple labeled groups:

```python
groups = SmartList(range(1, 7)).group_by_labeled_predicates(
    ("even", lambda number: number % 2 == 0),
    ("large", lambda number: number >= 5),
)

assert groups == [("even", [2, 4, 6]), ("large", [5, 6])]
```

## `ObjectDict`

Mappings are recursively wrapped at assignment time. Mutating a nested object
therefore updates the original structure:

```python
config = ObjectDict(
    database={"host": "localhost", "ports": [5432, 5433]},
    services=[{"name": "api", "enabled": True}],
)

config.database.host = "db.example.com"
config.services[0].enabled = False

assert config["database"]["host"] == "db.example.com"
assert config.services[0]["enabled"] is False
```

Dictionary methods take precedence over colliding keys. For a key such as `items`,
use item access (`value["items"]`) rather than attribute access (`value.items`).

`clone()` returns a deep, independent copy. `drop_keys()` and `keep_only_keys()`
mutate the object and return it, allowing them to be chained.

## Concurrency

`threaded_map()` uses Python's `ThreadPoolExecutor`, preserves input order, and
propagates worker exceptions. It accepts ordinary synchronous functions and is
mainly useful for I/O-bound operations. It does not execute `async def` functions;
use an asyncio-native workflow for coroutine functions.

## Development

```shell
uv sync --all-groups
uv run pytest --cov
uv run ruff check .
uv run ruff format --check .
uv run mypy
uv build
```

See [CHANGELOG.md](CHANGELOG.md) for release notes.
