import sqlalchemy from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import scoped_session, sessionmaker Engine = sqlalchemy.create_engine('sqlite:///:memory:', echo=True) Session = scoped_session(sessionmaker(bind=Engine)) Base = declarative_base() #sqlalchemy vanilla model from sqlalchemy import Column, Integer, String, Text, Date, TIMESTAMP from sqlalchemy.orm import relationship, synonym from sqlalchemy import Column, ForeignKey, Integer, String, Text, Date, DateTime, TIMESTAMP, Boolean #These models represent a Google account object class GoogleAccount_websites(Base): #SQLAlchemy Table Definition __tablename__ = 'googleAccountsWebsites' #website_id = Column('website_id', Integer, primary_key=True) googleId = Column('googleId', String, ForeignKey('googleAccounts.id'), primary_key=True) type = Column('type', String, primary_key=True) value = Column('value', String) class GoogleAccount_emails(Base): #SQLAlchemy Table Definition __tablename__ = 'googleAccountsEmails' #email_id = Column('email_id', Integer, primary_key=True) googleId = Column('googleId', String, ForeignKey('googleAccounts.id'), primary_key=True) address = Column('address', String, primary_key=True) primary = Column('primary', Boolean) class GoogleAccount_externalIds(Base): #SQLAlchemy Table Mapping __tablename__ = 'googleAccountsExternalIds' #external_id = Column('external_id', Integer, primary_key=True) googleId = Column('googleId', String, ForeignKey('googleAccounts.id'), primary_key=True) customType = Column('customType', String, primary_key=True) type = Column('type', String) value = Column('value', String) class GoogleAccount_name(Base): #SQLAlchemy Table Mapping __tablename__ = 'googleAccountsNames' #name_id = Column('name_id', Integer, primary_key=True) googleId = Column('googleId', String, ForeignKey('googleAccounts.id'), primary_key=True) familyName = Column('familyName', String) fullName = Column('fullName', String) givenName = Column('givenName', String) class GoogleAccount_phones(Base): #SQLAlchemy Table Mapping __tablename__ = 'googleAccountsPhones' #phone_id = Column('phone_id', Integer, primary_key=True) googleId = Column('googleId', String, ForeignKey('googleAccounts.id'), primary_key=True) type = Column('type', String, primary_key=True) customType = Column('customType', String) value = Column('value', String) class GoogleAccount_aliases(Base): #SQLAlchemy Table Definition __tablename__ = 'googleAccountsAliases' #alias_id = Column('alias_id', Integer, primary_key=True) googleId = Column('googleId', String, ForeignKey('googleAccounts.id'), primary_key=True) emailAddress = Column('emailAddress', String, primary_key=True) class GoogleAccount_nonEditableAliases(Base): #SQLAlchemy Table Definition __tablename__ = 'googleAccountsNoneditableAliases' #noneditable_alias_id = Column('noneditableAliasId', Integer, primary_key=True) googleId = Column('googleId', String, ForeignKey('googleAccounts.id'), primary_key=True) emailAddress = Column('emailAddress', String, primary_key=True) class GoogleAccountSAModel(Base): #SQLAlchemy Table Definition __tablename__ = 'googleAccounts' id = Column('id', String, primary_key=True, nullable=False) agreedToTerms = Column('agreedToTerms', Boolean) changePasswordAtNextLogin = Column('changePasswordAtNextLogin', Boolean) creationTime = Column('creationTime', String) customerId = Column('customerId', String) etag = Column('etag', String) includeInGlobalAddressList = Column('includeInGlobalAddressList', Boolean) ipWhitelisted = Column('ipWhitelisted', Boolean) isAdmin = Column('isAdmin', Boolean) isDelegatedAdmin = Column('isDelegatedAdmin', Boolean) isMailboxSetup = Column('isMailboxSetup', Boolean) kind = Column('kind', String) lastLoginTime = Column('lastLoginTime', String) orgUnitPath = Column('orgUnitPath', String) primaryEmail = Column('primaryEmail', String) suspended = Column('suspended', Boolean) thumbnailPhotoEtag = Column('thumbnailPhotoEtag', String) thumbnailPhotoUrl = Column('thumbnailPhotoUrl', String) websites = relationship('GoogleAccount_websites', collection_class=list, cascade='all, delete-orphan', passive_deletes=True, backref='GoogleAccountRecord_SA') emails = relationship('GoogleAccount_emails', collection_class=list, cascade='all, delete-orphan', passive_deletes=True, backref='GoogleAccountRecord_SA') externalIds = relationship('GoogleAccount_externalIds', collection_class=list, cascade='all, delete-orphan', passive_deletes=True, backref='GoogleAccountRecord_SA') #Name is an Object, not a List type, it must have uselist=False name = relationship('GoogleAccount_name', uselist=False, cascade='all, delete-orphan', passive_deletes=True, backref='GoogleAccountRecord_SA') phones = relationship('GoogleAccount_phones', collection_class=list, cascade='all, delete-orphan', passive_deletes=True, backref='GoogleAccountRecord_SA') aliases = relationship('GoogleAccount_aliases', collection_class=list, cascade='all, delete-orphan', passive_deletes=True, backref='GoogleAccountRecord_SA') nonEditableAliases = relationship('GoogleAccount_nonEditableAliases', collection_class=list, cascade='all, delete-orphan', passive_deletes=True, backref='GoogleAccountRecord_SA') Base.metadata.create_all(Engine) #Marshmallow-SQLAlchemy Schema - Builds Schema based on ORM Model from marshmallow_sqlalchemy import ModelSchema from marshmallow import fields class GoogleAccountSASchema(ModelSchema): aliases = fields.Method(deserialize='processAliases') nonEditableAliases = fields.Method(deserialize='processNonEditableAliases') class Meta: model = GoogleAccountSAModel def deserializeListType(self, value, saType): saListOut = [] for item in value: newAlias = saType() newAlias.emailAddress = item saListOut.append(newAlias) return saListOut def processAliases(self, value): return self.deserializeListType(value, GoogleAccount_aliases) def processNonEditableAliases(self, value): return self.deserializeListType(value, GoogleAccount_nonEditableAliases) Base.metadata.create_all(Engine) #start of our test googleAccountJson = '''{ "agreedToTerms": true, "aliases": ["jd@testd.com", "j.e.doe@testd.com", "zz.dd@testd.com", "ff.qwerty@testd.com" ], "changePasswordAtNextLogin": false, "creationTime": "", "customerId": "C02fmcj1i", "emails": [{ "address": "john.doe@testd.com", "primary": true }], "etag": "XRsypGOPUmlmxokHB51cC07Vb3s/nnW4dSQR5PacZne6JsXkOrTfeBM", "externalIds": [{ "customType": "GID", "type": "custom", "value": "336168" }], "id": "108488927407595610966", "includeInGlobalAddressList": true, "ipWhitelisted": false, "isAdmin": false, "isDelegatedAdmin": true, "isMailboxSetup": true, "kind": "admin#directory#user", "lastLoginTime": "", "name": { "familyName": "Doe", "fullName": "John E Doe", "givenName": "John E" }, "nonEditableAliases": ["john.doe@webmail.testd.com", "doe@webmail.testd.com", "john.doe2@webmail.testd.com", "john_doe3@webmail.testd.com", "john.doe4@webmail.testd.com" ], "orgUnitPath": "/Ops", "phones": [{ "type": "work", "value": "+1 839-439-7484" }, { "type": "mobile", "value": "+1 407-393-5048" }, { "customType": "Ztel", "type": "custom", "value": "4-439-7484" }], "primaryEmail": "john.e.doe@testd.com", "suspended": false, "thumbnailPhotoEtag": "XRGOPUm7Vb3s/mVS-OxFB0Dyxo8", "thumbnailPhotoUrl": "https://plus.google.com/_/focus/photos/public/AI0YzcyZjRhNWZjOWExNDkwARP7Z8ODD5GJ", "websites": [{ "type": "profile", "value": "https://employees.testd.com/seto/336168" }] }''' from sqlalchemy.orm.attributes import get_history, instance_state def GetUncommittedChangesDict(saRow): #{'columnName' : {'newValue' : '2', 'oldValue' : '1'} #for key in z: changed = False changedDict = {} unchangedDict = {} outDict = {} for column in saRow._sa_instance_state.attrs: if (column.history): if (column.history.added) and (not column.history.deleted): changed = True changedDict[column.key] = {'newValue' : column.history.added, 'oldValue' : None} outDict[column.key] = {'newValue' : column.history.added, 'oldValue' : None} if (column.history.unchanged): unchangedDict[column.key] = {'newValue' : None, 'oldValue' : column.history.unchanged} outDict[column.key] = {'newValue' : None, 'oldValue' : column.history.unchanged} if (column.history.deleted): changed = True changedDict[column.key] = {'newValue' : column.history.added, 'oldValue' : None} outDict[column.key] = {'newValue' : column.history.added, 'oldValue' : column.history.deleted} return changed, changedDict, unchangedDict, outDict def hasChanged(saRow): changed, changedDict, unchangedDict, outDict = GetUncommittedChangesDict(saRow) print('Change detected: {}'.format(changedDict)) return changed #First pass x = GoogleAccountSASchema(strict=True, session=Session).loads(googleAccountJson) firstRow = x.data print('1.1- Session.dirty: {}\n\n'.format(Session.dirty)) print('1.1- hasChanged(firstRow): {}\n\n'.format(hasChanged(firstRow))) Session.add(firstRow) #Expected hasChanged == True print('1.2- Session.dirty: {}\n'.format(Session.dirty)) print('1.2- hasChanged(firstRow): {}\n\n'.format(hasChanged(firstRow))) Session.commit() #Expected hasChanged == False print('1.3- Session.dirty: {}\n'.format(Session.dirty)) print('1.3- hasChanged(firstRow): {}\n\n'.format(hasChanged(firstRow))) print() y = GoogleAccountSASchema(strict=True, session=Session).loads(googleAccountJson_CHANGED) secondRow = y.data print('2.1- Session.dirty: {}\n'.format(Session.dirty)) print('2.1- hasChanged(secondRow): {}\n\n'.format(hasChanged(secondRow))) print(get_history(secondRow, 'websites')) #Session.merge(secondRow) #print('2.2- Session.dirty: {}\n'.format(Session.dirty)) #print('2.2- hasChanged(firstRow): {}\n\n'.format(hasChanged(secondRow))) #z = GoogleAccountSASchema(strict=True, session=Session).loads(googleAccountJson) #thirdRow = z.data #print('3.1- Session.dirty: {}\n'.format(Session.dirty)) #print('3.1- hasChanged(firstRow): {}\n\n'.format(hasChanged(secondRow))) print() """ Test Output- 1.1- Session.dirty: IdentitySet([]) Change detected: {'phones': {'newValue': [<__main__.GoogleAccount_phones object at 0x10a44bf50>, <__main__.GoogleAccount_phones object at 0x10a463b50>, <__main__.GoogleAccount_phones object at 0x10a44b890>], 'oldValue': None}, u'isDelegatedAdmin': {'newValue': [True], 'oldValue': None}, u'thumbnailPhotoEtag': {'newValue': [u'XRGOPUm7Vb3s/mVS-OxFB0Dyxo8'], 'oldValue': None}, u'suspended': {'newValue': [False], 'oldValue': None}, u'id': {'newValue': [u'108488927407595610966'], 'oldValue': None}, 'aliases': {'newValue': [<__main__.GoogleAccount_aliases object at 0x10a44b990>, <__main__.GoogleAccount_aliases object at 0x10a44b810>, <__main__.GoogleAccount_aliases object at 0x10a44be50>, <__main__.GoogleAccount_aliases object at 0x10a44bed0>], 'oldValue': None}, 'nonEditableAliases': {'newValue': [<__main__.GoogleAccount_nonEditableAliases object at 0x10a44bd90>, <__main__.GoogleAccount_nonEditableAliases object at 0x10a44bcd0>, <__main__.GoogleAccount_nonEditableAliases object at 0x10a463bd0>, <__main__.GoogleAccount_nonEditableAliases object at 0x10a463dd0>, <__main__.GoogleAccount_nonEditableAliases object at 0x10a463c50>], 'oldValue': None}, u'thumbnailPhotoUrl': {'newValue': [u'https://plus.google.com/_/focus/photos/public/AI0YzcyZjRhNWZjOWExNDkwARP7Z8ODD5GJ'], 'oldValue': None}, u'includeInGlobalAddressList': {'newValue': [True], 'oldValue': None}, u'isAdmin': {'newValue': [False], 'oldValue': None}, u'etag': {'newValue': [u'XRsypGOPUmlmxokHB51cC07Vb3s/nnW4dSQR5PacZne6JsXkOrTfeBM'], 'oldValue': None}, u'lastLoginTime': {'newValue': [u''], 'oldValue': None}, u'orgUnitPath': {'newValue': [u'/Ops'], 'oldValue': None}, u'agreedToTerms': {'newValue': [True], 'oldValue': None}, 'externalIds': {'newValue': [<__main__.GoogleAccount_externalIds object at 0x10a463f90>], 'oldValue': None}, u'ipWhitelisted': {'newValue': [False], 'oldValue': None}, u'primaryEmail': {'newValue': [u'john.e.doe@testd.com'], 'oldValue': None}, u'isMailboxSetup': {'newValue': [True], 'oldValue': None}, 'emails': {'newValue': [<__main__.GoogleAccount_emails object at 0x10a471fd0>], 'oldValue': None}, u'kind': {'newValue': [u'admin#directory#user'], 'oldValue': None}, 'name': {'newValue': [<__main__.GoogleAccount_name object at 0x10a477bd0>], 'oldValue': None}, u'creationTime': {'newValue': [u''], 'oldValue': None}, 'websites': {'newValue': [<__main__.GoogleAccount_websites object at 0x10a47d810>], 'oldValue': None}, u'changePasswordAtNextLogin': {'newValue': [False], 'oldValue': None}, u'customerId': {'newValue': [u'C02fmcj1i'], 'oldValue': None}} 1.1- hasChanged(firstRow): True 1.2- Session.dirty: IdentitySet([]) Change detected: {'phones': {'newValue': [<__main__.GoogleAccount_phones object at 0x10a44bf50>, <__main__.GoogleAccount_phones object at 0x10a463b50>, <__main__.GoogleAccount_phones object at 0x10a44b890>], 'oldValue': None}, u'isDelegatedAdmin': {'newValue': [True], 'oldValue': None}, u'thumbnailPhotoEtag': {'newValue': [u'XRGOPUm7Vb3s/mVS-OxFB0Dyxo8'], 'oldValue': None}, u'suspended': {'newValue': [False], 'oldValue': None}, u'id': {'newValue': [u'108488927407595610966'], 'oldValue': None}, 'aliases': {'newValue': [<__main__.GoogleAccount_aliases object at 0x10a44b990>, <__main__.GoogleAccount_aliases object at 0x10a44b810>, <__main__.GoogleAccount_aliases object at 0x10a44be50>, <__main__.GoogleAccount_aliases object at 0x10a44bed0>], 'oldValue': None}, 'nonEditableAliases': {'newValue': [<__main__.GoogleAccount_nonEditableAliases object at 0x10a44bd90>, <__main__.GoogleAccount_nonEditableAliases object at 0x10a44bcd0>, <__main__.GoogleAccount_nonEditableAliases object at 0x10a463bd0>, <__main__.GoogleAccount_nonEditableAliases object at 0x10a463dd0>, <__main__.GoogleAccount_nonEditableAliases object at 0x10a463c50>], 'oldValue': None}, u'thumbnailPhotoUrl': {'newValue': [u'https://plus.google.com/_/focus/photos/public/AI0YzcyZjRhNWZjOWExNDkwARP7Z8ODD5GJ'], 'oldValue': None}, u'includeInGlobalAddressList': {'newValue': [True], 'oldValue': None}, u'isAdmin': {'newValue': [False], 'oldValue': None}, u'etag': {'newValue': [u'XRsypGOPUmlmxokHB51cC07Vb3s/nnW4dSQR5PacZne6JsXkOrTfeBM'], 'oldValue': None}, u'lastLoginTime': {'newValue': [u''], 'oldValue': None}, u'orgUnitPath': {'newValue': [u'/Ops'], 'oldValue': None}, u'agreedToTerms': {'newValue': [True], 'oldValue': None}, 'externalIds': {'newValue': [<__main__.GoogleAccount_externalIds object at 0x10a463f90>], 'oldValue': None}, u'ipWhitelisted': {'newValue': [False], 'oldValue': None}, u'primaryEmail': {'newValue': [u'john.e.doe@testd.com'], 'oldValue': None}, u'isMailboxSetup': {'newValue': [True], 'oldValue': None}, 'emails': {'newValue': [<__main__.GoogleAccount_emails object at 0x10a471fd0>], 'oldValue': None}, u'kind': {'newValue': [u'admin#directory#user'], 'oldValue': None}, 'name': {'newValue': [<__main__.GoogleAccount_name object at 0x10a477bd0>], 'oldValue': None}, u'creationTime': {'newValue': [u''], 'oldValue': None}, 'websites': {'newValue': [<__main__.GoogleAccount_websites object at 0x10a47d810>], 'oldValue': None}, u'changePasswordAtNextLogin': {'newValue': [False], 'oldValue': None}, u'customerId': {'newValue': [u'C02fmcj1i'], 'oldValue': None}} 1.2- hasChanged(firstRow): True 1.3- Session.dirty: IdentitySet([]) Change detected: {} 1.3- hasChanged(firstRow): False 2.1- Session.dirty: IdentitySet([<__main__.GoogleAccountSAModel object at 0x10a47d890>, <__main__.GoogleAccount_websites object at 0x10a4d2c10>]) Change detected: {'websites': {'newValue': [<__main__.GoogleAccount_websites object at 0x10a4c3d10>], 'oldValue': None}} 2.1- hasChanged(firstRow): True History(added=[<__main__.GoogleAccount_websites object at 0x106f38c10>], unchanged=[], deleted=[<__main__.GoogleAccount_websites object at 0x106f38210>]) """