<commit_before>"""
Generalized Recommender models.

This module contains matrix factorization recommender interfaces
used throughout the whole scikit-crab package.

The interfaces are realized as abstract base classes (ie., some optional
functionality is provided in the interface itself, so that the interfaces
can be subclassed).

"""

# Author: Marcel Caraciolo <marcel@muricoca.com>
#
# License: BSD Style.
import random

from base import SVDRecommender
from ..knn.item_strategies import ItemsNeighborhoodStrategy
import numpy as np
from math import sqrt
import logging

logger = logging.getLogger('crab')

class MatrixFactorBasedRecommender(SVDRecommender):
    """
    Matrix Factorization Based Recommender using
    Expectation Maximization algorithm.

    Parameters
    -----------
    data_model: The data model instance that will be data source
         for the recommender.

    items_selection_strategy: The item candidates strategy that you
     can choose for selecting the possible items to recommend.
     default = ItemsNeighborhoodStrategy

    n_features: int
            Number of latent factors. default = 10

    learning_rate: float
        Learning rate used. default =  0.01

    regularization: float
            Parameter used to prevent overfitting. default = 0.02

    init_mean: float
            Mean of the normal distribution used to initialize
            the factors. default = 0.1

    init_std: float
            Standard deviation of the normal distribution used to
            initialize the factors. default = 0.1

    n_interations: int
            Number of iterations over the training data. default = 30

    capper: bool (default=True)
        Cap the preferences with maximum and minimum preferences
        in the model.
    with_preference: bool (default=False)
        Return the recommendations with the estimated preferences if True.

    Attributes
    -----------
    `model`: The data model instance that will be data source
         for the recommender.

    `items_selection_strategy`: The item candidates strategy that you
         can choose for selecting the possible items to recommend.
         default = ItemsNeighborhoodStrategy

    `n_features`:  int
            Number of latent factors. default = 10

    `learning_rate`: float
            Learning rate used. default = 0.01

    `regularization`: float
            Parameter used to prevent overfitting. default = 0.02

    `random_noise`: float
            Parameter used to initialize the latent factors.

    `n_interations`: int
            Number of iterations over the training data

    `capper`: bool (default=True)
        Cap the preferences with maximum and minimum preferences
        in the model.

    `with_preference`: bool (default=False)
        Return the recommendations with the estimated preferences if True.

    `user_factors`: array of shape [n_users, n_features]
             Matrix containing the latent item factors

    `item_factors`: array of shape [n_items, n_features]
            Matrix containing the latent item factors

    Examples
    -----------
    >>> from scikits.crab.models.classes import MatrixPreferenceDataModel
    >>> from scikits.crab.recommenders.svd.classes import MatrixFactorBasedRecommender
    >>> from scikits.crab.recommenders.knn.item_strategies import ItemsNeighborhoodStrategy
    >>> movies = {'Marcel Caraciolo': {'Lady in the Water': 2.5, \
     'Snakes on a Plane': 3.5, \
     'Just My Luck': 3.0, 'Superman Returns': 3.5, 'You, Me and Dupree': 2.5, \
     'The Night Listener': 3.0}, \
     'Paola Pow': {'Lady in the Water': 3.0, 'Snakes on a Plane': 3.5, \
     'Just My Luck': 1.5, 'Superman Returns': 5.0, 'The Night Listener': 3.0, \
     'You, Me and Dupree': 3.5}, \
    'Leopoldo Pires': {'Lady in the Water': 2.5, 'Snakes on a Plane': 3.0, \
     'Superman Returns': 3.5, 'The Night Listener': 4.0}, \
    'Lorena Abreu': {'Snakes on a Plane': 3.5, 'Just My Luck': 3.0, \
     'The Night Listener': 4.5, 'Superman Returns': 4.0, \
     'You, Me and Dupree': 2.5}, \
    'Steve Gates': {'Lady in the Water': 3.0, 'Snakes on a Plane': 4.0, \
     'Just My Luck': 2.0, 'Superman Returns': 3.0, 'The Night Listener': 3.0, \
     'You, Me and Dupree': 2.0}, \
    'Sheldom': {'Lady in the Water': 3.0, 'Snakes on a Plane': 4.0, \
     'The Night Listener': 3.0, 'Superman Returns': 5.0, \
     'You, Me and Dupree': 3.5}, \
    'Penny Frewman': {'Snakes on a Plane':4.5,'You, Me and Dupree':1.0, \
    'Superman Returns':4.0}, \
    'Maria Gabriela': {}}
    >>> model = MatrixPreferenceDataModel(movies)
    >>> items_strategy = ItemsNeighborhoodStrategy()
    >>> recsys = MatrixFactorBasedRecommender( \
        model=model, \
        items_selection_strategy=items_strategy, \
        n_features=2)
    >>> #Return the recommendations for the given user.
    >>> recsys.recommend('Leopoldo Pires')
    array(['Just My Luck', 'You, Me and Dupree'],\
          dtype='|S18')

    Notes
    -----------
    This MatrixFactorizationRecommender does not yet provide
    suppot for rescorer functions.

    This MatrixFactorizationRecommender does not yet provide
    suppot for DictDataModels.

    References
    -----------

    """

    def __init__(self, model, items_selection_strategy=None,
            n_features=10, learning_rate=0.01, regularization=0.02, init_mean=0.1,
            init_stdev=0.1, n_interations=30, capper=True, with_preference=False):
        SVDRecommender.__init__(self, model, with_preference)
        self.capper = capper
        self.n_features = n_features
        self.learning_rate = learning_rate
        self.regularization = regularization
        self.init_mean = init_mean
        self.init_stdev = init_stdev
        self.n_interations = n_interations
        self._global_bias = self._get_average_preference()
        self.user_factors = None
        self.item_factors = None

        if items_selection_strategy is None:
            self.items_selection_strategy = ItemsNeighborhoodStrategy()
        else:
            self.items_selection_strategy = items_selection_strategy

        self.factorize()

    def _init_models(self):
        num_users = self.model.users_count()
        num_items = self.model.items_count()

        self.user_factors = np.empty(shape=(num_users, self.n_features),
                    dtype=float)

        self.item_factors = np.empty(shape=(num_items, self.n_features),
                    dtype=float)

        '''
        pref_interval = self.model.max_preference() - self.model.min_preference()
        default_value = math.sqrt(global_bias - pref_interval * 0.1) / self.n_features
        interval = pref_interval * 0.1 / self.n_features

        for i in range(len(self.n_features)):
            for user_idx in self.model.num_users():
                self.user_factors[user_idx, i] = default_value + (random.random() - 0.5) * interval * 0.2

        for i in range(len(self.n_features)):
            for item_idx in self.model.num_items():
                self.item_factors[item_idx, i] = default_value + (random.random() - 0.5) * interval * 0.2
        '''
        #Initialize the matrix with normal distributed (Gaussian) Noise
        self.user_factors = self.init_mean * np.random.randn(num_users, self.n_features) + self.init_stdev ** 2
        self.item_factors = self.init_mean * np.random.randn(num_items, self.n_features) + self.init_stdev ** 2

    def _get_average_preference(self):
        if hasattr(self.model, 'index'):
            mdat = np.ma.masked_array(self.model.index, np.isnan(self.model.index))
        else:
            raise TypeError('This model is not yet supported for this recommender.')
        return np.mean(mdat)

    def _predict(self, user_index, item_index, trailing=True):
        #Compute the scalar product between two rows of two matrices
        result = self._global_bias + np.sum(self.user_factors[user_index] *
                                            self.item_factors[item_index])
        if trailing:
            max_preference = self.model.max_preference()
            min_preference = self.model.min_preference()
            if result > max_preference:
                result = max_preference
            elif result < min_preference:
                result = min_preference

        return result

    def _train(self, rating_indices, update_user, update_item):
        '''
        Iterate once over rating data and adjust corresponding factors (stochastic gradient descent)
        '''
        err_total = 0.0
        for user_idx, item_idx in rating_indices:
            p = self._predict(user_idx, item_idx, False)
            err = self.model.index[user_idx, item_idx] - p
            err_total += err

            #Adjust the factors
            u_f = self.user_factors[user_idx]
            i_f = self.item_factors[item_idx]

            #Compute factor updates
            delta_u = err * u_f - self.regularization * u_f
            delta_i = err * u_f - self.regularization * i_f
            #if necessary apply updates
            if update_user:
                self.user_factors[user_idx] += self.learning_rate * delta_u
            if update_item:
                self.item_factors[item_idx] += self.learning_rate * delta_i

        return err_total

    def _rating_indices(self):
        if hasattr(self.model, 'index'):
            rating_indices = [(idx, jdx) for idx in range(self.model.users_count())
                                for jdx in range(self.model.items_count())
                        if not np.isnan(self.model.index[idx, jdx])]
        else:
            raise TypeError('This model is not yet supported for this recommender.')

        return rating_indices

    def learn_factors(self, update_user=True, update_item=True):
        rating_indices = self._rating_indices()
        random.shuffle(rating_indices)

        for index in range(self.n_interations):
            err = self._train(rating_indices, update_user, update_item)
            rmse = sqrt((err ** 2.0) / len(rating_indices))
            logger.debug("Finished the interation %i with RMSE %f" %  \
                    (index, rmse))

    def factorize(self):
        #init factor matrices
        self._init_models()
        #Learn the model parameters
        self.learn_factors()

    def recommend(self, user_id, how_many=None, **params):
        '''
        Return a list of recommended items, ordered from most strongly
        recommend to least.

        Parameters
        ----------
        user_id: int or string
                 User for which recommendations are to be computed.
        how_many: int
                 Desired number of recommendations (default=None ALL)

        '''
        self._set_params(**params)

        candidate_items = self.all_other_items(user_id)

        recommendable_items = self._top_matches(user_id, \
                 candidate_items, how_many)

        return recommendable_items

    def estimate_preference(self, user_id, item_id, **params):
        '''
        A preference is estimated by computing the dot-product
        of the user and item feature vectors.
        Parameters
        ----------
        user_id: int or string
                 User for which recommendations are to be computed.

        item_id:  int or string
            ID of item for which wants to find the estimated preference.

        Returns
        -------
        Return an estimated preference if the user has not expressed a
        preference for the item, or else the user's actual preference for the
        item. If a preference cannot be estimated, returns None.
        '''

        preference = self.model.preference_value(user_id, item_id)
        if not np.isnan(preference):
            return preference

        #How to catch the user_id and item_id from the matrix.

        user_features = self.user_factors[np.where(self.model.user_ids() == user_id)]
        item_features = self.item_factors[np.where(self.model.item_ids() == item_id)]

        estimated = self._global_bias + np.sum(user_features * item_features)

        if self.capper:
            max_p = self.model.maximum_preference_value()
            min_p = self.model.minimum_preference_value()
            estimated = max_p if estimated > max_p else min_p \
                     if estimated < min_p else estimated
        return estimated

    def all_other_items(self, user_id, **params):
        '''
        Parameters
        ----------
        user_id: int or string
                 User for which recommendations are to be computed.

        Returns
        ---------
        Return items in the `model` for which the user has not expressed
        the preference and could possibly be recommended to the user.

        '''
        return self.items_selection_strategy.candidate_items(user_id, \
                            self.model)

    def _top_matches(self, source_id, target_ids, how_many=None, **params):
        '''
        Parameters
        ----------
        target_ids: array of shape [n_target_ids]

        source_id: int or string
                item id to compare against.

        how_many: int
            Desired number of most top items to recommend (default=None ALL)

        Returns
        --------
        Return the top N matches
        It can be user_ids or item_ids.
        '''
        #Empty target_ids
        if target_ids.size == 0:
            return np.array([])

        estimate_preferences = np.vectorize(self.estimate_preference)

        preferences = estimate_preferences(source_id, target_ids)

        preferences = preferences[~np.isnan(preferences)]
        target_ids = target_ids[~np.isnan(preferences)]

        sorted_preferences = np.lexsort((preferences,))[::-1]

        sorted_preferences = sorted_preferences[0:how_many] \
             if how_many and sorted_preferences.size > how_many \
                else sorted_preferences

        if self.with_preference:
            top_n_recs = np.array([(target_ids[ind], \
                     preferences[ind]) for ind in sorted_preferences])
        else:
            top_n_recs = np.array([target_ids[ind]
                 for ind in sorted_preferences])

        return top_n_recs

    def recommended_because(self, user_id, item_id, how_many=None, **params):
        '''
        Returns the items that were most influential in recommending a
        given item to a given user. In most implementations, this
        method will return items that the user prefers and that
        are similar to the given item.

        Parameters
        -----------
        user_id : int or string
            ID of the user who was recommended the item

        item_id: int or string
            ID of item that was recommended

        how_many: int
            Maximum number of items to return (default=None ALL)

        Returns
        ----------
        The list of items ordered from most influential in
        recommended the given item to least
        '''
        pass
<commit_msg>Removed some elements from SVD.<commit_after>"""
Generalized Recommender models.

This module contains matrix factorization recommender interfaces
used throughout the whole scikit-crab package.

The interfaces are realized as abstract base classes (ie., some optional
functionality is provided in the interface itself, so that the interfaces
can be subclassed).

"""

# Author: Marcel Caraciolo <marcel@muricoca.com>
#
# License: BSD Style.
import random

from base import SVDRecommender
from ..knn.item_strategies import ItemsNeighborhoodStrategy
import numpy as np
from math import sqrt
import logging

logger = logging.getLogger('crab')

class MatrixFactorBasedRecommender(SVDRecommender):
    """
    Matrix Factorization Based Recommender using
    Expectation Maximization algorithm.

    Parameters
    -----------
    data_model: The data model instance that will be data source
         for the recommender.

    items_selection_strategy: The item candidates strategy that you
     can choose for selecting the possible items to recommend.
     default = ItemsNeighborhoodStrategy

    n_features: int
            Number of latent factors. default = 10

    learning_rate: float
        Learning rate used. default =  0.01

    regularization: float
            Parameter used to prevent overfitting. default = 0.02

    init_mean: float
            Mean of the normal distribution used to initialize
            the factors. default = 0.1

    init_std: float
            Standard deviation of the normal distribution used to
            initialize the factors. default = 0.1

    n_interations: int
            Number of iterations over the training data. default = 30

    capper: bool (default=True)
        Cap the preferences with maximum and minimum preferences
        in the model.
    with_preference: bool (default=False)
        Return the recommendations with the estimated preferences if True.

    Attributes
    -----------
    `model`: The data model instance that will be data source
         for the recommender.

    `items_selection_strategy`: The item candidates strategy that you
         can choose for selecting the possible items to recommend.
         default = ItemsNeighborhoodStrategy

    `n_features`:  int
            Number of latent factors. default = 10

    `learning_rate`: float
            Learning rate used. default = 0.01

    `regularization`: float
            Parameter used to prevent overfitting. default = 0.02

    `random_noise`: float
            Parameter used to initialize the latent factors.

    `n_interations`: int
            Number of iterations over the training data

    `capper`: bool (default=True)
        Cap the preferences with maximum and minimum preferences
        in the model.

    `with_preference`: bool (default=False)
        Return the recommendations with the estimated preferences if True.

    `user_factors`: array of shape [n_users, n_features]
             Matrix containing the latent item factors

    `item_factors`: array of shape [n_items, n_features]
            Matrix containing the latent item factors

    Examples
    -----------
    >>> from scikits.crab.models.classes import MatrixPreferenceDataModel
    >>> from scikits.crab.recommenders.svd.classes import MatrixFactorBasedRecommender
    >>> from scikits.crab.recommenders.knn.item_strategies import ItemsNeighborhoodStrategy
    >>> movies = {'Marcel Caraciolo': {'Lady in the Water': 2.5, \
     'Snakes on a Plane': 3.5, \
     'Just My Luck': 3.0, 'Superman Returns': 3.5, 'You, Me and Dupree': 2.5, \
     'The Night Listener': 3.0}, \
     'Paola Pow': {'Lady in the Water': 3.0, 'Snakes on a Plane': 3.5, \
     'Just My Luck': 1.5, 'Superman Returns': 5.0, 'The Night Listener': 3.0, \
     'You, Me and Dupree': 3.5}, \
    'Leopoldo Pires': {'Lady in the Water': 2.5, 'Snakes on a Plane': 3.0, \
     'Superman Returns': 3.5, 'The Night Listener': 4.0}, \
    'Lorena Abreu': {'Snakes on a Plane': 3.5, 'Just My Luck': 3.0, \
     'The Night Listener': 4.5, 'Superman Returns': 4.0, \
     'You, Me and Dupree': 2.5}, \
    'Steve Gates': {'Lady in the Water': 3.0, 'Snakes on a Plane': 4.0, \
     'Just My Luck': 2.0, 'Superman Returns': 3.0, 'The Night Listener': 3.0, \
     'You, Me and Dupree': 2.0}, \
    'Sheldom': {'Lady in the Water': 3.0, 'Snakes on a Plane': 4.0, \
     'The Night Listener': 3.0, 'Superman Returns': 5.0, \
     'You, Me and Dupree': 3.5}, \
    'Penny Frewman': {'Snakes on a Plane':4.5,'You, Me and Dupree':1.0, \
    'Superman Returns':4.0}, \
    'Maria Gabriela': {}}
    >>> model = MatrixPreferenceDataModel(movies)
    >>> items_strategy = ItemsNeighborhoodStrategy()
    >>> recsys = MatrixFactorBasedRecommender( \
        model=model, \
        items_selection_strategy=items_strategy, \
        n_features=2)
    >>> #Return the recommendations for the given user.
    >>> recsys.recommend('Leopoldo Pires')
    array(['Just My Luck', 'You, Me and Dupree'],\
          dtype='|S18')

    Notes
    -----------
    This MatrixFactorizationRecommender does not yet provide
    suppot for rescorer functions.

    This MatrixFactorizationRecommender does not yet provide
    suppot for DictDataModels.

    References
    -----------

    """

    def __init__(self, model, items_selection_strategy=None,
            n_features=10, learning_rate=0.01, regularization=0.02, init_mean=0.1,
            init_stdev=0.1, n_interations=30, capper=True, with_preference=False):
        SVDRecommender.__init__(self, model, with_preference)
        self.capper = capper
        self.n_features = n_features
        self.learning_rate = learning_rate
        self.regularization = regularization
        self.init_mean = init_mean
        self.init_stdev = init_stdev
        self.n_interations = n_interations
        self._global_bias = self._get_average_preference()
        self.user_factors = None
        self.item_factors = None

        if items_selection_strategy is None:
            self.items_selection_strategy = ItemsNeighborhoodStrategy()
        else:
            self.items_selection_strategy = items_selection_strategy

        self.factorize()

    def _init_models(self):
        num_users = self.model.users_count()
        num_items = self.model.items_count()

        self.user_factors = np.empty(shape=(num_users, self.n_features),
                    dtype=float)

        self.item_factors = np.empty(shape=(num_items, self.n_features),
                    dtype=float)

        '''
        pref_interval = self.model.max_preference() - self.model.min_preference()
        default_value = math.sqrt(global_bias - pref_interval * 0.1) / self.n_features
        interval = pref_interval * 0.1 / self.n_features

        for i in range(len(self.n_features)):
            for user_idx in self.model.num_users():
                self.user_factors[user_idx, i] = default_value + (random.random() - 0.5) * interval * 0.2

        for i in range(len(self.n_features)):
            for item_idx in self.model.num_items():
                self.item_factors[item_idx, i] = default_value + (random.random() - 0.5) * interval * 0.2
        '''
        #Initialize the matrix with normal distributed (Gaussian) Noise
        self.user_factors = self.init_mean * np.random.randn(num_users, self.n_features) + self.init_stdev ** 2
        self.item_factors = self.init_mean * np.random.randn(num_items, self.n_features) + self.init_stdev ** 2

    def _get_average_preference(self):
        if hasattr(self.model, 'index'):
            mdat = np.ma.masked_array(self.model.index, np.isnan(self.model.index))
        else:
            raise TypeError('This model is not yet supported for this recommender.')
        return np.mean(mdat)

    def _predict(self, user_index, item_index, trailing=True):
        #Compute the scalar product between two rows of two matrices
        result = self._global_bias + np.sum(self.user_factors[user_index] *
                                            self.item_factors[item_index])
        if trailing:
            max_preference = self.model.max_preference()
            min_preference = self.model.min_preference()
            if result > max_preference:
                result = max_preference
            elif result < min_preference:
                result = min_preference

        return result

    def _train(self, rating_indices, update_user, update_item):
        '''
        Iterate once over rating data and adjust corresponding factors (stochastic gradient descent)
        '''
        err_total = 0.0
        for user_idx, item_idx in rating_indices:
            p = self._predict(user_idx, item_idx, False)
            err = self.model.index[user_idx, item_idx] - p
            err_total += err

            #Adjust the factors
            u_f = self.user_factors[user_idx]
            i_f = self.item_factors[item_idx]

            #Compute factor updates
            delta_u = err * u_f - self.regularization * u_f
            delta_i = err * u_f - self.regularization * i_f
            #if necessary apply updates
            if update_user:
                self.user_factors[user_idx] += self.learning_rate * delta_u
            if update_item:
                self.item_factors[item_idx] += self.learning_rate * delta_i

        return err_total

    def _rating_indices(self):
        if hasattr(self.model, 'index'):
            rating_indices = [(idx, jdx) for idx in range(self.model.users_count())
                                for jdx in range(self.model.items_count())
                        if not np.isnan(self.model.index[idx, jdx])]
        else:
            raise TypeError('This model is not yet supported for this recommender.')

        return rating_indices

    def learn_factors(self, update_user=True, update_item=True):
        rating_indices = self._rating_indices()
        random.shuffle(rating_indices)

        for index in range(self.n_interations):
            err = self._train(rating_indices, update_user, update_item)
            rmse = sqrt((err ** 2.0) / len(rating_indices))
            logger.debug("Finished the interation %i with RMSE %f" %  \
                    (index, rmse))

    def factorize(self):
        #init factor matrices
        self._init_models()
        #Learn the model parameters
        self.learn_factors()

    def recommend(self, user_id, how_many=None, **params):
        '''
        Return a list of recommended items, ordered from most strongly
        recommend to least.

        Parameters
        ----------
        user_id: int or string
                 User for which recommendations are to be computed.
        how_many: int
                 Desired number of recommendations (default=None ALL)

        '''
        self._set_params(**params)

        candidate_items = self.all_other_items(user_id)

        recommendable_items = self._top_matches(user_id, \
                 candidate_items, how_many)

        return recommendable_items

    def estimate_preference(self, user_id, item_id, **params):
        '''
        A preference is estimated by computing the dot-product
        of the user and item feature vectors.
        Parameters
        ----------
        user_id: int or string
                 User for which recommendations are to be computed.

        item_id:  int or string
            ID of item for which wants to find the estimated preference.

        Returns
        -------
        Return an estimated preference if the user has not expressed a
        preference for the item, or else the user's actual preference for the
        item. If a preference cannot be estimated, returns None.
        '''

        preference = self.model.preference_value(user_id, item_id)
        if not np.isnan(preference):
            return preference

        #How to catch the user_id and item_id from the matrix.

        user_features = self.user_factors[np.where(self.model.user_ids() == user_id)]
        item_features = self.item_factors[np.where(self.model.item_ids() == item_id)]

        estimated = self._global_bias + np.sum(user_features * item_features)

        if self.capper:
            max_p = self.model.maximum_preference_value()
            min_p = self.model.minimum_preference_value()
            estimated = max_p if estimated > max_p else min_p \
                     if estimated < min_p else estimated
        return estimated

    def all_other_items(self, user_id, **params):
        '''
        Parameters
        ----------
        user_id: int or string
                 User for which recommendations are to be computed.

        Returns
        ---------
        Return items in the `model` for which the user has not expressed
        the preference and could possibly be recommended to the user.

        '''
        return self.items_selection_strategy.candidate_items(user_id, \
                            self.model)

    def _top_matches(self, source_id, target_ids, how_many=None, **params):
        '''
        Parameters
        ----------
        target_ids: array of shape [n_target_ids]

        source_id: int or string
                item id to compare against.

        how_many: int
            Desired number of most top items to recommend (default=None ALL)

        Returns
        --------
        Return the top N matches
        It can be user_ids or item_ids.
        '''
        #Empty target_ids
        if target_ids.size == 0:
            return np.array([])

        estimate_preferences = np.vectorize(self.estimate_preference)

        preferences = estimate_preferences(source_id, target_ids)

        preferences = preferences[~np.isnan(preferences)]
        target_ids = target_ids[~np.isnan(preferences)]

        sorted_preferences = np.lexsort((preferences,))[::-1]

        sorted_preferences = sorted_preferences[0:how_many] \
             if how_many and sorted_preferences.size > how_many \
                else sorted_preferences

        if self.with_preference:
            top_n_recs = np.array([(target_ids[ind], \
                     preferences[ind]) for ind in sorted_preferences])
        else:
            top_n_recs = np.array([target_ids[ind]
                 for ind in sorted_preferences])

        return top_n_recs
<|endoftext|>


<commit_before># Copyright (C) 2014 Universidad Politecnica de Madrid
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging

from django import http
from django.contrib import auth
from django.core import exceptions as django_exceptions
from django.core.urlresolvers import reverse_lazy, reverse
from django.shortcuts import redirect
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View
from django.views.generic.edit import FormView

from openstack_auth import views as auth_views
from openstack_dashboard import fiware_api
from openstack_dashboard.dashboards.idm import utils as idm_utils
from openstack_dashboard.fiware_oauth2 import forms
# from openstack_auth import forms
from openstack_dashboard.fiware_auth import forms as fiware_auth_forms

from horizon import messages

LOG = logging.getLogger('idm_logger')

class AuthorizeView(FormView):
    """ Shows the user info about the application requesting authorization. If its the first
    time (the user has never authorized this application before)
    """
    template_name = 'oauth2/authorize.html'
    form_class = forms.AuthorizeForm
    application_credentials = {}
    success_url = reverse_lazy('horizon:user_home')
    oauth_data = {}

    def dispatch(self, request, *args, **kwargs):
        try:
            self.application = fiware_api.keystone.application_get(
                request,
                request.GET.get('client_id'),
                use_idm_account=True)
        except Exception:
            msg = ('Unable to retrieve application.')
            LOG.error(msg)
            messages.error(request, (msg))
            return redirect('horizon:user_home')

        # Load credentials from the request
        self.application_credentials = self.load_credentials(request)

        if request.user.is_authenticated():
            # continue
            return super(AuthorizeView, self).dispatch(request, *args, **kwargs)
        else:
            # redirect to the login page showing some info about the application
            self.application.avatar = idm_utils.get_avatar(
                self.application,
                'img_medium', idm_utils.DEFAULT_APP_MEDIUM_AVATAR)
            context = {
                'next':(reverse('fiware_oauth2_authorize') + '?'
                    + self.request.GET.urlencode()),
                'redirect_field_name': auth.REDIRECT_FIELD_NAME,
                'show_application_details':True,
                'application':self.application,
            }
            LOG.debug('OAUTH2: Login page with consumer details')

            return auth_views.login(request,
                                extra_context=context,
                                form_class=fiware_auth_forms.LoginWithEmailForm,
                                **kwargs)

    def load_credentials(self, request):
        # TODO(garcianavalon) check it's set to code
        credentials = {
            'response_type':request.GET.get('response_type'),
            'application_id':request.GET.get('client_id'),
            'redirect_uri':request.GET.get('redirect_uri'),
            'state':request.GET.get('state'),
        }
        return credentials

    def _request_authorization(self, request, credentials):
        # forward the request to the keystone server to store the credentials
        try:
            self.oauth_data = \
                fiware_api.keystone.request_authorization_for_application(
                    request,
                    credentials.get('application_id'),
                    credentials.get('redirect_uri'),
                    response_type=credentials.get('response_type'),
                    state=credentials.get('state', None))
        except Exception as e:
            LOG.warning(('OAUTH2: exception when requesting '
                         'authorization %s'), e)
            # TODO(garcianavalon) finner exception handling
            self.oauth_data = {
                'error': e
            }

    def _already_authorized(self, request, credentials):
        # check if the user already authorized the app for that redirect uri
        app_id = credentials.get('application_id', None)
        if not app_id:
            LOG.debug('OAUTH2: no application_id in credentials')
            return False
        try:
            access_tokens = fiware_api.keystone.get_user_access_tokens(
                request, request.user.id)
            for token in access_tokens:
                if token.consumer_id == app_id:
                    LOG.debug(('OAUTH2: Application %s already '
                               'authorized'), app_id)
                    return True
            LOG.debug(('OAUTH2: Application %s NOT already'
                       'authorized'), app_id)
            return False
        except Exception as e:
            LOG.error(('OAUTH2: exception when checking'
                       'access tokens %s'), e)
            # TODO(garcianavalon) finner exception handling
            return False

    def get(self, request, *args, **kwargs):
        """Show a form with info about the scopes and the application to the user"""
        if self.application_credentials:
            self._request_authorization(request, self.application_credentials)

            # check if user already authorized this app
            if self._already_authorized(request, self.application_credentials):
                return self.obtain_access_token(request)

            # if not, request authorization from user
            return super(AuthorizeView, self).get(request, *args, **kwargs)
        else:
            LOG.debug('OAUTH2: there is no pending authorization request, redirect to index')
            return redirect('horizon:user_home')

    def get_context_data(self, **kwargs):
        context = super(AuthorizeView, self).get_context_data(**kwargs)
        context['oauth_data'] = self.oauth_data
        context['application_credentials'] = self.application_credentials
        context['application'] = self.application
        context['query_string'] = '?' + self.request.GET.urlencode()
        return context

    def post(self, request, *args, **kwargs):
        # Pass request to get_form
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        if form.is_valid():
            # Pass request to form_valid.
            return self.form_valid(request, form)
        else:
            return self.form_invalid(form)

    def form_valid(self, request, form):
        return self.obtain_access_token(request)

    def form_invalid(self, form):
        # NOTE(garcianavalon) there is no case right now where this form would be
        # invalid, because is an empty form. In the future we might use a more complex
        # form (multiple choice scopes for example)
        pass

    def obtain_access_token(self, request):
        try:
            if self.application_credentials.get('response_type', None) == 'code':

                authorization_code = fiware_api.keystone.authorize_application(
                    request,
                    application=self.application_credentials['application_id'])

                LOG.debug('OAUTH2: Authorization Code obtained %s',
                    authorization_code.code)
                # redirect resource owner to client with the authorization code
                LOG.debug('OAUTH2: Redirecting user back to %s',
                    authorization_code.redirect_uri)

                # NOTE(garcianavalon) to support custom schemes for mobile apps
                # Create custom response to avoid security check in protocol
                res = http.HttpResponse(authorization_code.redirect_uri, status=302)
                res['Location'] = authorization_code.redirect_uri
                return res

            elif self.application_credentials.get('response_type', None) == 'token':
                # Implicit grant
                response = fiware_api.keystone.forward_implicit_grant_authorization_request(
                    request,
                    application_id=self.application_credentials['application_id'])

                # NOTE(garcianavalon) to support custom schemes for mobile apps
                # Create custom response to avoid security check in protocol
                res = http.HttpResponse(
                    response.headers['location'],
                    status=response.status_code)
                res['Location'] = response.headers['location']
                res.reason_phrase = response.reason
                return res

        except Exception as e:
            LOG.error('OAUTH2: exception when authorizing %s', e)
            msg = (('An error occurred when trying to obtain'
                    'the authorization code.'))
            messages.error(request, (msg))
            return redirect('horizon:user_home')

def cancel_authorize(request, **kwargs):
    LOG.debug('OAUTH2: authorization request dennied, redirect to home')
    return redirect('horizon:user_home')

class AccessTokenView(View):
    """ Handles the access token request form the clients (applications). Forwards the
    request to the Keystone backend.
    """
    @csrf_exempt
    def dispatch(self, request, *args, **kwargs):
        return super(AccessTokenView, self).dispatch(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        # NOTE(garcianavalon) Instead of using the client we simply redirect the request
        # because is simpler than extracting all the data to make the exact same request
        # again from it
        LOG.debug('OAUTH2: forwading the access_token request')
        try:
            response = fiware_api.keystone.forward_access_token_request(request)
            return http.HttpResponse(
                content=response.content,
                content_type=response.headers['content-type'],
                status=response.status_code,
                reason=response.reason)
        except django_exceptions.PermissionDenied:
            return http.HttpResponseBadRequest(
                content='Authentication header missing. Use HTTP Basic.')

class UserInfoView(View):
    """ Forwards to the Keystone backend the validate token request (access the user info).
    """

    def get(self, request, *args, **kwargs):
        # NOTE(garcianavalon) Instead of using the client we simply redirect the request
        # because is simpler than extracting all the data to make the exact same request
        # again from it
        LOG.debug('OAUTH2: forwading the user info (validate token) request')
        response = fiware_api.keystone.forward_validate_token_request(request)
        return http.HttpResponse(content=response.content,
                            content_type=response.headers['content-type'],
                            status=response.status_code,
                            reason=response.reason)<commit_msg>better handling of missing client_id in oauth parameters<commit_after># Copyright (C) 2014 Universidad Politecnica de Madrid
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging

from django import http
from django.contrib import auth
from django.core import exceptions as django_exceptions
from django.core.urlresolvers import reverse_lazy, reverse
from django.shortcuts import redirect
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View
from django.views.generic.edit import FormView

from openstack_auth import views as auth_views
from openstack_dashboard import fiware_api
from openstack_dashboard.dashboards.idm import utils as idm_utils
from openstack_dashboard.fiware_oauth2 import forms
# from openstack_auth import forms
from openstack_dashboard.fiware_auth import forms as fiware_auth_forms

from horizon import messages

LOG = logging.getLogger('idm_logger')

class AuthorizeView(FormView):
    """ Shows the user info about the application requesting authorization. If its the first
    time (the user has never authorized this application before)
    """
    template_name = 'oauth2/authorize.html'
    form_class = forms.AuthorizeForm
    application_credentials = {}
    success_url = reverse_lazy('horizon:user_home')
    oauth_data = {}

    def dispatch(self, request, *args, **kwargs):
        # Load credentials from the request
        self.application_credentials = self.load_credentials(request)

        if not self.application_credentials['application_id']:

            # if no client_id was found, notify and progress no further
            messages.error(request, 'OUATH2 ERROR: client_id is missing in query_string')
            context = {
                'next':(reverse('fiware_oauth2_authorize') + '?'
                    + self.request.GET.urlencode()),
                'redirect_field_name': auth.REDIRECT_FIELD_NAME,
            }
            return auth_views.login(
                request,
                extra_context=context,
                form_class=fiware_auth_forms.LoginWithEmailForm,
                **kwargs)

        # Get the application details
        try:
            self.application = fiware_api.keystone.application_get(
                request,
                self.application_credentials['application_id'],
                use_idm_account=True)

        except Exception:
            msg = ('Unable to retrieve application.')
            LOG.exception(msg)
            messages.error(request, (msg))
            return redirect('horizon:user_home')

        if request.user.is_authenticated():
            # continue
            return super(AuthorizeView, self).dispatch(request, *args, **kwargs)
        else:
            # redirect to the login page showing some info about the application
            self.application.avatar = idm_utils.get_avatar(
                self.application,
                'img_medium', idm_utils.DEFAULT_APP_MEDIUM_AVATAR)
            context = {
                'next':(reverse('fiware_oauth2_authorize') + '?'
                    + self.request.GET.urlencode()),
                'redirect_field_name': auth.REDIRECT_FIELD_NAME,
                'show_application_details':True,
                'application':self.application,
            }

            LOG.debug('OAUTH2: Login page with consumer details')

            return auth_views.login(
                request,
                extra_context=context,
                form_class=fiware_auth_forms.LoginWithEmailForm,
                **kwargs)

    def load_credentials(self, request):
        # TODO(garcianavalon) check it's set to code
        credentials = {
            'response_type': request.GET.get('response_type'),
            'application_id': request.GET.get('client_id'),
            'redirect_uri': request.GET.get('redirect_uri'),
            'state': request.GET.get('state'),
        }
        return credentials

    def _request_authorization(self, request, credentials):
        # forward the request to the keystone server to store the credentials
        try:
            self.oauth_data = \
                fiware_api.keystone.request_authorization_for_application(
                    request,
                    credentials.get('application_id'),
                    credentials.get('redirect_uri'),
                    response_type=credentials.get('response_type'),
                    state=credentials.get('state', None))
        except Exception as e:
            LOG.warning(('OAUTH2: exception when requesting '
                         'authorization %s'), e)
            # TODO(garcianavalon) finner exception handling
            self.oauth_data = {
                'error': e
            }

    def _already_authorized(self, request, credentials):
        # check if the user already authorized the app for that redirect uri
        app_id = credentials.get('application_id', None)
        if not app_id:
            LOG.debug('OAUTH2: no application_id in credentials')
            return False
        try:
            access_tokens = fiware_api.keystone.get_user_access_tokens(
                request, request.user.id)
            for token in access_tokens:
                if token.consumer_id == app_id:
                    LOG.debug(('OAUTH2: Application %s already '
                               'authorized'), app_id)
                    return True
            LOG.debug(('OAUTH2: Application %s NOT already'
                       'authorized'), app_id)
            return False
        except Exception as e:
            LOG.error(('OAUTH2: exception when checking'
                       'access tokens %s'), e)
            # TODO(garcianavalon) finner exception handling
            return False

    def get(self, request, *args, **kwargs):
        """Show a form with info about the scopes and the application to the user"""
        if self.application_credentials:
            self._request_authorization(request, self.application_credentials)

            # check if user already authorized this app
            if self._already_authorized(request, self.application_credentials):
                return self.obtain_access_token(request)

            # if not, request authorization from user
            return super(AuthorizeView, self).get(request, *args, **kwargs)
        else:
            LOG.debug('OAUTH2: there is no pending authorization request, redirect to index')
            return redirect('horizon:user_home')

    def get_context_data(self, **kwargs):
        context = super(AuthorizeView, self).get_context_data(**kwargs)
        context['oauth_data'] = self.oauth_data
        context['application_credentials'] = self.application_credentials
        context['application'] = self.application
        context['query_string'] = '?' + self.request.GET.urlencode()
        return context

    def post(self, request, *args, **kwargs):
        # Pass request to get_form
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        if form.is_valid():
            # Pass request to form_valid.
            return self.form_valid(request, form)
        else:
            return self.form_invalid(form)

    def form_valid(self, request, form):
        return self.obtain_access_token(request)

    def form_invalid(self, form):
        # NOTE(garcianavalon) there is no case right now where this form would be
        # invalid, because is an empty form. In the future we might use a more complex
        # form (multiple choice scopes for example)
        pass

    def obtain_access_token(self, request):
        try:
            if self.application_credentials.get('response_type', None) == 'code':

                authorization_code = fiware_api.keystone.authorize_application(
                    request,
                    application=self.application_credentials['application_id'])

                LOG.debug('OAUTH2: Authorization Code obtained %s',
                    authorization_code.code)
                # redirect resource owner to client with the authorization code
                LOG.debug('OAUTH2: Redirecting user back to %s',
                    authorization_code.redirect_uri)

                # NOTE(garcianavalon) to support custom schemes for mobile apps
                # Create custom response to avoid security check in protocol
                res = http.HttpResponse(authorization_code.redirect_uri, status=302)
                res['Location'] = authorization_code.redirect_uri
                return res

            elif self.application_credentials.get('response_type', None) == 'token':
                # Implicit grant
                response = fiware_api.keystone.forward_implicit_grant_authorization_request(
                    request,
                    application_id=self.application_credentials['application_id'])

                # NOTE(garcianavalon) to support custom schemes for mobile apps
                # Create custom response to avoid security check in protocol
                res = http.HttpResponse(
                    response.headers['location'],
                    status=response.status_code)
                res['Location'] = response.headers['location']
                res.reason_phrase = response.reason
                return res

        except Exception as e:
            LOG.error('OAUTH2: exception when authorizing %s', e)
            msg = (('An error occurred when trying to obtain'
                    'the authorization code.'))
            messages.error(request, (msg))
            return redirect('horizon:user_home')

def cancel_authorize(request, **kwargs):
    LOG.debug('OAUTH2: authorization request dennied, redirect to home')
    return redirect('horizon:user_home')

class AccessTokenView(View):
    """ Handles the access token request form the clients (applications). Forwards the
    request to the Keystone backend.
    """
    @csrf_exempt
    def dispatch(self, request, *args, **kwargs):
        return super(AccessTokenView, self).dispatch(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        # NOTE(garcianavalon) Instead of using the client we simply redirect the request
        # because is simpler than extracting all the data to make the exact same request
        # again from it
        LOG.debug('OAUTH2: forwading the access_token request')
        try:
            response = fiware_api.keystone.forward_access_token_request(request)
            return http.HttpResponse(
                content=response.content,
                content_type=response.headers['content-type'],
                status=response.status_code,
                reason=response.reason)
        except django_exceptions.PermissionDenied:
            return http.HttpResponseBadRequest(
                content='Authentication header missing. Use HTTP Basic.')

class UserInfoView(View):
    """ Forwards to the Keystone backend the validate token request (access the user info).
    """

    def get(self, request, *args, **kwargs):
        # NOTE(garcianavalon) Instead of using the client we simply redirect the request
        # because is simpler than extracting all the data to make the exact same request
        # again from it
        LOG.debug('OAUTH2: forwading the user info (validate token) request')
        response = fiware_api.keystone.forward_validate_token_request(request)
        return http.HttpResponse(content=response.content,
                            content_type=response.headers['content-type'],
                            status=response.status_code,
                            reason=response.reason)<|endoftext|>


<commit_before>#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import argparse
from datetime import datetime

from dateutil.parser import parse
from dateutil.relativedelta import relativedelta

from utils import get_target_directory_from_config_file
from utils.summary_downloader import SummaryDownloader

if __name__ == '__main__':

    # retrieving arguments specified on command line
    parser = argparse.ArgumentParser(
        description='Download NHL game summary reports.')
    parser.add_argument(
        '-d', '--tgt_dir', dest='tgt_dir', required=False,
        metavar='download target directory',
        help="Target directory for downloads")
    parser.add_argument(
        '-f', '--from', dest='from_date', required=False,
        metavar='first date to download summaries for',
        help="The first date summaries will be downloaded for")
    parser.add_argument(
        '-t', '--to', dest='to_date', required=False,
        metavar='last date to download summaries for',
        help="The last date summaries will be downloaded for")
    parser.add_argument(
        '--exclude', dest='exclude', required=False, nargs='+',
        choices=['game_feed', 'shift_chart', 'html_reports'],
        help="Exclude the specified datasets from downloading")

    args = parser.parse_args()

    # setting time interval of interest from command line options
    from_date = args.from_date
    to_date = args.to_date

    # setting target directory from command line options...
    if args.tgt_dir is not None:
        tgt_dir = args.tgt_dir
    # ...or from configuration file
    else:
        cfg_src = os.path.join(os.path.dirname(__file__), r"_config.ini")
        tgt_dir = get_target_directory_from_config_file(cfg_src, 'downloading')

    # bailing out if target directory doesn't exist
    if not os.path.isdir(tgt_dir):
        print("+ Download target directory '%s' does not exist" % tgt_dir)
        print("+ Update configuration or specify via -d/--tgt_dir option")
        sys.exit()

    # setting first date to download summaries for if not specified
    if from_date is None:
        # using previously downloaded files in target directory to retrieve
        # last date data have already been downloaded before
        all_dates = list()
        for root, dirs, files in os.walk(tgt_dir):
            for file in files:
                if file.lower().endswith(".zip") and file.lower()[0].isdigit():
                    try:
                        curr_date = parse(os.path.basename(file.split(".")[0]))
                        all_dates.append(curr_date)
                    except:
                        pass

        from_date = (sorted(all_dates)[-1] + relativedelta(days=1)).strftime(
            "%B %d, %Y")

    # setting last date to download summaries for...
    if to_date is None:
        # ...to same as first date to download summaries for if this one is set
        if args.from_date:
            to_date = from_date
        # ...to date before current one otherwise
        else:
            to_date = (datetime.now() + relativedelta(days=-1)).strftime(
                "%B %d, %Y")

    downloader = SummaryDownloader(
        tgt_dir, from_date, to_date, workers=8, exclude=args.exclude)
    downloader.run()
<commit_msg>Adjust exception handling<commit_after>#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import argparse
from datetime import datetime

from dateutil.parser import parse
from dateutil.relativedelta import relativedelta

from utils import get_target_directory_from_config_file
from utils.summary_downloader import SummaryDownloader

if __name__ == '__main__':

    # retrieving arguments specified on command line
    parser = argparse.ArgumentParser(
        description='Download NHL game summary reports.')
    parser.add_argument(
        '-d', '--tgt_dir', dest='tgt_dir', required=False,
        metavar='download target directory',
        help="Target directory for downloads")
    parser.add_argument(
        '-f', '--from', dest='from_date', required=False,
        metavar='first date to download summaries for',
        help="The first date summaries will be downloaded for")
    parser.add_argument(
        '-t', '--to', dest='to_date', required=False,
        metavar='last date to download summaries for',
        help="The last date summaries will be downloaded for")
    parser.add_argument(
        '--exclude', dest='exclude', required=False, nargs='+',
        choices=['game_feed', 'shift_chart', 'html_reports'],
        help="Exclude the specified datasets from downloading")

    args = parser.parse_args()

    # setting time interval of interest from command line options
    from_date = args.from_date
    to_date = args.to_date

    # setting target directory from command line options...
    if args.tgt_dir is not None:
        tgt_dir = args.tgt_dir
    # ...or from configuration file
    else:
        cfg_src = os.path.join(os.path.dirname(__file__), r"_config.ini")
        tgt_dir = get_target_directory_from_config_file(cfg_src, 'downloading')

    # bailing out if target directory doesn't exist
    if not os.path.isdir(tgt_dir):
        print("+ Download target directory '%s' does not exist" % tgt_dir)
        print("+ Update configuration or specify via -d/--tgt_dir option")
        sys.exit()

    # setting first date to download summaries for if not specified
    if from_date is None:
        # using previously downloaded files in target directory to retrieve
        # last date data have already been downloaded before
        all_dates = list()
        for root, dirs, files in os.walk(tgt_dir):
            for file in files:
                if file.lower().endswith(".zip") and file.lower()[0].isdigit():
                    try:
                        curr_date = parse(os.path.basename(file.split(".")[0]))
                        all_dates.append(curr_date)
                    except Exception as e:
                        pass

        from_date = (sorted(all_dates)[-1] + relativedelta(days=1)).strftime(
            "%B %d, %Y")

    # setting last date to download summaries for...
    if to_date is None:
        # ...to same as first date to download summaries for if this one is set
        if args.from_date:
            to_date = from_date
        # ...to date before current one otherwise
        else:
            to_date = (datetime.now() + relativedelta(days=-1)).strftime(
                "%B %d, %Y")

    downloader = SummaryDownloader(
        tgt_dir, from_date, to_date, workers=8, exclude=args.exclude)
    downloader.run()
<|endoftext|>


<commit_before>"""Templates and placeholders."""
import ast
import operator as op
import abc

from mpf.core.mpf_controller import MpfController

# supported operators
operators = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul,
             ast.Div: op.truediv, ast.Pow: op.pow, ast.BitXor: op.xor,
             ast.USub: op.neg, ast.Not: op.not_}

bool_operators = {ast.And: lambda a, b: a and b, ast.Or: lambda a, b: a or b}

comparisons = {ast.Eq: op.eq, ast.Lt: op.lt, ast.Gt: op.gt, ast.LtE: op.le, ast.GtE: op.ge}

class BaseTemplate(metaclass=abc.ABCMeta):

    """Base class for templates."""

    def __init__(self, template, placeholder_manger, default_value):
        """Initialise template."""
        self.template = template
        self.placeholder_manager = placeholder_manger
        self.default_value = default_value

    @abc.abstractmethod
    def evaluate(self, parameters, fail_on_missing_params=False):
        """Evaluate template."""
        pass

class BoolTemplate(BaseTemplate):

    """Bool template."""

    def evaluate(self, parameters, fail_on_missing_params=False):
        """Evaluate template to bool."""
        try:
            result = self.placeholder_manager.evaluate_template(self.template, parameters)
        except ValueError:
            if fail_on_missing_params:
                raise
            return self.default_value
        return bool(result)

class FloatTemplate(BaseTemplate):

    """Float template."""

    def evaluate(self, parameters, fail_on_missing_params=False):
        """Evaluate template to float."""
        try:
            result = self.placeholder_manager.evaluate_template(self.template, parameters)
        except ValueError:
            if fail_on_missing_params:
                raise
            return self.default_value
        return float(result)

class IntTemplate(BaseTemplate):

    """Float template."""

    def evaluate(self, parameters, fail_on_missing_params=False):
        """Evaluate template to float."""
        try:
            result = self.placeholder_manager.evaluate_template(self.template, parameters)
        except ValueError:
            if fail_on_missing_params:
                raise
            return self.default_value
        return int(result)

class PlaceholderManager(MpfController):

    """Manages templates and placeholders for MPF."""

    @staticmethod
    def _parse_template(template_str):
        return ast.parse(template_str, mode='eval').body

    def _eval(self, node, variables):
        if isinstance(node, ast.Num):   # <number>
            return node.n
        elif isinstance(node, ast.Str):
            return node.s
        elif node is None:
            return None
        elif isinstance(node, ast.NameConstant):   # <number>
            return node.value
        elif isinstance(node, ast.BinOp):       # <left> <operator> <right>
            return operators[type(node.op)](self._eval(node.left, variables), self._eval(node.right, variables))
        elif isinstance(node, ast.UnaryOp):     # <operator> <operand> e.g., -1
            return operators[type(node.op)](self._eval(node.operand, variables))
        elif isinstance(node, ast.Compare):
            if len(node.ops) > 1:
                raise AssertionError("Only single comparisons are supported.")
            return comparisons[type(node.ops[0])](self._eval(node.left, variables),
                                                  self._eval(node.comparators[0], variables))
        elif isinstance(node, ast.BoolOp):
            result = self._eval(node.values[0], variables)
            for i in range(1, len(node.values)):
                result = bool_operators[type(node.op)](result,
                                                       self._eval(node.values[i], variables))
            return result
        elif isinstance(node, ast.Attribute):
            return getattr(self._eval(node.value, variables), node.attr)
        elif isinstance(node, ast.Subscript):
            if isinstance(node.slice, ast.Index):
                return self._eval(node.value, variables)[self._eval(node.slice.value, variables)]
            elif isinstance(node.slice, ast.Slice):
                return self._eval(node.value, variables)[self._eval(node.slice.lower, variables):self._eval(node.slice.upper, variables):self._eval(node.slice.step, variables)]
            else:
                raise TypeError(type(node))
        elif isinstance(node, ast.Name):
            if node.id in variables:
                return variables[node.id]
            else:
                raise ValueError("Mising variable {}".format(node.id))
        else:
            raise TypeError(type(node))

    def build_float_template(self, template_str, default_value=0.0):
        """Build a float template from a string."""
        return FloatTemplate(self._parse_template(template_str), self, default_value)

    def build_int_template(self, template_str, default_value=0):
        """Build a int template from a string."""
        return IntTemplate(self._parse_template(template_str), self, default_value)

    def build_bool_template(self, template_str, default_value=False):
        """Build a bool template from a string."""
        return BoolTemplate(self._parse_template(template_str), self, default_value)

    def evaluate_template(self, template, parameters):
        """Evaluate template"""
        parameters["settings"] = self.machine.settings
        parameters["current_player"] = self.machine.game.player
        parameters["players"] = self.machine.game.player_list
        parameters["game"] = self.machine.game
        return self._eval(template, parameters)
<commit_msg>make game parameters optional<commit_after>"""Templates and placeholders."""
import ast
import operator as op
import abc

from mpf.core.mpf_controller import MpfController

# supported operators
operators = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul,
             ast.Div: op.truediv, ast.Pow: op.pow, ast.BitXor: op.xor,
             ast.USub: op.neg, ast.Not: op.not_}

bool_operators = {ast.And: lambda a, b: a and b, ast.Or: lambda a, b: a or b}

comparisons = {ast.Eq: op.eq, ast.Lt: op.lt, ast.Gt: op.gt, ast.LtE: op.le, ast.GtE: op.ge}

class BaseTemplate(metaclass=abc.ABCMeta):

    """Base class for templates."""

    def __init__(self, template, placeholder_manger, default_value):
        """Initialise template."""
        self.template = template
        self.placeholder_manager = placeholder_manger
        self.default_value = default_value

    @abc.abstractmethod
    def evaluate(self, parameters, fail_on_missing_params=False):
        """Evaluate template."""
        pass

class BoolTemplate(BaseTemplate):

    """Bool template."""

    def evaluate(self, parameters, fail_on_missing_params=False):
        """Evaluate template to bool."""
        try:
            result = self.placeholder_manager.evaluate_template(self.template, parameters)
        except ValueError:
            if fail_on_missing_params:
                raise
            return self.default_value
        return bool(result)

class FloatTemplate(BaseTemplate):

    """Float template."""

    def evaluate(self, parameters, fail_on_missing_params=False):
        """Evaluate template to float."""
        try:
            result = self.placeholder_manager.evaluate_template(self.template, parameters)
        except ValueError:
            if fail_on_missing_params:
                raise
            return self.default_value
        return float(result)

class IntTemplate(BaseTemplate):

    """Float template."""

    def evaluate(self, parameters, fail_on_missing_params=False):
        """Evaluate template to float."""
        try:
            result = self.placeholder_manager.evaluate_template(self.template, parameters)
        except ValueError:
            if fail_on_missing_params:
                raise
            return self.default_value
        return int(result)

class PlaceholderManager(MpfController):

    """Manages templates and placeholders for MPF."""

    @staticmethod
    def _parse_template(template_str):
        return ast.parse(template_str, mode='eval').body

    def _eval(self, node, variables):
        if isinstance(node, ast.Num):   # <number>
            return node.n
        elif isinstance(node, ast.Str):
            return node.s
        elif node is None:
            return None
        elif isinstance(node, ast.NameConstant):   # <number>
            return node.value
        elif isinstance(node, ast.BinOp):       # <left> <operator> <right>
            return operators[type(node.op)](self._eval(node.left, variables), self._eval(node.right, variables))
        elif isinstance(node, ast.UnaryOp):     # <operator> <operand> e.g., -1
            return operators[type(node.op)](self._eval(node.operand, variables))
        elif isinstance(node, ast.Compare):
            if len(node.ops) > 1:
                raise AssertionError("Only single comparisons are supported.")
            return comparisons[type(node.ops[0])](self._eval(node.left, variables),
                                                  self._eval(node.comparators[0], variables))
        elif isinstance(node, ast.BoolOp):
            result = self._eval(node.values[0], variables)
            for i in range(1, len(node.values)):
                result = bool_operators[type(node.op)](result,
                                                       self._eval(node.values[i], variables))
            return result
        elif isinstance(node, ast.Attribute):
            return getattr(self._eval(node.value, variables), node.attr)
        elif isinstance(node, ast.Subscript):
            if isinstance(node.slice, ast.Index):
                return self._eval(node.value, variables)[self._eval(node.slice.value, variables)]
            elif isinstance(node.slice, ast.Slice):
                return self._eval(node.value, variables)[self._eval(node.slice.lower, variables):self._eval(node.slice.upper, variables):self._eval(node.slice.step, variables)]
            else:
                raise TypeError(type(node))
        elif isinstance(node, ast.Name):
            if node.id in variables:
                return variables[node.id]
            else:
                raise ValueError("Mising variable {}".format(node.id))
        else:
            raise TypeError(type(node))

    def build_float_template(self, template_str, default_value=0.0):
        """Build a float template from a string."""
        return FloatTemplate(self._parse_template(template_str), self, default_value)

    def build_int_template(self, template_str, default_value=0):
        """Build a int template from a string."""
        return IntTemplate(self._parse_template(template_str), self, default_value)

    def build_bool_template(self, template_str, default_value=False):
        """Build a bool template from a string."""
        return BoolTemplate(self._parse_template(template_str), self, default_value)

    def evaluate_template(self, template, parameters):
        """Evaluate template"""
        parameters["settings"] = self.machine.settings
        if self.machine.game:
            parameters["current_player"] = self.machine.game.player
            parameters["players"] = self.machine.game.player_list
            parameters["game"] = self.machine.game
        return self._eval(template, parameters)
<|endoftext|>


<commit_before>import fileinput
import logging
import os
import re
import sys

from errors import *

log = logging.getLogger('Core')

class TvRenamr():
    def __init__(self, working_dir, log_level='info', log_file=None):
        """
        :param working_dir: The working directory.
        :param log_level: The log level to set.
        """
        self.working_dir = working_dir
        logging.getLogger().setLevel(self.__set_log_level(log_level))
        self.log_file = log_file

    def extract_episode_details_from_file(self, fn, user_regex=None):
        """
        Looks at the file given and extracts from it the show title, it's season number and episode number
        using regular expression magic. The default formats accepted are: series.0x00.xxx or series.s0e00.xxx
        or series.000.xxx
        A user can specify their own regular expression for a format not already covered.

        :param fn: The file name passed in.
        :param user_regex: A user specified regular expression. Defaults to None.

        :returns: The show name, season number, episode number and last four characters (assumed to be the extension) extracted from
        the file passed in.
        :rtype: A dictionary with the keys 'show', 'season', 'episode' and 'extension'.
        """
        fn = fn.replace("_", ".").replace(" ", ".")
        log.info('Renaming file: '+fn)
        regex = self.__build_regex(user_regex)
        log.info('Renaming using: '+regex)
        m = re.compile(regex).match(fn)
        if m is not None:
            show = m.group('show').replace('.',' ').strip()
            log.debug('Returned show: %s, season: %s, episode: %s, extension: %s' % (show, m.group('season'), m.group('episode'), fn[-4:]))
            return {'show': show, 'season': m.group('season'), 'episode': m.group('episode'), 'extension': fn[-4:]}
        else: raise UnexpectedFormatException(fn)

    def convert_show_names_using_exceptions_file(self, exceptions_file, show_name):
        """
        Converts a show name to a new name specified in the exceptions_file. This method is designed for use in
        conjunction with the daemon.

        :param exceptions_file: Location of the exceptions file to use.
        :param show_name: The show name to replace from the exceptions file.

        :returns: The new show name.
        :rtype: A string.
        """
        for show in [line.strip().split(' => ') for line in fileinput.input(exceptions_file) if not line.startswith('#')]:
            try:
                if show[0] == show_name.lower():
                    log.debug('Replacing '+show_name+' with '+show[1])
                    return show[1]
            except ShowNotInExceptionsList(show_name): pass

    def retrieve_episode_name(self, show, season, episode, library='tvrage'):
        """
        Retrieves the name of a given episode. The series name, season and episode numbers must be specified
        to get the episode's name. The library can be specified by the user, but will default to Tv Rage.

        :param show: The show name to search for.
        :param season: The season number to search for.
        :param episode: The episode number to search for.
        :param library: The library to search in.

        :returns: The episode title.
        :rtype: A string.
        """
        if library == 'thetvdb':
            from lib.thetvdb import TheTvDb as library
            log.debug('Opening The Tv Db library')
        elif library == 'tvrage':
            from lib.tvrage import TvRage as library
            log.debug('Opening Tv Rage library')

        lib = library(show)
        return lib.get_episode_name(str(int(season)), episode)

    def move_leading_the_to_trailing_the(self, show_name):
        """
        Moves the leading 'The' of a show name to a trailing 'The'. A comma and space are added before the new 'The'.

        :param show_name: The show name.

        :returns: The new show name.
        :rtype: A string.
        """
        if not(show_name.startswith('The ')): raise NoLeadingTheException(show_name)
        log.debug('Moving the leading \'The\' to end of: '+show_name)
        return show_name[4:]+', The'

    def remove_part_from_multiple_episodes(self, show_name):
        """
        In episode titles of multiple part episodes that use the format (Part n) remove the 'Part ' section so
        the format is (n)
        """
        log.debug('Removing Part from episode name')
        return show_name.replace('(Part ','(')

    def set_format_for_multiple_part_episodes(self, show_name, format):
        """
        Set the format for multiple part episodes and return a formatted episode title
        """
        # find the current multiple part section
        # extract multipart number
        # build new episode title
        pass

    def build_path(self, show, season, episode, title, extension, format=None, renamed_dir=None, organise=False):
        """
        Set the output format for the file name of a renamed show. By default the format is:
        Show Name - Season NumberEpisode Number - Episode Title.format.

        Builds the new path for the file to be renamed to, by default this is the working directory. Users can
        specify a directory to move files to once renamed using the renamed_dir option. The auto_move option
        can be used to specify a top level directory where files will be placed in season and show folders,
        i.e. Show/Season 1/episodes

        :param show: The show name.
        :param season: The season number.
        :param episode: The episode number.
        :param title: The episode title.
        :param extension: The file extension.
        :param format: The order in of the show name, season and episode numbers, episode title and extension in the renamed file's name.
        :param renamed_dir: The directory to place the renamed file into.
        :param organise: A boolean to set whether the renamed directory path should be constructed from the show name and season number.

        :returns: The full path to the new file including the formatted file name.
        :rtype: A string.
        """
        if format is None: format = '%n - %s%e - %t'
        else:
            error = []
            if format.find('%n') is -1: error.append('show name')
            if format.find('%s') is -1: error.append('season')
            if format.find('%e') is -1: error.append('episode')
            if format.find('%t') is -1: error.append('episode title')
            if len(error) is not 0 : raise OutputFormatMissingSyntaxException(error)

        if len(episode) == 1: episode = '0'+ episode
        formatted = format.replace('%n', show.replace(show[:1], show[:1].upper(), 1)).replace('%s', str(int(season))).replace('%e', episode).replace('%t', title)
        log.info('Destination file: '+formatted)

        if renamed_dir is None: renamed = self.working_dir
        else: renamed = renamed_dir

        if organise is True: renamed = self.__build_organise_path(renamed, show, season)

        log.debug('Destination directory: '+renamed)
        return os.path.join(renamed, formatted+extension)

    def clean_names(self, fn, character_to_replace=':', replacement_character=' -'):
        """
        Cleans the string passed in, making it be safe for all file systems. Also allows the user to specify
        the new characters to be used.
        """
        print fn
        return fn.replace(character_to_replace, replacement_character)

    def rename(self, fn, new_fn):
        """
        Renames the file passed in to the new filename path passed in and returns the new filename

        :param fn: The file to rename.
        :param new_fn: The name to rename the file to.
        """
        if not os.path.exists(new_fn):
            log.info('Beginning rename')
            os.rename(os.path.join(self.working_dir, fn), new_fn)
            renamed = os.path.split(new_fn)
            log.debug('Renamed '+fn+' to '+renamed[1])
        else: raise EpisodeAlreadyExistsInDirectoryException(fn, os.path.split(new_fn)[0])

    def __set_log_level(self, level):
        """
        Converts a user specified log level into a logging object level. By default this is Info.

        :param level: The log level to set.

        :returns: A log level useable by a logging object
        :rtype: A string.

        """
        LEVELS = {
            'debug': logging.DEBUG,     #10
            'info': logging.INFO,       #20
            'warning': logging.WARNING, #30
            'error': logging.ERROR,     #40
            'critical': logging.CRITICAL#50
        }
        return LEVELS.get(level, logging.INFO)

    def __build_regex(self, regex=None):
        """
        Builds the regular expression to extract a files details. Custom syntax can be used in the regular expression to help specify
        parts of the episode's file name. These custom syntax snippets are replaced by the regular expression blocks show.

        %n - [\w\s.,_-]+ - The show name.
        %s - [\d]{1,2} - The season number.
        %e - [\d]{2} - The episode number.

        :param regex: The regular expression string.

        :returns: An actual regular expression.
        :rtype: A string.
        """
        series = '(?P<show>[\w\s.,_-]+)'
        season = '(?P<season>[\d]{1,2})'
        episode = '(?P<episode>[\d]{2})'

        if regex is None: return series+'\.[Ss]?'+season+'[XxEe]?'+episode
        if regex.find('%s') is -1 or regex.find('%e') is -1: raise IncorrectCustomRegularExpressionSyntaxException(regex)

        # series name
        regex = regex.replace('%n', series)

        # season number
        # %s{n}
        if regex.find('%s{') is not -1:
            log.debug('Season digit number found')
            r = regex.split('%s{')[1][:1]
            log.debug('Specified '+r+' season digits')
            s = season.replace('1,2', r)
            regex = regex.replace('%s{'+r+'}', s)
            log.debug('Season regex set: %s' % s)

        # %s
        if regex.find('%s') is not -1:
            regex = regex.replace('%s', season)
            log.debug('Default season regex set: %s' % regex)

        # episode number
        # %e{n}
        if regex.find('%e{') is not -1:
            log.debug('User set episode digit number found')
            r = regex.split('%e{')[1][:1]
            log.debug('User specified '+r+' episode digits')
            e = episode.replace('2', r)
            regex = regex.replace('%e{'+r+'}', e)
            log.debug('Episode regex set: %s' % e)

        # %e
        if regex.find('%e') is not -1:
            regex = regex.replace('%e', episode)
            log.debug('Default episode regex set: %s' % regex)

        return regex

    def __build_organise_path(self, start_path, show_name, season_number):
        """
        Constructs a directory path using the show name and season number of an episode.

        :param start_path: The root path to construct the new path under.
        :param show_name: The show name.
        :param season_number: The season number.

        :return: The path to move a renamed episode to.
        :rtype: A string.
        """
        if start_path[-1:] != '/': start_path = start_path +'/'
        path = start_path + show_name +'/Season '+ str(int(season_number)) +'/'
        if not os.path.exists(path):
            os.makedirs(path)
            log.debug('Directories created for path: '+path)
        return path
    <commit_msg>set the default library to thetvdb<commit_after>import fileinput
import logging
import os
import re
import sys

from errors import *

log = logging.getLogger('Core')

class TvRenamr():
    def __init__(self, working_dir, log_level='info', log_file=None):
        """
        :param working_dir: The working directory.
        :param log_level: The log level to set.
        """
        self.working_dir = working_dir
        logging.getLogger().setLevel(self.__set_log_level(log_level))
        self.log_file = log_file

    def extract_episode_details_from_file(self, fn, user_regex=None):
        """
        Looks at the file given and extracts from it the show title, it's season number and episode number
        using regular expression magic. The default formats accepted are: series.0x00.xxx or series.s0e00.xxx
        or series.000.xxx
        A user can specify their own regular expression for a format not already covered.

        :param fn: The file name passed in.
        :param user_regex: A user specified regular expression. Defaults to None.

        :returns: The show name, season number, episode number and last four characters (assumed to be the extension) extracted from
        the file passed in.
        :rtype: A dictionary with the keys 'show', 'season', 'episode' and 'extension'.
        """
        fn = fn.replace("_", ".").replace(" ", ".")
        log.info('Renaming file: '+fn)
        regex = self.__build_regex(user_regex)
        log.info('Renaming using: '+regex)
        m = re.compile(regex).match(fn)
        if m is not None:
            show = m.group('show').replace('.',' ').strip()
            log.debug('Returned show: %s, season: %s, episode: %s, extension: %s' % (show, m.group('season'), m.group('episode'), fn[-4:]))
            return {'show': show, 'season': m.group('season'), 'episode': m.group('episode'), 'extension': fn[-4:]}
        else: raise UnexpectedFormatException(fn)

    def convert_show_names_using_exceptions_file(self, exceptions_file, show_name):
        """
        Converts a show name to a new name specified in the exceptions_file. This method is designed for use in
        conjunction with the daemon.

        :param exceptions_file: Location of the exceptions file to use.
        :param show_name: The show name to replace from the exceptions file.

        :returns: The new show name.
        :rtype: A string.
        """
        for show in [line.strip().split(' => ') for line in fileinput.input(exceptions_file) if not line.startswith('#')]:
            try:
                if show[0] == show_name.lower():
                    log.debug('Replacing '+show_name+' with '+show[1])
                    return show[1]
            except ShowNotInExceptionsList(show_name): pass

    def retrieve_episode_name(self, show, season, episode, library='thetvdb'):
        """
        Retrieves the name of a given episode. The series name, season and episode numbers must be specified
        to get the episode's name. The library can be specified by the user, but will default to Tv Rage.

        :param show: The show name to search for.
        :param season: The season number to search for.
        :param episode: The episode number to search for.
        :param library: The library to search in.

        :returns: The episode title.
        :rtype: A string.
        """
        if library == 'thetvdb':
            from lib.thetvdb import TheTvDb as library
            log.debug('Opening The Tv Db library')
        elif library == 'tvrage':
            from lib.tvrage import TvRage as library
            log.debug('Opening Tv Rage library')

        lib = library(show)
        return lib.get_episode_name(str(int(season)), episode)

    def move_leading_the_to_trailing_the(self, show_name):
        """
        Moves the leading 'The' of a show name to a trailing 'The'. A comma and space are added before the new 'The'.

        :param show_name: The show name.

        :returns: The new show name.
        :rtype: A string.
        """
        if not(show_name.startswith('The ')): raise NoLeadingTheException(show_name)
        log.debug('Moving the leading \'The\' to end of: '+show_name)
        return show_name[4:]+', The'

    def remove_part_from_multiple_episodes(self, show_name):
        """
        In episode titles of multiple part episodes that use the format (Part n) remove the 'Part ' section so
        the format is (n)
        """
        log.debug('Removing Part from episode name')
        return show_name.replace('(Part ','(')

    def set_format_for_multiple_part_episodes(self, show_name, format):
        """
        Set the format for multiple part episodes and return a formatted episode title
        """
        # find the current multiple part section
        # extract multipart number
        # build new episode title
        pass

    def build_path(self, show, season, episode, title, extension, format=None, renamed_dir=None, organise=False):
        """
        Set the output format for the file name of a renamed show. By default the format is:
        Show Name - Season NumberEpisode Number - Episode Title.format.

        Builds the new path for the file to be renamed to, by default this is the working directory. Users can
        specify a directory to move files to once renamed using the renamed_dir option. The auto_move option
        can be used to specify a top level directory where files will be placed in season and show folders,
        i.e. Show/Season 1/episodes

        :param show: The show name.
        :param season: The season number.
        :param episode: The episode number.
        :param title: The episode title.
        :param extension: The file extension.
        :param format: The order in of the show name, season and episode numbers, episode title and extension in the renamed file's name.
        :param renamed_dir: The directory to place the renamed file into.
        :param organise: A boolean to set whether the renamed directory path should be constructed from the show name and season number.

        :returns: The full path to the new file including the formatted file name.
        :rtype: A string.
        """
        if format is None: format = '%n - %s%e - %t'
        else:
            error = []
            if format.find('%n') is -1: error.append('show name')
            if format.find('%s') is -1: error.append('season')
            if format.find('%e') is -1: error.append('episode')
            if format.find('%t') is -1: error.append('episode title')
            if len(error) is not 0 : raise OutputFormatMissingSyntaxException(error)

        if len(episode) == 1: episode = '0'+ episode
        formatted = format.replace('%n', show.replace(show[:1], show[:1].upper(), 1)).replace('%s', str(int(season))).replace('%e', episode).replace('%t', title)
        log.info('Destination file: '+formatted)

        if renamed_dir is None: renamed = self.working_dir
        else: renamed = renamed_dir

        if organise is True: renamed = self.__build_organise_path(renamed, show, season)

        log.debug('Destination directory: '+renamed)
        return os.path.join(renamed, formatted+extension)

    def clean_names(self, fn, character_to_replace=':', replacement_character=' -'):
        """
        Cleans the string passed in, making it be safe for all file systems. Also allows the user to specify
        the new characters to be used.
        """
        print fn
        return fn.replace(character_to_replace, replacement_character)

    def rename(self, fn, new_fn):
        """
        Renames the file passed in to the new filename path passed in and returns the new filename

        :param fn: The file to rename.
        :param new_fn: The name to rename the file to.
        """
        if not os.path.exists(new_fn):
            log.info('Beginning rename')
            os.rename(os.path.join(self.working_dir, fn), new_fn)
            renamed = os.path.split(new_fn)
            log.debug('Renamed '+fn+' to '+renamed[1])
        else: raise EpisodeAlreadyExistsInDirectoryException(fn, os.path.split(new_fn)[0])

    def __set_log_level(self, level):
        """
        Converts a user specified log level into a logging object level. By default this is Info.

        :param level: The log level to set.

        :returns: A log level useable by a logging object
        :rtype: A string.

        """
        LEVELS = {
            'debug': logging.DEBUG,     #10
            'info': logging.INFO,       #20
            'warning': logging.WARNING, #30
            'error': logging.ERROR,     #40
            'critical': logging.CRITICAL#50
        }
        return LEVELS.get(level, logging.INFO)

    def __build_regex(self, regex=None):
        """
        Builds the regular expression to extract a files details. Custom syntax can be used in the regular expression to help specify
        parts of the episode's file name. These custom syntax snippets are replaced by the regular expression blocks show.

        %n - [\w\s.,_-]+ - The show name.
        %s - [\d]{1,2} - The season number.
        %e - [\d]{2} - The episode number.

        :param regex: The regular expression string.

        :returns: An actual regular expression.
        :rtype: A string.
        """
        series = '(?P<show>[\w\s.,_-]+)'
        season = '(?P<season>[\d]{1,2})'
        episode = '(?P<episode>[\d]{2})'

        if regex is None: return series+'\.[Ss]?'+season+'[XxEe]?'+episode
        if regex.find('%s') is -1 or regex.find('%e') is -1: raise IncorrectCustomRegularExpressionSyntaxException(regex)

        # series name
        regex = regex.replace('%n', series)

        # season number
        # %s{n}
        if regex.find('%s{') is not -1:
            log.debug('Season digit number found')
            r = regex.split('%s{')[1][:1]
            log.debug('Specified '+r+' season digits')
            s = season.replace('1,2', r)
            regex = regex.replace('%s{'+r+'}', s)
            log.debug('Season regex set: %s' % s)

        # %s
        if regex.find('%s') is not -1:
            regex = regex.replace('%s', season)
            log.debug('Default season regex set: %s' % regex)

        # episode number
        # %e{n}
        if regex.find('%e{') is not -1:
            log.debug('User set episode digit number found')
            r = regex.split('%e{')[1][:1]
            log.debug('User specified '+r+' episode digits')
            e = episode.replace('2', r)
            regex = regex.replace('%e{'+r+'}', e)
            log.debug('Episode regex set: %s' % e)

        # %e
        if regex.find('%e') is not -1:
            regex = regex.replace('%e', episode)
            log.debug('Default episode regex set: %s' % regex)

        return regex

    def __build_organise_path(self, start_path, show_name, season_number):
        """
        Constructs a directory path using the show name and season number of an episode.

        :param start_path: The root path to construct the new path under.
        :param show_name: The show name.
        :param season_number: The season number.

        :return: The path to move a renamed episode to.
        :rtype: A string.
        """
        if start_path[-1:] != '/': start_path = start_path +'/'
        path = start_path + show_name +'/Season '+ str(int(season_number)) +'/'
        if not os.path.exists(path):
            os.makedirs(path)
            log.debug('Directories created for path: '+path)
        return path
    <|endoftext|>