Package: py-automapper
py-automapperBuild StatusTable of Contents:VersionsAboutContributeUsageInstallationGet startedMap dictionary source to target objectDifferent field namesOverwrite field value in mappingDisable DeepcopyExtensionsPydantic/FastAPI SupportTortoiseORM SupportSQLAlchemy SupportCreate your own extension (Advanced)VersionsCheckCHANGELOG.mdAboutPython auto mapperis useful for multilayer architecture which requires constant mapping between objects from separate layers (data layer, presentation layer, etc).Inspired by:object-mapperThe 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 indocumentation.ContributeReadCONTRIBUTING.mdguide.UsageInstallationInstall package:pipinstallpy-automapperGet startedLet's say we have domain modelUserInfoand its API representationPublicUserInfowithout exposing userage:classUserInfo:def__init__(self,name:str,profession:str,age:int):self.name=nameself.profession=professionself.age=ageclassPublicUserInfo:def__init__(self,name:str,profession:str):self.name=nameself.profession=professionuser_info=UserInfo("John Malkovich","engineer",35)To createPublicUserInfoobject:fromautomapperimportmapperpublic_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:# Registermapper.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 objectIf 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 namesIf your target class field name is different from source class.classPublicUserInfo:def__init__(self,full_name:str,profession:str):self.full_name=full_name# UserInfo has `name` insteadself.profession=professionSimple 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 mappingVery 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 DeepcopyBy default, py-automapper performs a recursivecopy.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, setuse_deepcopy=Falseinmapfunction.fromdataclassesimportdataclassfromautomapperimportmapper@dataclassclassAddress:street:strnumber:intzip_code:intcity:strclassPersonInfo:def__init__(self,name:str,age:int,address:Address):self.name=nameself.age=ageself.address=addressclassPublicPersonInfo:def__init__(self,name:str,address:Address):self.name=nameself.address=addressaddress=Address(street="Main Street",number=1,zip_code=100001,city='Test City')info=PersonInfo('John Doe',age=35,address=address)# default deepcopy behaviorpublic_info=mapper.to(PublicPersonInfo).map(info)print("Target public_info.address is same as source address: ",addressispublic_info.address)# Target public_info.address is same as source address: False# disable deepcopypublic_info=mapper.to(PublicPersonInfo).map(info,use_deepcopy=False)print("Target public_info.address is same as source address: ",addressispublic_info.address)# Target public_info.address is same as source address: TrueExtensionspy-automapperhas few predefined extensions for mapping support to classes for frameworks:FastAPIandPydanticTortoiseORMSQLAlchemyPydantic/FastAPI SupportOut of the box Pydantic models support:frompydanticimportBaseModelfromtypingimportListfromautomapperimportmapperclassUserInfo(BaseModel):id:intfull_name:strpublic_name:strhobbies:List[str]classPublicUserInfo(BaseModel):id:intpublic_name:strhobbies: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 mappingprint(vars(result))# {'id': 2, 'public_name': 'dannyd', 'hobbies': ['acting', 'comedy', 'swimming']}TortoiseORM SupportOut of the box TortoiseORM models support:fromtortoiseimportModel,fieldsfromautomapperimportmapperclassUserInfo(Model):id=fields.IntField(pk=True)full_name=fields.TextField()public_name=fields.TextField()hobbies=fields.JSONField()classPublicUserInfo(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:valueforkey,valueinvars(result)ifnotkey.startswith("_")})# {'id': 2, 'public_name': 'dannyd', 'hobbies': ['acting', 'comedy', 'swimming']}SQLAlchemy SupportOut of the box SQLAlchemy models support:fromsqlalchemy.ormimportdeclarative_basefromsqlalchemyimportColumn,Integer,StringfromautomapperimportmapperBase=declarative_base()classUserInfo(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,)classPublicUserInfo(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:valueforkey,valueinvars(result)ifnotkey.startswith("_")})# {'id': 2, 'public_name': 'dannyd', 'hobbies': "acting, comedy, swimming"}Create your own extension (Advanced)When you first time importmapperfromautomapperit checks default extensions and if modules are found for these extensions, then they will be automatically loaded for defaultmapperobject.What does extension do?To know what fields in Target class are available for mapping,py-automapperneeds to know how to extract the list of fields. There is no generic way to do that for all Python objects. For this purposepy-automapperuses extensions.List of default extensions can be found in/automapper/extensionsfolder. 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 inmapper:fromautomapperimportmapperclassTargetClass:def__init__(self,**kwargs):self.name=kwargs["name"]self.age=kwargs["age"]@staticmethoddefget_fields(cls):return["name","age"]source_obj={"name":"Andrii","age":30}try:# Map objecttarget_obj=mapper.to(TargetClass).map(source_obj)exceptExceptionase: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 onesmapper.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 acceptskwargsparameter in__init__method and you want to copy only specific fields. Next example is a bit complex but probably rarely will be needed:fromtypingimportType,TypeVarfromautomapperimportMapper# Create your own Mapper object without any predefined extensionsmapper=Mapper()classTargetClass:def__init__(self,**kwargs):self.data=kwargs.copy()@classmethoddeffields(cls):return["name","age","profession"]source_obj={"name":"Andrii","age":30,"profession":None}try:target_obj=mapper.to(TargetClass).map(source_obj)exceptExceptionase: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` propertyT=TypeVar("T")defclass_has_fields_property(target_cls:Type[T])->bool:returncallable(getattr(target_cls,"fields",None))mapper.add_spec(class_has_fields_property,lambdat: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` valuetarget_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