def va_diff(cls, session, va_version=None, va_id=None): """ Compares version identified by 'va_version' or 'va_id' with previous version. Provide one of va_version, va_id :param session: flushed session :param va_id: version id of log row to be compared :return: dict with versions, user_id's and dict with columns as keys and changes as values :return: dict """ if va_version is not None and va_id is not None: log.warning( "both va_version and va_id provided, only va_version will be used, please exclude one of them " "from call") if va_version is None and va_id is None: raise LogIdentifyError( "Please provide at least one from va_version, va_id to identify column" ) if va_version is not None: filter_condition = (cls.ArchiveTable.va_version == va_version, ) else: filter_condition = (cls.ArchiveTable.va_id == va_id, ) this_row = utils.result_to_dict( session.execute( sa.select({cls.ArchiveTable}).where(*filter_condition))) if not len(this_row): if va_version is not None: identify_str = 'va_version={}'.format(va_version) else: identify_str = 'va_id={}'.format(va_id) raise HistoryItemNotFound( "Can't find log record by {}".format(identify_str)) this_row = this_row[0] va_id = this_row['va_id'] all_history_items = { col_name: this_row[col_name] for col_name in cls.ArchiveTable._version_col_names } prev_log = [ log for log in cls.va_list_by_pk(session, **all_history_items) if log['va_id'] < va_id ] if not prev_log: return utils.compare_rows(None, this_row) prev_va_id = prev_log[-1]['va_id'] prev_row = utils.result_to_dict( session.execute( sa.select({cls.ArchiveTable }).where(cls.ArchiveTable.va_id == prev_va_id)))[0] return utils.compare_rows(prev_row, this_row)
def _get_historical_time_slice(va_table, session, t, conds, include_deleted, limit, offset): at = va_table.ArchiveTable vc = va_table.va_version_columns pk_conditions = _get_conditions_list(va_table, conds) and_clause = _get_conditions( pk_conditions, [at.va_updated_at <= t] + [] if include_deleted else [va_table.ArchiveTable.va_deleted.is_(False)], ) t2 = at.__table__.alias('t2') return utils.result_to_dict(session.execute( sa.select([at]) .select_from(at.__table__.join( t2, sa.and_( t2.c.va_updated_at <= t, at.va_version < t2.c.va_version, *[getattr(at, c) == getattr(t2.c, c) for c in vc] ), isouter=True, )) .where(t2.c.va_version.is_(None) & and_clause) .order_by(*_get_order_clause(at)) .limit(limit) .offset(offset) ))
def va_get_all_by_pk(cls, session, **kwargs): all_history_items = utils.result_to_dict( session.execute( sa.select([ cls.ArchiveTable.va_id, cls.ArchiveTable.va_version, cls.ArchiveTable.user_id, cls.ArchiveTable.va_data.label('record') ]).where(cls.create_log_select_expression(kwargs)))) return all_history_items
def va_list_by_pk(cls, session, **kwargs): """ Returns all VA version id's of this record with there corresponding user_id. This can be called after a row has been inserted into the table and the session has been flushed. """ return utils.result_to_dict(session.execute( sa.select([cls.ArchiveTable.va_id, cls.ArchiveTable.user_id, cls.ArchiveTable.va_version]) .where(cls.create_log_select_expression(kwargs)) ))
def va_diff_all_by_pk(cls, session, **kwargs): all_history_items = utils.result_to_dict(session.execute( sa.select([cls.ArchiveTable]) .where(cls.create_log_select_expression(kwargs)) )) all_changes = [] for i in range(len(all_history_items)): if i is 0: all_changes.append(utils.compare_rows(None, all_history_items[i])) else: all_changes.append(utils.compare_rows(all_history_items[i-1], all_history_items[i])) return all_changes
def _get_latest_time_slice(va_table, session, conds, include_deleted, limit, offset): and_clause = _get_conditions( _get_conditions_list(va_table, conds, archive=False), [] if include_deleted else [va_table.ArchiveTable.va_deleted.is_(False)], ) result = session.execute( sa.select([va_table.ArchiveTable]).select_from( va_table.ArchiveTable.__table__.join( va_table, va_table.ArchiveTable.va_id == va_table.va_id)).where(and_clause).order_by(*_get_order_clause( va_table.ArchiveTable)).limit(limit).offset(offset)) return utils.result_to_dict(result)
def _get_historical_changes(va_table, session, conds, t1, t2, include_deleted, limit, offset): pk_conditions = _get_conditions_list(va_table, conds) and_clause = _get_conditions( pk_conditions, [va_table.ArchiveTable.va_updated_at >= t1, va_table.ArchiveTable.va_updated_at < t2] + [] if include_deleted else [va_table.ArchiveTable.va_deleted.is_(False)], ) return utils.result_to_dict(session.execute( sa.select([va_table.ArchiveTable]) .where(and_clause) .order_by(*_get_order_clause(va_table.ArchiveTable)) .limit(limit) .offset(offset) ))
def va_get(cls, session, va_version=None, va_id=None): """ Returns historic object (log record). Provide one of va_version, va_id to identify version This can be called after a row has been inserted into the table and the session has been flushed. :param session: flushed session :param va_version - va_version of log row (va_version field in va_version) :param va_id: va_id of requested record (va_id field). Can be used as alternative to va_version :return: a dictionary of key value pairs representing version id, id of the record in model, and versioned model's data :rtype: dict """ if va_version is not None and va_id is not None: log.warning( "both va_version and va_id provided, only va_version will be used, please exclude one of them " "from call") if va_version is None and va_id is None: raise LogIdentifyError( "Please provide at least one from va_version, va_id to identify column" ) if va_version is not None: filter_condition = (cls.ArchiveTable.va_version == va_version, ) else: filter_condition = (cls.ArchiveTable.va_id == va_id, ) result = utils.result_to_dict( session.execute( sa.select({cls.ArchiveTable.va_id, cls.ArchiveTable.va_data }).where(*filter_condition))) if not len(result): if va_version is not None: identify_str = 'va_version={}'.format(va_version) else: identify_str = 'va_id={}'.format(va_id) raise HistoryItemNotFound( "Can't find log record by {}".format(identify_str)) result = result[0] historic_object = result['va_data'] historic_object['va_id'] = result['va_id'] return historic_object
def get( va_table, session, va_id=None, t1=None, t2=None, fields=None, conds=None, include_deleted=True, page=1, page_size=100, ): ''' :param va_table: the model class which inherits from \ :class:`~versionalchemy.models.user_table.VAModelMixin` and specifies the model of \ the user table from which we are querying :param session: a sqlalchemy session with connections to the database :param va_id: if specified, the value of t1 and t2 will be ignored. If specified, this will \ return all records after the specified va_id. :param t1: lower bound time for this query; if None or unspecified, \ defaults to the unix epoch. If this is specified and t2 is not, this query \ will simply return the time slice of data at t1. This must either be a valid \ sql time string or a datetime.datetime object. :param t2: upper bound time for this query; if both t1 and t2 are none or unspecified, \ this will return the latest data (i.e. time slice of data now). This must either be a \ valid sql time string or a datetime.datetime object. :param fields: a list of strings which corresponds to columns in the table; If \ None or unspecified, returns all fields in the table. :param conds: a list of dictionary of key value pairs where keys are columns in the table \ and values are values the column should take on. If specified, this query will \ only return rows where the columns meet all the conditions. The columns specified \ in this dictionary must be exactly the unique columns that versioning pivots around. :param include_deleted: if ``True``, the response will include deleted changes. Else it will \ only include changes where ``deleted = 0`` i.e. the data was in the user table. :param page: the offset of the result set (1-indexed); i.e. if page_size is 100 and page is 2, \ the result set will contain results 100 - 199 :param page_size: upper bound on number of results to display. Note the actual returned result \ set may be smaller than this due to the roll up. ''' limit, offset = _get_limit_and_offset(page, page_size) version_col_names = va_table.va_version_columns if fields is None: fields = [ name for name in utils.get_column_names(va_table) if name != 'va_id' ] if va_id is not None: return _format_response( utils.result_to_dict( session.execute( sa.select([ va_table.ArchiveTable ]).where(va_table.ArchiveTable.va_id > va_id).order_by( *_get_order_clause(va_table.ArchiveTable)).limit( page_size).offset(offset))), fields, version_col_names) if t1 is None and t2 is None: rows = _get_latest_time_slice(va_table, session, conds, include_deleted, limit, offset) return _format_response(rows, fields, version_col_names) if t2 is None: # return a historical time slice rows = _get_historical_time_slice(va_table, session, t1, conds, include_deleted, limit, offset) return _format_response(rows, fields, version_col_names) if t1 is None: t1 = 0 rows = _get_historical_changes(va_table, session, conds, t1, t2, include_deleted, limit, offset) return _format_response(rows, fields, version_col_names)
def _result_to_dict(self, res): return utils.result_to_dict(res)