Metadata-Version: 2.1
Name: dataclass-as-data
Version: 0.1.0
Summary: Simple configurable conversion of dataclasses to raw data
Home-page: https://gitlab.com/sigmath_bits/dataclass-as-data
Author: Sigmath Bits
License: GPLv3
Classifier: Development Status :: 4 - Beta
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Provides-Extra: test
Provides-Extra: dev
License-File: LICENSE

# Dataclass As Data
This is a simple package for configurable conversion of dataclasses to a data representation, typically a dict or a tuple.
The behaviour for how a dataclass is converted to and from data can be configured to differ from the default per dataclass if desired.

This package only supports simple primitive types, other dataclasses, and the primitive generics `dict[...]`, `list[...]`, `tuple[...]`, `Union[...]`, and `Optional[...]` as type annotations by default. 
Single-Input functions can be used in the place of type hints as simple converters. 



## Install

This package supports **Python 3.9** and above.

```bash
pip install dataclass-as-data
```

## Quick Start

```python
import dataclasses
from dataclass_as_data import as_data, from_data


# Create a dataclass
@dataclasses.dataclass
class Person:
    name: str
    age: int

    
# Create a dataclass object
person = Person("Simon", 21)

>>> person
Person(name='Simon', age=21)

# Call as_data with the dataclass object to convert it to a dictionary
data = as_data(person)

>>> data
{'name': 'Simon', 'age': 21}

# Call from_data with the dataclass and the data to get the object instance back
>>> from_data(Person, data)
Person(name='Simon', age=21)
```

Dataclasses can be nested within dataclasses, which are recursively converted to their data representation.

```python
@dataclasses.dataclass
class Friends:
    people: list[Person]


# All dataclasses are converted recursively
>>> as_data(Friends([Person("Sunset", 22), Person("Starlight", 20)]))
{'people': [{'name': 'Sunset', 'age': 22}, {'name': 'Starlight', 'age': 20}]}

>>> from_data(Friends, {'people': [{'name': 'Sunset', 'age': 22}, {'name': 'Starlight', 'age': 20}]})
Friends(people=[Person(name='Sunset', age=22), Person(name='Starlight', age=20)])
```

## Configuring as_data and from_data

To change what data is constructed when using `as_data` and `from_data`, override the `as_data` method and `from_data` class methods in your dataclass.

**Note:** you must use one of `as_dict`, `as_tuple`, `from_dict`, or `from_tuple` (**not** `as_data` or `from_data`) if you wish to use the default behaviour and modify it.

```python
from dataclass_as_data import as_data, as_dict, from_data, from_dict


@dataclasses.dataclass
class Config:
    VERSION = (1, 0)
    version: tuple[int, int] = VERSION
    
    def as_data(self) -> dict:
        # Ensure correct version when converting to data
        assert self.version == self.VERSION, "Incorrect version!"
        
        return as_dict(self)  # use as_dict to otherwise use default behaviour
    
    @classmethod
    def from_data(cls, data: dict):
       # Update version on data load
        if data['version'] < cls.VERSION:
            data['version'] = cls.VERSION

        return from_dict(cls, data)  # use from_dict to otherwise use default behaviour


# Now these methods are called instead
>>> as_data(Config((0, 1)))
AssertionError: Incorrect version!

>> from_data(Config, {'version': (0, 1)})
Config(version=(1, 0))
```


### DataAsTuple

If you'd simply like a dataclass to be represented as a tuple instead of a dict when calling `as_data`, 
inherent from the `DataAsTuple` abstract base class.

```python
from dataclass_as_data import as_data, DataAsTuple


# Create a dataclass inheriting from DataAsTuple
@dataclasses.dataclass
class Person(DataAsTuple):
    name: str
    age: int

    
# Calling as_data now returns a tuple
>>> as_data(Person("Summer", 24))
("Summer", 24)
```

This merely overrides `as_data` and `from_data` to use `as_tuple` and `from_tuple` for you respectively.
```python
from dataclass_as_data import as_tuple, from_tuple


# Same as inheriting from DataAsTuple
@dataclasses.dataclass
class Person:
    name: str
    age: int
    
    def as_data(self):
        return as_tuple(self)

    @classmethod
    def from_data(cls, data: tuple):
        return from_tuple(cls, data)
```

## Custom converters

`from_data` supports very basic custom converters in the form on single-input functions.
These converters are called on the relevant data entries when `from_data` is called.
Note that regular types, such as `int`, are also technically treated this way.

**Note:** `as_data` performs no type conversion at all.

```python
from dataclass_as_data import from_data


def lower_str(value) -> str:
    """Convert to lowercase str"""
    return str(value).lower()

    
@dataclasses.dataclass
class Employee:
    id: int
    name: lower_str


# The `lower_str` converter is called on the value of the `name` parameter
>>> from_data(Employee, {'id': 123, 'name': "Sylvester"})
Employee(id=123, name='silvia')

# The string value of `id` is coerced into an int
>>> from_data(Employee, {'id': "456", 'name': "Sunny"})
Employee(id=456, name='sunny')
```
