Skip to main content

Library for automatically mapping one object to another

Project description

py-automapper

Build Status Main branch status


Table of Contents:

Versions

Check CHANGELOG.md

About

Python auto mapper is useful for multilayer architecture which requires constant mapping between objects from separate layers (data layer, presentation layer, etc).

Inspired by: object-mapper

The major advantage of py-automapper is its extensibility, that allows it to map practically any type, discover custom class fields and customize mapping rules. Read more in documentation.

Contribute

Read CONTRIBUTING.md guide.

Usage

Installation

Install package:

pip install py-automapper

Get started

Let's say we have domain model UserInfo and its API representation PublicUserInfo without exposing user age:

class UserInfo:
    def __init__(self, name: str, profession: str, age: int):
        self.name = name
        self.profession = profession
        self.age = age

class PublicUserInfo:
    def __init__(self, name: str, profession: str):
        self.name = name
        self.profession = profession

user_info = UserInfo("John Malkovich", "engineer", 35)

To create PublicUserInfo object:

from automapper import mapper

public_user_info = mapper.to(PublicUserInfo).map(user_info)

print(vars(public_user_info))
# {'name': 'John Malkovich', 'profession': 'engineer'}

You can register which class should map to which first:

# Register
mapper.add(UserInfo, PublicUserInfo)

public_user_info = mapper.map(user_info)

print(vars(public_user_info))
# {'name': 'John Malkovich', 'profession': 'engineer'}

Map dictionary source to target object

If source object is dictionary:

source = {
    "name": "John Carter",
    "profession": "hero"
}
public_info = mapper.to(PublicUserInfo).map(source)

print(vars(public_info))
# {'name': 'John Carter', 'profession': 'hero'}

Different field names

If your target class field name is different from source class.

class PublicUserInfo:
    def __init__(self, full_name: str, profession: str):
        self.full_name = full_name       # UserInfo has `name` instead
        self.profession = profession

Simple map:

public_user_info = mapper.to(PublicUserInfo).map(user_info, fields_mapping={
    "full_name": user_info.name
})

Preregister and map. Source field should start with class name followed by period sign and field name:

mapper.add(UserInfo, PublicUserInfo, fields_mapping={"full_name": "UserInfo.name"})
public_user_info = mapper.map(user_info)

print(vars(public_user_info))
# {'full_name': 'John Malkovich', 'profession': 'engineer'}

Overwrite field value in mapping

Very easy if you want to field just have different value, you provide a new value:

public_user_info = mapper.to(PublicUserInfo).map(user_info, fields_mapping={
    "full_name": "John Cusack"
})

print(vars(public_user_info))
# {'full_name': 'John Cusack', 'profession': 'engineer'}

Disable Deepcopy

By default, py-automapper performs a recursive copy.deepcopy() call on all attributes when copying from source object into target class instance. This makes sure that changes in the attributes of the source do not affect the target and vice versa. If you need your target and source class share same instances of child objects, set use_deepcopy=False in map function.

from dataclasses import dataclass
from automapper import mapper

@dataclass
class Address:
    street: str
    number: int
    zip_code: int
    city: str
  
class PersonInfo:
    def __init__(self, name: str, age: int, address: Address):
        self.name = name
        self.age = age
        self.address = address

class PublicPersonInfo:
    def __init__(self, name: str, address: Address):
        self.name = name
        self.address = address

address = Address(street="Main Street", number=1, zip_code=100001, city='Test City')
info = PersonInfo('John Doe', age=35, address=address)

# default deepcopy behavior
public_info = mapper.to(PublicPersonInfo).map(info)
print("Target public_info.address is same as source address: ", address is public_info.address)
# Target public_info.address is same as source address: False

# disable deepcopy
public_info = mapper.to(PublicPersonInfo).map(info, use_deepcopy=False)
print("Target public_info.address is same as source address: ", address is public_info.address)
# Target public_info.address is same as source address: True

Extensions

py-automapper has few predefined extensions for mapping support to classes for frameworks:

Pydantic/FastAPI Support

Out of the box Pydantic models support:

from pydantic import BaseModel
from typing import List
from automapper import mapper

class UserInfo(BaseModel):
    id: int
    full_name: str
    public_name: str
    hobbies: List[str]

class PublicUserInfo(BaseModel):
    id: int
    public_name: str
    hobbies: List[str]

obj = UserInfo(
    id=2,
    full_name="Danny DeVito",
    public_name="dannyd",
    hobbies=["acting", "comedy", "swimming"]
)

result = mapper.to(PublicUserInfo).map(obj)
# same behaviour with preregistered mapping

print(vars(result))
# {'id': 2, 'public_name': 'dannyd', 'hobbies': ['acting', 'comedy', 'swimming']}

TortoiseORM Support

Out of the box TortoiseORM models support:

from tortoise import Model, fields
from automapper import mapper

class UserInfo(Model):
    id = fields.IntField(pk=True)
    full_name = fields.TextField()
    public_name = fields.TextField()
    hobbies = fields.JSONField()

class PublicUserInfo(Model):
    id = fields.IntField(pk=True)
    public_name = fields.TextField()
    hobbies = fields.JSONField()

obj = UserInfo(
    id=2,
    full_name="Danny DeVito",
    public_name="dannyd",
    hobbies=["acting", "comedy", "swimming"],
    using_db=True
)

result = mapper.to(PublicUserInfo).map(obj)
# same behaviour with preregistered mapping

# filtering out protected fields that start with underscore "_..."
print({key: value for key, value in vars(result) if not key.startswith("_")})
# {'id': 2, 'public_name': 'dannyd', 'hobbies': ['acting', 'comedy', 'swimming']}

SQLAlchemy Support

Out of the box SQLAlchemy models support:

from sqlalchemy.orm import declarative_base
from sqlalchemy import Column, Integer, String
from automapper import mapper

Base = declarative_base()

class UserInfo(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    full_name = Column(String)
    public_name = Column(String)
    hobbies = Column(String)
    def __repr__(self):
        return "<User(full_name='%s', public_name='%s', hobbies='%s')>" % (
            self.full_name,
            self.public_name,
            self.hobbies,
        )

class PublicUserInfo(Base):
    __tablename__ = 'public_users'
    id = Column(Integer, primary_key=True)
    public_name = Column(String)
    hobbies = Column(String)
    
obj = UserInfo(
            id=2,
            full_name="Danny DeVito",
            public_name="dannyd",
            hobbies="acting, comedy, swimming",
        )

result = mapper.to(PublicUserInfo).map(obj)
# same behaviour with preregistered mapping

# filtering out protected fields that start with underscore "_..."
print({key: value for key, value in vars(result) if not key.startswith("_")})
# {'id': 2, 'public_name': 'dannyd', 'hobbies': "acting, comedy, swimming"}

Create your own extension (Advanced)

When you first time import mapper from automapper it checks default extensions and if modules are found for these extensions, then they will be automatically loaded for default mapper object.

What does extension do? To know what fields in Target class are available for mapping, py-automapper needs to know how to extract the list of fields. There is no generic way to do that for all Python objects. For this purpose py-automapper uses extensions.

List of default extensions can be found in /automapper/extensions folder. You can take a look how it's done for a class with __init__ method or for Pydantic or TortoiseORM models.

You can create your own extension and register in mapper:

from automapper import mapper

class TargetClass:
    def __init__(self, **kwargs):
        self.name = kwargs["name"]
        self.age = kwargs["age"]
    
    @staticmethod
    def get_fields(cls):
        return ["name", "age"]

source_obj = {"name": "Andrii", "age": 30}

try:
    # Map object
    target_obj = mapper.to(TargetClass).map(source_obj)
except Exception as e:
    print(f"Exception: {repr(e)}")
    # Output:
    # Exception: KeyError('name')

    # mapper could not find list of fields from BaseClass
    # let's register extension for class BaseClass and all inherited ones
    mapper.add_spec(TargetClass, TargetClass.get_fields)
    target_obj = mapper.to(TargetClass).map(source_obj)

    print(f"Name: {target_obj.name}; Age: {target_obj.age}")

You can also create your own clean Mapper without any extensions and define extension for very specific classes, e.g. if class accepts kwargs parameter in __init__ method and you want to copy only specific fields. Next example is a bit complex but probably rarely will be needed:

from typing import Type, TypeVar

from automapper import Mapper

# Create your own Mapper object without any predefined extensions
mapper = Mapper()

class TargetClass:
    def __init__(self, **kwargs):
        self.data = kwargs.copy()

    @classmethod
    def fields(cls):
        return ["name", "age", "profession"]

source_obj = {"name": "Andrii", "age": 30, "profession": None}

try:
    target_obj = mapper.to(TargetClass).map(source_obj)
except Exception as e:
    print(f"Exception: {repr(e)}")
    # Output:
    # Exception: MappingError("No spec function is added for base class of <class 'type'>")

# Instead of using base class, we define spec for all classes that have `fields` property
T = TypeVar("T")

def class_has_fields_property(target_cls: Type[T]) -> bool:
    return callable(getattr(target_cls, "fields", None))
    
mapper.add_spec(class_has_fields_property, lambda t: getattr(t, "fields")())

target_obj = mapper.to(TargetClass).map(source_obj)
print(f"Name: {target_obj.data['name']}; Age: {target_obj.data['age']}; Profession: {target_obj.data['profession']}")
# Output:
# Name: Andrii; Age: 30; Profession: None

# Skip `None` value
target_obj = mapper.to(TargetClass).map(source_obj, skip_none_values=True)
print(f"Name: {target_obj.data['name']}; Age: {target_obj.data['age']}; Has profession: {hasattr(target_obj, 'profession')}")
# Output:
# Name: Andrii; Age: 30; Has profession: False

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

py-automapper-1.2.3.tar.gz (15.2 kB view details)

Uploaded Source

Built Distribution

py_automapper-1.2.3-py3-none-any.whl (17.6 kB view details)

Uploaded Python 3

File details

Details for the file py-automapper-1.2.3.tar.gz.

File metadata

  • Download URL: py-automapper-1.2.3.tar.gz
  • Upload date:
  • Size: 15.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.1.12 CPython/3.8.15 Darwin/20.2.0

File hashes

Hashes for py-automapper-1.2.3.tar.gz
Algorithm Hash digest
SHA256 ae1f6f270c3bb42cd52bfc124630841019b278258c001a8ed3032cffa03327b4
MD5 40275dfcf814756f00fd5b3cf4f22d27
BLAKE2b-256 7687ae6a27ddaa1e0f7f8a93bf1e2f4071008cd1cd64bc12ef53bf384a681c98

See more details on using hashes here.

File details

Details for the file py_automapper-1.2.3-py3-none-any.whl.

File metadata

  • Download URL: py_automapper-1.2.3-py3-none-any.whl
  • Upload date:
  • Size: 17.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.1.12 CPython/3.8.15 Darwin/20.2.0

File hashes

Hashes for py_automapper-1.2.3-py3-none-any.whl
Algorithm Hash digest
SHA256 bcc5236fa0caeffa10416e31dd379fb6ddd15f18e6c2d7a1d49fb3d341f07653
MD5 a87abb0f81bb07de3d020a172e9ed197
BLAKE2b-256 ad65c8c93949da7961a066433a9dd292f18e5c7e653c156a1ee4d2d2cc60cb2f

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page