Giter Club home page Giter Club logo

py-automapper's Introduction

py-automapper

Important

Renewing maintanance of this library!

After a long pause, I see the library is still in use and receives more stars. Thank you all who likes and uses it. So, I renew the py-automapper maintanance. Expect fixes and new version soon.

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

py-automapper's People

Contributors

anikolaienko avatar g-pichler avatar soldag avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

py-automapper's Issues

Custom mapping estrange behavior when creating object in SQL Alchemy

I'm trying to add a field mapping, but when it has more than one level it is not getting the value. The value I want to map is inside "TodoDomain.user.id". I want this to work with preregistered mapping (which I cannot make it work)

from dataclasses import dataclass
from typing import Optional
from uuid import uuid4

from automapper import mapper




@dataclass
class UserDomain:
    id: int
    name: str
    email: str


@dataclass
class UserModel:
    id: int
    name: str
    email: str


@dataclass
class TodoDomain:
    description: str
    user: UserDomain


@dataclass
class TodoModel:
    description: str
    user: UserDomain
    user_id: Optional[int] = None


user = UserDomain(id=1, name="carlo", email="mail_carlo")
todo = TodoDomain(description="todo_carlo", user=user)


mapper.add(UserDomain, UserModel)
# This doesn't work. user_id is None
# mapper.add(TodoDomain, TodoModel)

# This works properly. user_id is 1
todo = mapper.to(TodoModel).map(todo, fields_mapping={"user_id": todo.user.id})

print(todo.__dict__)

Dict mapping throws Exception

If source objects contains dict attribute like
enums = { "enmu0": "some value", "enum1": "some other value" }
automapper throws
ValueError: too many values to unpack (expected 2)
I guess the problem is this part of code:
if is_sequence(obj): if isinstance(obj, dict): result = { k: self._map_subobject( v, _visited_stack, skip_none_values=skip_none_values ) for k, v in obj }
I think it should be for k, v in obj.items()

Create template for new issues

As a developer, I would like to have new issues coming with certain template.
I would like to know which Python version person was using. A code sample. Etc.

Support for new version of PyDantic modules

With the latest version of PyDantic the automapper seems broken. Seems the below function from extenions/default.py isn't working. If init is removed, working with latest pydantic

image

Add mapping from multiple classes

I want to be able to map from more than one class:

mapper.add(FromClassA, FromClassB, ToClassC)

target_obj = mapper.multimap(obj1, obj2)
# or
target_obj = mapper.to(TargetType).multimap(obj1, obj2)

TypeError on _map_subobject

The error occurs in the _map_subobject function when attempting to instantiate a new object incorrectly. Specifically, the error arises in the else block when obj is not a dictionary and the skip_none_values flag is set to True. In this case, the code attempts to create a new object of the same type as obj by filtering out null or empty values from the sequence using a list comprehension. However, the object instantiation fails because the constructor for the object is unable to handle the additional argument passed through the list comprehension. This results in a TypeError indicating that the constructor expected only one positional argument but received two.

Subscriptable check

Reading through the code I spotted the following line 42 in automapper/mapper.py:

return hasattr(obj, "__get_item__")

Is this on purpose, or should it read

return hasattr(obj, "__getitem__")

(__getitem__ instead of __get_item__)?

Add custom field mapping into mapping registration

I want to register custom field mappings:

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

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

mapper.add(SourceClass, TargetClass, {"profession": "Software Engineer"})

source_obj = SourceClass("Andrii", 30)
target_obj = mapper.map(source_obj)

print(f"Name: {target_obj.name}; Age: {target_obj.age}; Profession: {target_obj.profession}")
# Output:
# Name: Andrii; age: 30; Profession: Software Engineer

Suggestion: Adding a flag that allows for operating automapper without deepcopy

Every time the automapper encounters an object, it does not pass it to the __init__ of the target class directly, but performs a copy.deepcopy() operation at mapper.py, L252. While this behavior is not documented (I did not find it mentioned in README.md), after some deliberation, I do consider it a sane default for an automapper.

But there can be occasions, where this behavior might not be desired. I am facing such a situation currently, where I want the autmapper to simply pass the object directly through to __init__. I did not find a way to achieve this.

My proposed solution is to create an optional parameter for the .map() call that can be used to disable deepcopy(). This will preserve the current behavior, which I believe to be a sane default, but allow direct pass-through. Would you be interested in this feature? You can find a proof-of-concept in my fork.

PS: If you are wondering why I need this feature: I am using automapper to handle database objects created with SQLAlchemy. If the same object is referenced more than once, then deepcopy() is performed more than once and SQLAlchemy complains about duplicates with the same primary key.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.