Usage examples

Basic usage

Lets create data.json as following:

{
  "name": "Yaroslav",
  "age": 22,
  "salary": 100,
  "office": "Central Office",
  "position": "Manager",
  "greeting_template": "Hello, {}"
}

Now lets load it:

from pankoff.base import Container
from pankoff.combinator import combine
from pankoff.exceptions import ValidationError
from pankoff.magic import autoinit, Alias
from pankoff.validators import String, Number, BaseValidator, Predicate, LazyLoad


class Salary(BaseValidator):

    def __setup__(self, amount, currency):
        self.amount = amount
        self.currency = currency

    def mutate(self, instance, value):
        return f"{instance.name} salary is: {value}"

    def validate(self, instance, value):
        amount, currency = value.split()
        if int(amount) != self.amount or currency != self.currency:
            raise ValidationError(f"Wrong data in field: `{self.field_name}`")


@autoinit(merge=True)
class Person(Container):
    name = String()
    age = Number(min_value=18)
    salary = combine(
        Predicate, Salary,
        # Predicate
        predicate=lambda instance, value: value == "100 USD",
        default=lambda instance, value: str(value) + " USD",
        # Salary
        amount=100, currency="USD"
    )
    office = Predicate(
        predicate=lambda instance, value: value in ["Central Office"],
        error_message="Invalid value for field {field_name}, got {value}"
    )
    position = Predicate(
        # NOTE: we can use `salary` field at this point
        predicate=lambda instance, value: value == "Manager" and "100 USD" in instance.salary,
        error_message="Invalid value for {field_name}, person got into wrong position: {value}"
    )
    payment = Alias("salary")
    job_desc = LazyLoad(factory=lambda instance: f"{instance.position} at {instance.office}")

    def  __init__(self, greeting_template):
        self.greeting = greeting_template.format(self.name)

person = Person.from_path("data.json")
print(person)  # Person(name=Yaroslav, age=22, salary=Yaroslav salary is: 100 USD, office=Central Office, position=Manager, job_desc=Manager at Central Office)
print(person.greeting)  # Hello, Yaroslav

Trying invalid data

Change your data.json

{
  "name": "Yaroslav",
  "age": 22,
  "salary": 100,
  "office": "Central Office",
  "position": "HR",
  "greeting_template": "Hello, {}"
}

Now load it:

>>> person = Person.from_path("data.json")
Traceback (most recent call last):
...
pankoff.exceptions.ValidationError: ['Invalid value for position, person got into wrong position: HR']

Lets do some transformations

Here’s our data.json:

{
    "name": "Yaroslav",
    "salary": "100 USD",
    "kind": 1
}
kinds = {
    1: "Good person",
    2: "Bad person"
}

class KindMutator(BaseValidator):

    def validate(self, instance, value):
        if value not in kinds:
            raise ValidationError(f"Person kind should be in {kinds.keys()}")

    def mutate(self, instance, value):
        return kinds[value]


@autoinit
class Person(Container):
    name = String()
    salary = Salary(amount=100, currency="USD")
    kind = KindMutator()

Person.from_path("data.json").to_path("mutated_data.json", indent=4)

And here’s what we get in mutated_data.json:

{
    "name": "Yaroslav",
    "salary": "Yaroslav salary is: 100 USD",
    "kind": "Good person"
}

Making object factories

It is possible to make object factory based on the same Container class.

class Multiplication(BaseValidator):

    def validate(self, instance, value):
        if not isinstance(value, (int, float)):
            raise ValidationError(f"`{self.field_name}` should be a number")

    def mutate(self, instance, value):
        return value * instance.get_extra("multiplicator", default=1)

@autoinit
class Person(Container):
    name = String()
    age = Multiplication()


young_person = Person.extra(multiplicator=2)
old_person = Person.extra(multiplicator=5)

john = young_person(name="John", age=10)
yaroslav = old_person(name="yaroslav", age=10)

print(john)  # Person(name=John, age=20)
print(yaroslav)  # Person(name=yaroslav, age=50)

As you can see, young_person and old_person acting like completely different things, but in fact they’re not.

Also, you can access underlying extra structure by doing yaroslav._extra, which returns MappingProxyType view.

Magic mixins

Lets say you want to create a mixin, normally you’d do:

class HelloMixin:

    def say(self):
        value = super().say()
        return f"My name is {value} !!!"

class Hello:

    def say(self):
        return self.name

class Person(HelloMixin, Hello):
    def __init__(self, name):
        self.name = name

person = Person("Yaroslav")
print(person.say())  # My name is Yaroslav !!!

As you can see, we’re using super() here. Magic mixins allows you to avoid that, e.g:

from pankoff.magic import MagicMixin

class HelloMixin(MagicMixin):

    def say(self, value):
        return f"My name is {value} !!!"

class Hello:

    def say(self):
        return self.name

class Person(HelloMixin, Hello):
    def __init__(self, name):
        self.name = name

person = Person("Yaroslav")
print(person.say())  # My name is Yaroslav !!!

So the idea is, when you have same method names in both mixin and its parent, mixin will consume parents’ value implicitly.

In the example above, value parameter for HelloMixin is the result say method on Hello class.

Note that it’ll chain through all the mixins in MRO.