Metadata-Version: 2.4
Name: attrbox
Version: 0.1.6
Summary: Attribute-based data structures.
Author-email: Metaist LLC <metaist@metaist.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/metaist/attrbox
Project-URL: Documentation, https://metaist.github.io/attrbox/
Project-URL: Repository, https://github.com/metaist/attrbox.git
Project-URL: Changelog, https://github.com/metaist/attrbox/blob/main/CHANGELOG.md
Keywords: attr,attributes,dict,list
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
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: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE.md
License-File: src/attrbox/_vendor/docopt/LICENSE-MIT
Requires-Dist: tomli==2.0.1; python_version < "3.11"
Provides-Extra: dev
Requires-Dist: coverage; extra == "dev"
Requires-Dist: ds-run; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: pdoc3; extra == "dev"
Requires-Dist: pyright; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: pytest; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: cogapp; extra == "dev"
Dynamic: license-file

# attrbox: attribute-based data structures

<p align="center">
  <a href="https://metaist.github.io/attrbox/"><img alt="Otto the Otter" width="200" src="https://raw.githubusercontent.com/metaist/attrbox/main/otter-box.png" /></a><br />
  <em>Otto the Otter</em>
</p>
<p align="center">
  <a href="https://github.com/metaist/attrbox/actions/workflows/ci.yaml"><img alt="Build" src="https://img.shields.io/github/actions/workflow/status/metaist/attrbox/.github/workflows/ci.yaml?branch=main&logo=github"/></a>
  <a href="https://pypi.org/project/attrbox"><img alt="PyPI" src="https://img.shields.io/pypi/v/attrbox.svg?color=blue" /></a>
  <a href="https://pypi.org/project/attrbox"><img alt="Supported Python Versions" src="https://img.shields.io/pypi/pyversions/attrbox" /></a>
</p>

## Why?

I have common use cases where I want to improve python's `dict` and `list`:

- [`AttrDict`](#attrdict): attribute-based `dict` with better merge and deep value access
- [`AttrList`](#attrlist): `list` that broadcasts operations to its members
- [Environment](#environment): reading environment files
- [Configuration](#configuration): loading command-line arguments and configuration files
- [`JSend`](#jsend): sending JSON responses

## Install

```bash
python -m pip install attrbox
```

## AttrDict

`AttrDict` features:

- **Attribute Syntax** for `dict` similar to [accessing properties in JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_objects#accessing_properties): `thing.prop` means `thing["prop"]` for get / set / delete.

- **No `KeyError`**: if a key is missing, just return `None` (like `dict.get()`).

- **Deep Indexing**: use a list of keys and `int` to get and set deeply nested values. This is similar to [lodash.get](https://lodash.com/docs/#get) except that only the array-like syntax is supported and you must use actual `int` to index across `list` objects.

- **Deep Merge**: combine two `dict` objects by extending deeply-nested keys where possible. This is different than the new `dict` union operator ([PEP 584](https://peps.python.org/pep-0584/)).

```python
from attrbox import AttrDict

items = AttrDict(a=1, b=[{"c": {"d": 5}}], e={"f": {"g": 7}})
items.a
# => 1
items.x is None
# => True
items.x = 10
items['x']
# => 10
items.get(["b", 0, "c", "d"])
# => 5
items <<= {"e": {"f": {"g": 20, "h": [30, 40]}}}
items.e.f.g
# => 20
items[['e', 'f', 'h', 1]]
# => 40
```

[Read more about `AttrDict`](https://metaist.github.io/attrbox/attrdict.html#attrbox.attrdict.AttrDict)

## AttrList

`AttrList` provides **member broadcast**: performing operations on the list performs the operation on all the items in the list. I typically use this to achieve the [Composite design pattern](https://en.wikipedia.org/wiki/Composite_pattern).

```python
from attrbox import AttrDict, AttrList

numbers = AttrList([complex(1, 2), complex(3, 4), complex(5, 6)])
numbers.real
# => [1.0, 3.0, 5.0]

words = AttrList(["Apple", "Bat", "Cat"])
words.lower()
# => ['apple', 'bat', 'cat']

items = AttrList([AttrDict(a=1, b=2), AttrDict(a=5)])
items.a
# => [1, 5]
items.b
# => [2, None]
```

[Read more about `AttrList`](https://metaist.github.io/attrbox/attrlist.html#attrbox.attrlist.AttrList)

## Environment

`attrbox.env` is similar to [python-dotenv](https://github.com/theskumar/python-dotenv), but uses the `AttrDict` ability to do deep indexing to allow for things like dotted variable names. Typically, you'll use it by calling `attrbox.load_env()` which will find the nearest <code>.env</code> file and load it into `os.environ`.

[Read more about `attrbox.env`](https://metaist.github.io/attrbox/env.html)

## Configuration

`attrbox` supports loading configuration files from `.json`, `.toml`, and `.env` files. By default, `load_config()` looks for a key `imports` and will recursively import those files (relative to the current file) before loading the rest of the current file (data is merged using `AttrDict`). This allows you to create templates or smaller configurations that build up to a larger configuration.

For CLI applications, `attrbox.parse_docopt()` let's you use the power of [`docopt`](https://github.com/docopt/docopt) with the flexibility of `AttrDict`. By default, `--config` and `<config>` arguments will load the file using the `load_config()`

```python
"""Usage: prog.py [--help] [--version] [-c CONFIG] --file FILE

Options:
  --help                show this message and exit
  --version             show the version number and exit
  -c, --config CONFIG   load this configuration file (supported: .toml, .json, .env)
  --file FILE           the file to process
"""

def main():
    args = parse_docopt(__doc__, version=__version__)
    args.file # has the value of --file

if __name__ == "__main__":
    main()
```

Building on top of `docopt` we strip off leading dashes and convert them to underscores so that we can access the arguments as `AttrDict` attributes.

[Read more about `attrbox.config`](https://metaist.github.io/attrbox/config.html)

## JSend

`JSend` is an approximate implementation of the [`JSend` specification](https://labs.omniti.com/labs/jsend) that makes it easy to create standard JSON responses. The main difference is that I added an `ok` attribute to make it easy to tell if there was a problem (`fail` or `error`).

```python
from attrbox import JSend

def some_function(arg1):
    result = JSend() # default is "success"

    if not is_good(arg1):
        # fail = controlled problem
        return result.fail(message="You gone messed up.")

    try:
        result.success(data=process(arg1))
    except Exception:
        # error = uncontrolled problem
        return result.error(message="We have a problem.")

    return result
```

Because the `JSend` object is an `AttrDict`, it acts like a `dict` in every other respect (e.g., it is JSON-serializable).

[Read more about `JSend`](https://metaist.github.io/attrbox/jsend.html#attrbox.jsend.JSend)

## License

[MIT License](https://github.com/metaist/attrbox/blob/main/LICENSE.md)

The vendorized version of [`docopt-ng`](https://github.com/jazzband/docopt-ng) is also [licensed under the MIT License](https://github.com/metaist/attrbox/blob/main/src/attrbox/_vendor/docopt/LICENSE-MIT).
