safemodel 1.0.0
A safe container, a wrapper around a structure whose schema is unknown or may dynamically change.
Requires Python: >=3.12

# SafeModel

[![PyPI version](https://badge.fury.io/py/safemodel.svg)](https://badge.fury.io/py/safemodel)
[![Downloads](https://static.pepy.tech/badge/safemodel)](https://pepy.tech/project/safemodel)

A safe container, a wrapper around a structure whose schema is unknown or may dynamically change.

The idea is to put an object (e.g. a structure that is retrieved from an external source)
in a container that will support the `__getattr__` and `__getitem__` methods,
but will not throw errors if non-existent data is accessed.

## How to install

### Using uv

```bash
uv add safemodel
```

### Using pip

```bash
pip install safemodel
```

## How to Use

### General

SafeModel is a potentially iterable object.
If we try to iterate it, we get:
    1. If iterable is wrapped: (SafeModel(x) for x in iterable)
    2. If non-iterable is wrapped: empty iterator

When unpacking a wraparound value via `__call__`, it is passed to each processor sequentially.
If any processor raises `ProcessingError`, the value will be replaced with Placeholder.
If there is no wraparound data, Placeholder will be returned.

### Processors

Functions that take 1 positional argument and return a value
(usually the one passed in unchanged, but it can be changed) or throw a ProcessingError exception.
The result of a processor is used by the next one in the chain.

### NO_DATA

A special value denoting the absence of data.
`None` is not suitable because it may be the expected value in some cases.
The object denoting the absence of data must be fundamentally unobtainable from the source structure.

### Examples

```python
from safemodel import SafeModel, default_placeholder_context


unsafe_struct = {
    "hello": "world",
    "list_of_optional_dict_with_value": [
        {"value": 1},
        None,
        {"value": 2},
        3,
        4,
    ],
    "age_str": "24",
    "strange_struct": {
        "foo": "bar",
        "members": [
            {
                "fio": {
                    "name": "Ivan",
                }
            },
        ],
    },
    "camelCaseField": "ok",
}

safe_struct = SafeModel(unsafe_struct)

assert safe_struct.hello() == "world"

expected_values = [1, "-", 2, "-", "-"]
actual_values_1 = [value_container.value("-") for value_container in safe_struct.list_of_optional_dict_with_value]

with default_placeholder_context(safe_struct, "-"):
    actual_values_2 = [value_container.value() for value_container in safe_struct.list_of_optional_dict_with_value]

assert actual_values_1 == actual_values_2 == expected_values

assert safe_struct.age_str | int == 24

assert safe_struct.camel_case_field() == "ok"

assert safe_struct.strange_struct.members[0].fio.name() == "Ivan"

assert safe_struct.some.field.that.does.not_.exist("Nothing!!!") == "Nothing!!!"
assert not safe_struct.some.field.that.does.not_.exist.should.be.false

assert safe_struct.age_str | [float, str] == safe_struct.age_str(extend_processors=[float, str]) == "24.0"
```

## How To Develop

### Prepare your environment (once)

Install [uv](https://docs.astral.sh/uv/)

Using scoop

```bash
scoop install uv
```

Without scoop

```ps1
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```

### Run Linter

#### Using [Make](https://www.gnu.org/software/make/)

```bash
make lintfix
```

#### If Make is not installed

```bash
uv run ruff format
uv run ruff check --fix
```

### Run Tests

#### With Make

```bash
make
```

or

```bash
make test
```

#### Without Make

```bash
uv pip install -e .
uv run pytest -v
```

### Build

```bash
uv build
```