<commit_before>from copy import copy
from sqlalchemy import Column
from sqlalchemy.orm import ColumnProperty, RelationshipProperty
from sqlalchemy.orm import Session, exc
from sqlalchemy.util import KeyedTuple
from sqlalchemy.orm.session import make_transient, make_transient_to_detached

from . import MongoModel, MongoQuery


def expunge_instance(instance):
    """ Expunge an instance from its session.

    Sometimes we keep the previous version of an instance.
    In order to avoid SqlAlchemy conflicts because of two instances existing at the same time,
    we need to remove the previous version from the session.

    :param instance: Instance
    :type instance: sqlalchemy.ext.declarative.DeclarativeMeta|None
    :rtype: Session
    """
    if not instance:
        return
    try:
        session = Session.object_session(instance)
        if session:
            session.expunge(instance)
        return session
    except exc.UnmappedInstanceError:
        pass


class CrudHelper(object):
    """ Crud helper functions """

    def __init__(self, model):
        """ Init CRUD helper

        :param model: The model to work with
        :type model: type
        """
        self.model = model
        self.mongomodel = MongoModel.get_for(self.model)

    def mquery(self, query, query_obj=None):
        """ Construct a MongoQuery for the model.

        If `query` is provided, it's used for initial filtering

        :param query: Query to start with
        :type query: sqlalchemy.orm.Query
        :param query_obj: Apply initial filtering with the Query Object
        :type query_obj: dict|None
        :rtype: mongosql.MongoQuery
        :raises AssertionError: unknown operations specified in query_obj
        """
        assert query_obj is None or isinstance(query_obj, dict), 'Query Object should be a dict or None'

        mq = MongoQuery(self.mongomodel, query)
        if query_obj:
            mq = mq.query(**query_obj)
        return mq

    def check_columns(self, names):
        """ Test if all column names are known

        :param names: Column names
        :type names: Iterable
        :return: Set of unknown names
        :rtype: set
        """
        model_colnames = self.mongomodel.model_bag.columns.names
        names = set(names)
        return names - model_colnames

    def nullify_empty_fields(self, entity):
        """ Walk through the entity dict and handle nullable fields:

        - If a field has a value of '', set it to None

        :param entity: Entity
        :type entity:
        :return: Altered entity
        :rtype: dict
        """
        for k in self.mongomodel.model_bag.nullable.keys():
            if k in entity and entity[k] == '':
                entity[k] = None
        return entity

    def create_model(self, entity):
        """ Create an instance from entity dict.

        This only allows to assign column properties and not relations.

        :param entity: Entity dict
        :type entity: dict
        :return: Created instance
        :rtype: sqlalchemy.ext.declarative.DeclarativeMeta
        :raises AssertionError: validation errors
        """
        assert isinstance(entity, dict), 'Create model: entity should be a dict'

        # Check columns
        unk_cols = self.check_columns(entity.keys())
        assert not unk_cols, 'Create model: unknown fields: {}'.format(unk_cols)

        # Create
        return self.model(**entity)

    def instance_for_update(self, prev_instance):
        """ Based on `prev_instance`, create a new instance that will track updates.

            To achieve that, we first create an instance based on `prev_instance`'s primary key, and then mark it as "Detached":
            then SqlAlchemy thinks it was persisted previously, and therefore tracks property updates on it.

            Also, `prev_instance` is expunge()d from its session, and a new instance takes its place.

            :param prev_instance: Origin.
            :type prev_instance: sqlalchemy.ext.declarative.DeclarativeMeta
            :rtype: sqlalchemy.ext.declarative.DeclarativeMeta
        """
        # Create from PK
        pk_values = {c: getattr(prev_instance, c) for c in self.mongomodel.model_bag.pk.names}
        new_instance = self.model(**pk_values)

        # Make 'detached'
        make_transient_to_detached(new_instance)

        # Expunge previous, replace with new
        ssn = expunge_instance(prev_instance).add(new_instance)
        return new_instance

    def update_model(self, entity, prev_instance):
        """ Update an instance from entity dict by merging the fields

        - Properties are copied over
        - JSON dicts are shallowly merged

        :param entity: Entity dict
        :type entity: dict
        :param prev_instance: Previous version
        :type prev_instance: sqlalchemy.ext.declarative.DeclarativeMeta
        :return: New instance, updated
        :rtype: sqlalchemy.ext.declarative.DeclarativeMeta
        :raises AssertionError: validation errors
        """
        assert isinstance(entity, dict), 'Update model: entity should be a dict'
<commit_msg>crud: Refactored `CrudHelper.instance_for_update`<commit_after>from copy import copy
from sqlalchemy import Column
from sqlalchemy.orm import ColumnProperty, RelationshipProperty
from sqlalchemy.orm import Session, exc
from sqlalchemy.util import KeyedTuple
from sqlalchemy.orm.session import make_transient, make_transient_to_detached

from . import MongoModel, MongoQuery


class CrudHelper(object):
    """ Crud helper functions """

    def __init__(self, model):
        """ Init CRUD helper

        :param model: The model to work with
        :type model: type
        """
        self.model = model
        self.mongomodel = MongoModel.get_for(self.model)

    def mquery(self, query, query_obj=None):
        """ Construct a MongoQuery for the model.

        If `query` is provided, it's used for initial filtering

        :param query: Query to start with
        :type query: sqlalchemy.orm.Query
        :param query_obj: Apply initial filtering with the Query Object
        :type query_obj: dict|None
        :rtype: mongosql.MongoQuery
        :raises AssertionError: unknown operations specified in query_obj
        """
        assert query_obj is None or isinstance(query_obj, dict), 'Query Object should be a dict or None'

        mq = MongoQuery(self.mongomodel, query)
        if query_obj:
            mq = mq.query(**query_obj)
        return mq

    def check_columns(self, names):
        """ Test if all column names are known

        :param names: Column names
        :type names: Iterable
        :return: Set of unknown names
        :rtype: set
        """
        model_colnames = self.mongomodel.model_bag.columns.names
        names = set(names)
        return names - model_colnames

    def nullify_empty_fields(self, entity):
        """ Walk through the entity dict and handle nullable fields:

        - If a field has a value of '', set it to None

        :param entity: Entity
        :type entity:
        :return: Altered entity
        :rtype: dict
        """
        for k in self.mongomodel.model_bag.nullable.keys():
            if k in entity and entity[k] == '':
                entity[k] = None
        return entity

    def create_model(self, entity):
        """ Create an instance from entity dict.

        This only allows to assign column properties and not relations.

        :param entity: Entity dict
        :type entity: dict
        :return: Created instance
        :rtype: sqlalchemy.ext.declarative.DeclarativeMeta
        :raises AssertionError: validation errors
        """
        assert isinstance(entity, dict), 'Create model: entity should be a dict'

        # Check columns
        unk_cols = self.check_columns(entity.keys())
        assert not unk_cols, 'Create model: unknown fields: {}'.format(unk_cols)

        # Create
        return self.model(**entity)

    def instance_for_update(self, prev_instance):
        """ Based on `prev_instance`, create a new instance that will track updates.

            To achieve that, we first create an instance based on `prev_instance`'s primary key, and then mark it as "Detached":
            then SqlAlchemy thinks it was persisted previously, and therefore tracks property updates on it.

            Also, `prev_instance` is expunge()d from its session, replaced with the new instance:
            this is done to keep the previous instance intact while updates are made to the new instance.
            In order to avoid SqlAlchemy conflicts because of two instances existing at the same time,
            we need to remove the previous version from the session.

            :param prev_instance: Origin.
            :type prev_instance: sqlalchemy.ext.declarative.DeclarativeMeta
            :rtype: sqlalchemy.ext.declarative.DeclarativeMeta
            :raises sqlalchemy.orm.exc.UnmappedInstanceError: `prev_instance` was not bound to any Session
        """
        # Create from PK
        pk_values = {c: getattr(prev_instance, c) for c in self.mongomodel.model_bag.pk.names}
        new_instance = self.model(**pk_values)

        # Make the new instance 'detached'
        make_transient_to_detached(new_instance)

        # Expunge the previous instance and replace it with new
        ssn = Session.object_session(prev_instance)
        ssn.expunge(prev_instance)
        ssn.add(new_instance)

        # Finish
        return new_instance

    def update_model(self, entity, prev_instance):
        """ Update an instance from entity dict by merging the fields

        - Properties are copied over
        - JSON dicts are shallowly merged

        :param entity: Entity dict
        :type entity: dict
        :param prev_instance: Previous version
        :type prev_instance: sqlalchemy.ext.declarative.DeclarativeMeta
        :return: New instance, updated
        :rtype: sqlalchemy.ext.declarative.DeclarativeMeta
        :raises AssertionError: validation errors
        """
        assert isinstance(entity, dict), 'Update model: entity should be a dict'

{'fraction_non_alphanumeric': 0.07104148191940092, 'fraction_numerical': 0.0, 'mean_word_length': 2.5415212840195394, 'pattern_counts': {'":': 2, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5239313', 'n_tokens_mistral': 2773, 'n_tokens_neox': 2641, 'n_words': 962}