<commit_before># -*- coding: utf-8 -*-
##############################################################################
#
#    Author: Nicolas Bessi
#    Copyright 2014 Camptocamp SA
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as
#    published by the Free Software Foundation, either version 3 of the
#    License, or (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields

class credit_control_run(orm.Model):
    """Add computation of fees"""

    _inherit = "credit.control.run"

    def _generate_credit_lines(self, cr, uid, run_id, context=None):
        """Override method to add fees computation"""
        credit_line_ids = super(credit_control_run, self)._generate_credit_lines(
            cr,
            uid,
            run_id,
            context=context
        )
        fees_model = self.pool['credit.control.dunning.fees.computer']
        fees_model._compute_fees(cr, uid, credit_line_ids, context=context)
        return credit_line_ids
<commit_msg>[FIX] flake8 PEP8 for module account_credit_control_dunning_fees<commit_after># -*- coding: utf-8 -*-
##############################################################################
#
#    Author: Nicolas Bessi
#    Copyright 2014 Camptocamp SA
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as
#    published by the Free Software Foundation, either version 3 of the
#    License, or (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm

class credit_control_run(orm.Model):
    """Add computation of fees"""

    _inherit = "credit.control.run"

    def _generate_credit_lines(self, cr, uid, run_id, context=None):
        """Override method to add fees computation"""
        credit_line_ids = super(credit_control_run, self)._generate_credit_lines(
            cr,
            uid,
            run_id,
            context=context
        )
        fees_model = self.pool['credit.control.dunning.fees.computer']
        fees_model._compute_fees(cr, uid, credit_line_ids, context=context)
        return credit_line_ids
<|endoftext|>


<commit_before>import logging
import numpy as np
from copy import deepcopy

log = logging.getLogger(__name__)

class Block:
    """
    A block is a building block for a Quantum Algorithm Experiment.

            :param block_start_position: position of the block start. Defaults to
            before first pulse in list. Position should be changed in case the
            first pulse played is not the first one in the list.
        :param block_end_position: position of the block end. Defaults to after
            last pulse. If given, block_end_position MUST take into account the
            block_start i.e. the block start is added to the pulses before inserting
            the block end at its defined location.
    """
    counter = 0
    INSIDE_BLOCKINFO_NAME = "BlockInfo"

    def __init__(self, block_name, pulse_list:list, pulse_modifs=None,
                 **kw):
        self.name = block_name
        self.pulses = deepcopy(pulse_list)
        self.block_start = kw.get('block_start', {})
        self.block_end = kw.get('block_end', {})
        if pulse_modifs is not None:
            self.pulses = self.pulses_sweepcopy([pulse_modifs], [None])

    def build(self, ref_point="end", ref_point_new="start",
              ref_pulse='previous_pulse', block_delay=0, name=None,
              sweep_dicts_list=None, sweep_index_list=None,
               **kwargs):
        """
        Adds the block shell recursively through the pulse list.
        Returns the flattened pulse list
        :param ref_point: to which point of the previous pulse should this block
        be referenced to. Defaults to "end".
        :param ref_point_new: which point of this block is reference to the
            ref_point. Currently only supports "start".
        :param ref_pulse: to which pulse in the list is this block referenced to.
            Defaults to "previous_pulse" in list.
        :param block_delay: delay before the start of the block
        :param name: a custom name used to prefix the pulses. If None,
            the block name plus a counter is used.
        :param sweep_dicts_list: To build a block that corresponds to a
            point of an N-dimensional sweep, this param is a list of N
            sweep_dicts following the usual pycqed conventions (TODO: where
            to find those?)
            or a SweepPoints object with N dimenstions. Only used if also a
            sweep_index_list is provided. To have an effect, the block has
            to contain pulses with ParametricValues that refer to the
            parameters in the sweep_dicts.
            In addition, parameters of pulses can be modified by pulse modifier
            entries in the sweep dictionaries with the following format:

            'attr=X, key_1=val_1, ..., key_N=val_N'
            Searches all pulses p for which p[key_n] == val_n for all n,
            and sweeps the attribute attr of these pulses.
            If key_n is op_code, it suffices if the initial segment(s) of
            the op_code match (e.g., 'X180' will also match 'X180 qb1').
            If key_n is name or ref_pulse, it suffices if the original
            (ref)pulse name (before a potential change in a previous build())
            matches.

            'attr=X, key_1=val_1, ..., key_N=val_N, occurence=i'
            Sweep the attribute only for the ith pulse matching the
            criteria, where i is an integer (zero-indexed)

        :param sweep_index_list: A list of N indices for an N-dimensional
            sweep. Determines for which sweep points from sweep_dicts_list
            the block should be build. Only used if also a sweep_dicts_list
            is provided.

        :return:
        """
        if ref_point_new != "start":
            raise NotImplementedError("For now can only refer blocks to 'start'")
        if name is None:
            name = self.name + (f"_{self.counter}" if self.counter > 0 else "")
            self.counter += 1

        block_start = {"name": f"start",
                       "pulse_type": "VirtualPulse",
                       "pulse_delay": block_delay,
                       "ref_pulse": ref_pulse,
                       "ref_point": ref_point}
        block_start.update(kwargs.get("block_start", self.block_start))
        block_end = {"name": f"end",
                     "pulse_type": "VirtualPulse"}
        block_end.update(kwargs.get("block_end", self.block_end))
        if sweep_dicts_list is not None and sweep_index_list is not None:
            pulses_built = self.pulses_sweepcopy(sweep_dicts_list, sweep_index_list)
        else:
            pulses_built = deepcopy(self.pulses)

        # check if block_start/end  specified by user
        block_start_specified = False
        block_end_specified = False
        for p in pulses_built:
            if p.get("name", None) == "start":
                block_start = p #save reference
                block_start_specified = True
            elif p.get("name", None) == "end":
                block_end = p
                block_end_specified = True
        # add them if not specified
        if not block_start_specified:
            pulses_built = [block_start] + pulses_built
        if not block_end_specified:
            pulses_built = pulses_built + [block_end]

        for p in pulses_built:
            # if a dictionary wrapping a block is found, compile the inner block.
            if p.get("pulse_type", None) == self.INSIDE_BLOCKINFO_NAME:
                # p needs to have a block key
                assert 'block' in p, f"Inside block {p.get('name', 'Block')} " \
                    f"requires a key 'block' which refers to the uncompiled " \
                    f"block object."
                inside_block = p.pop('block')
                inside_block_pulses = inside_block.build(**p)
                # add all pulses of the inside block to the outer block
                pulses_built.extend(inside_block_pulses)

        # prepend block name to reference pulses and pulses names
        for p in pulses_built:
            # if the pulse has a name, prepend the blockname to it
            if p.get("name", None) is not None:
                p['name'] = name + "-|-" + p['name']

            ref_pulse = p.get("ref_pulse", "previous_pulse")
            p_is_block_start = self._is_block_start(p, block_start)

            # rename ref pulse within the block if not a special name
            escape_names = ("previous_pulse", "segment_start")
            if isinstance(ref_pulse, list):
                p['ref_pulse'] = [name + "-|-" + rp for rp in p['ref_pulse']]
            else:
                if ref_pulse not in escape_names and not p_is_block_start:
                    p['ref_pulse'] = name + "-|-" + p['ref_pulse']

        return pulses_built

    def set_end_after_all_pulses(self, **block_end):
        if len(self.pulses):
            for i, p in enumerate(self.pulses):
                p['name'] = p.get('name', f"pulse_{i}")
            self.block_end.update({
                'ref_function': 'max',
                'ref_pulse': [p['name'] for p in self.pulses],
                'ref_point': 'end',
            })
        self.block_end.update(block_end)

    def _is_shell(self, pulse, block_start, block_end):
        """
        Checks, based on the pulse name, whether a pulse belongs to the block shell.
        That is, if the pulse name is the same as the name of the block start or end.
        A simple equivalence p == block_start or p == p_end does not work as pulse
        could be a deepcopy of block_start, which would return False in the above
        expressions.
        Args:
            pulse (dict): pulse to check.
            block_start (dict): dictionary of the block start
            block_end (dict): dictionary of the block end

        Returns: whether pulse is a shell dictionary (bool)

        """
        return self._is_block_start(pulse, block_start) \
               or self._is_block_end(pulse, block_end)

    def _is_block_start(self, pulse, block_start):
        """
        Checks, based on the pulse name, whether a pulse belongs to the block shell.
        That is, if the pulse name is the same as the name of the block start or end.
        A simple equivalence p == block_start or p == p_end does not work as pulse
        could be a deepcopy of block_start, which would return False in the above
        expressions.
        Args:
            pulse (dict): pulse to check.
            block_start (dict): dictionary of the block start

        Returns: whether pulse is a the block start dictionary (bool)

        """
        return pulse.get('name', None) == block_start['name']

    def _is_block_end(self, pulse, block_end):
        """
        Checks, based on the pulse name, whether a pulse belongs to the block shell.
        That is, if the pulse name is the same as the name of the block start or end.
        A simple equivalence p == block_start or p == p_end does not work as pulse
        could be a deepcopy of block_start, which would return False in the above
        expressions.
        Args:
            pulse (dict): pulse to check.
            block_end (dict): dictionary of the block end

        Returns: whether pulse is a the block end dictionary (bool)

        """
        return pulse.get('name', None) == block_end['name']

    def extend(self, additional_pulses):
        self.pulses.extend(additional_pulses)

    def __add__(self, other_block):
        return Block(f"{self.name}_{other_block.name}",
                     self.pulses + other_block.pulses)

    def __len__(self):
        return len(self.pulses)

    def __repr__(self):
        string_repr = f"---- {self.name} ----\n"
        for i, p in enumerate(self.pulses):
            string_repr += f"{i}: " + repr(p) + "\n"
        return string_repr

    def pulses_sweepcopy(self, sweep_dicts_list, index_list):
        """
        Returns a deepcopy of the pulse list where, based on the provided
        sweep_dicts_list and index_list, ParametricValue() objects
        in all pulses are resolved and further pulse modifiers are applied.

        :param sweep_dicts_list: see description of build()
        :param sweep_index_list: see description of build()

        :return:
        """
        if isinstance(index_list, int):
            index_list = [index_list]
        if isinstance(sweep_dicts_list, dict):
            sweep_dicts_list = [sweep_dicts_list]
        pulses = deepcopy(self.pulses)
        # resolve parametric values first
        for p in pulses:
            for attr, s in p.items():
                if getattr(s, '_is_parametric_value', False):
                    for sweep_dict, ind in zip(sweep_dicts_list, index_list):
                        if s.param in sweep_dict:
                            p[attr], p['op_code'] = s.resolve(
                                sweep_dict, ind, p['op_code'])

        # resolve pulse modifiers now (they could overwrite parametric values)
        def check_candidate(k, v, p):
            attr = p.get(k, '')
            if k == 'op_code':
                return (attr + ' ').startswith(v + ' ')
            elif k in ['name', 'ref_pulse']:
                # make sure to also find pulse renamed by Block.build()
                return (attr == v or attr.endswith("-|-" + v))
            else:
                return (attr == v)

        for sweep_dict, ind in zip(sweep_dicts_list, index_list):
            for param, d in sweep_dict.items():
                if isinstance(param, int):
                    param = f'occurrence={param}'
                if param == 'all':
                    modif = {}
                elif '=' not in param:
                    continue
                else:
                    modif = {l[0]: l[1] for l in [s.strip().split('=') for s
                                                  in param.split(',')]}
                attr = modif.pop('attr', None)
                occurrence = modif.pop('occurrence', None)
                n_occ = 0
                for p in pulses:
                    if all([check_candidate(k, v, p) for k, v in modif.items()]):
                        if occurrence is None or int(occurrence) == n_occ:
                            if attr is None:
                                p.update(d)
                            else:
                                p.update({attr: ParametricValue(
                                    param).resolve(sweep_dict, ind)})
                            if occurrence is not None:
                                break
                        else:
                            n_occ += 1

        return pulses

    def prefix_parametric_values(self, prefix, params=None):
        """
        Adds a prefix to the parameter name of ParametricValues in the pulses
        of the block.

        :param prefix: (str) prefix to be added
        :param params: (optional list of str) if given, prefix only these
            parameter names
        """
        for p in self.pulses:
            for k, s in p.items():
                if getattr(s, '_is_parametric_value', False):
                    if params is None or s.param in params:
                        s.param = prefix + s.param

    def parametric_values(self):
        return {(i, attr) : s for i, p in enumerate(self.pulses)
                for attr, s in p.items()
                if getattr(s, '_is_parametric_value', False)}

class ParametricValue:
    """
    A ParametricValue can be used as a placeholder for a pulse attribute that
    will be chosen based on a parameter provided later (e.g.,
    by Block.pulses_sweepcopy).

    :param param: a string specifying the name of the parameter.
    :param func: (optional) a function applied to the value of the parameter.
    :param op_split: (optional) cache a splitted version of the op_code of
        the pulse to allow for correct op_code resolution in cases of spaces
        in a mathematical expression in an op_code.

    """
    _is_parametric_value = True

    def __init__(self, param, func=None, op_split=None):
        self.param = param
        self.func = func
        self.op_split = op_split

    def resolve(self, sweep_dict, ind=None, op_code=None):
        """
        Returns the resolved value of a pulse attribute for a chosen sweep
        point.

        :param sweep_dict: an entry of the sweep_dicts_list described in
            build(). Alternatively, a dict of the form {param_n: val_n},
            in which case ind is ignored.
        :param ind: The index of the desired sweep point in sweep_dict.
            None is only allowed in the case where ind is ignored (see
            above).
        :param op_code: (optional str) the op_code of the pulse to allow
            resolving a parametric expression in the op_code

        :return: the resolved numerical value. If op_code was provided,
            the resovled op_code is returned in addition.
        """
        d = sweep_dict[self.param]
        if not isinstance(d, list) and not isinstance(d, dict) and not \
                isinstance(d, tuple):
            v = d
        elif isinstance(sweep_dict[self.param], dict) and 'values' in \
                sweep_dict[self.param]:  # convention in old sweep_dicts
            v = d['values'][ind]
        else: # convention in SweepPoints class
            v = d[0][ind]
        v_processed = v if self.func is None else self.func(v)
        if op_code is not None:
            if f'[{self.param}]' in op_code:
                # if the is a cached splitted version, use that one instead
                # in order to allow for correct op_code resolution in cases
                # of in a mathematical expression in an op_code.
                # FIXME: remove op_code caching as soon as a new op_code
                #  concept (e.g. tuples instead of space-separated strings)
                #  makes it obsolete
                op_split = [s for s in self.op_split] if self.op_split is not \
                            None else op_code.split(' ')
                param_start = op_split[0].find(':')
                v_code = eval(op_split[0][(param_start + 1):].replace(
                    f'[{self.param}]', f"{v}"))
                op_split[0] = f"{op_split[0][:param_start]}{v_code}"
                op_code = ' '.join(op_split)
            else:
                op_code = op_code.replace(f':{self.param} ', f"{v} ")
            return v_processed, op_code
        else:
            return v_processed
<commit_msg>ParametricValue: fixed a typo in a docstring<commit_after>import logging
import numpy as np
from copy import deepcopy

log = logging.getLogger(__name__)

class Block:
    """
    A block is a building block for a Quantum Algorithm Experiment.

            :param block_start_position: position of the block start. Defaults to
            before first pulse in list. Position should be changed in case the
            first pulse played is not the first one in the list.
        :param block_end_position: position of the block end. Defaults to after
            last pulse. If given, block_end_position MUST take into account the
            block_start i.e. the block start is added to the pulses before inserting
            the block end at its defined location.
    """
    counter = 0
    INSIDE_BLOCKINFO_NAME = "BlockInfo"

    def __init__(self, block_name, pulse_list:list, pulse_modifs=None,
                 **kw):
        self.name = block_name
        self.pulses = deepcopy(pulse_list)
        self.block_start = kw.get('block_start', {})
        self.block_end = kw.get('block_end', {})
        if pulse_modifs is not None:
            self.pulses = self.pulses_sweepcopy([pulse_modifs], [None])

    def build(self, ref_point="end", ref_point_new="start",
              ref_pulse='previous_pulse', block_delay=0, name=None,
              sweep_dicts_list=None, sweep_index_list=None,
               **kwargs):
        """
        Adds the block shell recursively through the pulse list.
        Returns the flattened pulse list
        :param ref_point: to which point of the previous pulse should this block
        be referenced to. Defaults to "end".
        :param ref_point_new: which point of this block is reference to the
            ref_point. Currently only supports "start".
        :param ref_pulse: to which pulse in the list is this block referenced to.
            Defaults to "previous_pulse" in list.
        :param block_delay: delay before the start of the block
        :param name: a custom name used to prefix the pulses. If None,
            the block name plus a counter is used.
        :param sweep_dicts_list: To build a block that corresponds to a
            point of an N-dimensional sweep, this param is a list of N
            sweep_dicts following the usual pycqed conventions (TODO: where
            to find those?)
            or a SweepPoints object with N dimenstions. Only used if also a
            sweep_index_list is provided. To have an effect, the block has
            to contain pulses with ParametricValues that refer to the
            parameters in the sweep_dicts.
            In addition, parameters of pulses can be modified by pulse modifier
            entries in the sweep dictionaries with the following format:

            'attr=X, key_1=val_1, ..., key_N=val_N'
            Searches all pulses p for which p[key_n] == val_n for all n,
            and sweeps the attribute attr of these pulses.
            If key_n is op_code, it suffices if the initial segment(s) of
            the op_code match (e.g., 'X180' will also match 'X180 qb1').
            If key_n is name or ref_pulse, it suffices if the original
            (ref)pulse name (before a potential change in a previous build())
            matches.

            'attr=X, key_1=val_1, ..., key_N=val_N, occurence=i'
            Sweep the attribute only for the ith pulse matching the
            criteria, where i is an integer (zero-indexed)

        :param sweep_index_list: A list of N indices for an N-dimensional
            sweep. Determines for which sweep points from sweep_dicts_list
            the block should be build. Only used if also a sweep_dicts_list
            is provided.

        :return:
        """
        if ref_point_new != "start":
            raise NotImplementedError("For now can only refer blocks to 'start'")
        if name is None:
            name = self.name + (f"_{self.counter}" if self.counter > 0 else "")
            self.counter += 1

        block_start = {"name": f"start",
                       "pulse_type": "VirtualPulse",
                       "pulse_delay": block_delay,
                       "ref_pulse": ref_pulse,
                       "ref_point": ref_point}
        block_start.update(kwargs.get("block_start", self.block_start))
        block_end = {"name": f"end",
                     "pulse_type": "VirtualPulse"}
        block_end.update(kwargs.get("block_end", self.block_end))
        if sweep_dicts_list is not None and sweep_index_list is not None:
            pulses_built = self.pulses_sweepcopy(sweep_dicts_list, sweep_index_list)
        else:
            pulses_built = deepcopy(self.pulses)

        # check if block_start/end  specified by user
        block_start_specified = False
        block_end_specified = False
        for p in pulses_built:
            if p.get("name", None) == "start":
                block_start = p #save reference
                block_start_specified = True
            elif p.get("name", None) == "end":
                block_end = p
                block_end_specified = True
        # add them if not specified
        if not block_start_specified:
            pulses_built = [block_start] + pulses_built
        if not block_end_specified:
            pulses_built = pulses_built + [block_end]

        for p in pulses_built:
            # if a dictionary wrapping a block is found, compile the inner block.
            if p.get("pulse_type", None) == self.INSIDE_BLOCKINFO_NAME:
                # p needs to have a block key
                assert 'block' in p, f"Inside block {p.get('name', 'Block')} " \
                    f"requires a key 'block' which refers to the uncompiled " \
                    f"block object."
                inside_block = p.pop('block')
                inside_block_pulses = inside_block.build(**p)
                # add all pulses of the inside block to the outer block
                pulses_built.extend(inside_block_pulses)

        # prepend block name to reference pulses and pulses names
        for p in pulses_built:
            # if the pulse has a name, prepend the blockname to it
            if p.get("name", None) is not None:
                p['name'] = name + "-|-" + p['name']

            ref_pulse = p.get("ref_pulse", "previous_pulse")
            p_is_block_start = self._is_block_start(p, block_start)

            # rename ref pulse within the block if not a special name
            escape_names = ("previous_pulse", "segment_start")
            if isinstance(ref_pulse, list):
                p['ref_pulse'] = [name + "-|-" + rp for rp in p['ref_pulse']]
            else:
                if ref_pulse not in escape_names and not p_is_block_start:
                    p['ref_pulse'] = name + "-|-" + p['ref_pulse']

        return pulses_built

    def set_end_after_all_pulses(self, **block_end):
        if len(self.pulses):
            for i, p in enumerate(self.pulses):
                p['name'] = p.get('name', f"pulse_{i}")
            self.block_end.update({
                'ref_function': 'max',
                'ref_pulse': [p['name'] for p in self.pulses],
                'ref_point': 'end',
            })
        self.block_end.update(block_end)

    def _is_shell(self, pulse, block_start, block_end):
        """
        Checks, based on the pulse name, whether a pulse belongs to the block shell.
        That is, if the pulse name is the same as the name of the block start or end.
        A simple equivalence p == block_start or p == p_end does not work as pulse
        could be a deepcopy of block_start, which would return False in the above
        expressions.
        Args:
            pulse (dict): pulse to check.
            block_start (dict): dictionary of the block start
            block_end (dict): dictionary of the block end

        Returns: whether pulse is a shell dictionary (bool)

        """
        return self._is_block_start(pulse, block_start) \
               or self._is_block_end(pulse, block_end)

    def _is_block_start(self, pulse, block_start):
        """
        Checks, based on the pulse name, whether a pulse belongs to the block shell.
        That is, if the pulse name is the same as the name of the block start or end.
        A simple equivalence p == block_start or p == p_end does not work as pulse
        could be a deepcopy of block_start, which would return False in the above
        expressions.
        Args:
            pulse (dict): pulse to check.
            block_start (dict): dictionary of the block start

        Returns: whether pulse is a the block start dictionary (bool)

        """
        return pulse.get('name', None) == block_start['name']

    def _is_block_end(self, pulse, block_end):
        """
        Checks, based on the pulse name, whether a pulse belongs to the block shell.
        That is, if the pulse name is the same as the name of the block start or end.
        A simple equivalence p == block_start or p == p_end does not work as pulse
        could be a deepcopy of block_start, which would return False in the above
        expressions.
        Args:
            pulse (dict): pulse to check.
            block_end (dict): dictionary of the block end

        Returns: whether pulse is a the block end dictionary (bool)

        """
        return pulse.get('name', None) == block_end['name']

    def extend(self, additional_pulses):
        self.pulses.extend(additional_pulses)

    def __add__(self, other_block):
        return Block(f"{self.name}_{other_block.name}",
                     self.pulses + other_block.pulses)

    def __len__(self):
        return len(self.pulses)

    def __repr__(self):
        string_repr = f"---- {self.name} ----\n"
        for i, p in enumerate(self.pulses):
            string_repr += f"{i}: " + repr(p) + "\n"
        return string_repr

    def pulses_sweepcopy(self, sweep_dicts_list, index_list):
        """
        Returns a deepcopy of the pulse list where, based on the provided
        sweep_dicts_list and index_list, ParametricValue() objects
        in all pulses are resolved and further pulse modifiers are applied.

        :param sweep_dicts_list: see description of build()
        :param sweep_index_list: see description of build()

        :return:
        """
        if isinstance(index_list, int):
            index_list = [index_list]
        if isinstance(sweep_dicts_list, dict):
            sweep_dicts_list = [sweep_dicts_list]
        pulses = deepcopy(self.pulses)
        # resolve parametric values first
        for p in pulses:
            for attr, s in p.items():
                if getattr(s, '_is_parametric_value', False):
                    for sweep_dict, ind in zip(sweep_dicts_list, index_list):
                        if s.param in sweep_dict:
                            p[attr], p['op_code'] = s.resolve(
                                sweep_dict, ind, p['op_code'])

        # resolve pulse modifiers now (they could overwrite parametric values)
        def check_candidate(k, v, p):
            attr = p.get(k, '')
            if k == 'op_code':
                return (attr + ' ').startswith(v + ' ')
            elif k in ['name', 'ref_pulse']:
                # make sure to also find pulse renamed by Block.build()
                return (attr == v or attr.endswith("-|-" + v))
            else:
                return (attr == v)

        for sweep_dict, ind in zip(sweep_dicts_list, index_list):
            for param, d in sweep_dict.items():
                if isinstance(param, int):
                    param = f'occurrence={param}'
                if param == 'all':
                    modif = {}
                elif '=' not in param:
                    continue
                else:
                    modif = {l[0]: l[1] for l in [s.strip().split('=') for s
                                                  in param.split(',')]}
                attr = modif.pop('attr', None)
                occurrence = modif.pop('occurrence', None)
                n_occ = 0
                for p in pulses:
                    if all([check_candidate(k, v, p) for k, v in modif.items()]):
                        if occurrence is None or int(occurrence) == n_occ:
                            if attr is None:
                                p.update(d)
                            else:
                                p.update({attr: ParametricValue(
                                    param).resolve(sweep_dict, ind)})
                            if occurrence is not None:
                                break
                        else:
                            n_occ += 1

        return pulses

    def prefix_parametric_values(self, prefix, params=None):
        """
        Adds a prefix to the parameter name of ParametricValues in the pulses
        of the block.

        :param prefix: (str) prefix to be added
        :param params: (optional list of str) if given, prefix only these
            parameter names
        """
        for p in self.pulses:
            for k, s in p.items():
                if getattr(s, '_is_parametric_value', False):
                    if params is None or s.param in params:
                        s.param = prefix + s.param

    def parametric_values(self):
        return {(i, attr) : s for i, p in enumerate(self.pulses)
                for attr, s in p.items()
                if getattr(s, '_is_parametric_value', False)}

class ParametricValue:
    """
    A ParametricValue can be used as a placeholder for a pulse attribute that
    will be chosen based on a parameter provided later (e.g.,
    by Block.pulses_sweepcopy).

    :param param: a string specifying the name of the parameter.
    :param func: (optional) a function applied to the value of the parameter.
    :param op_split: (optional) cache a splitted version of the op_code of
        the pulse to allow for correct op_code resolution in cases of spaces
        in a mathematical expression in an op_code.

    """
    _is_parametric_value = True

    def __init__(self, param, func=None, op_split=None):
        self.param = param
        self.func = func
        self.op_split = op_split

    def resolve(self, sweep_dict, ind=None, op_code=None):
        """
        Returns the resolved value of a pulse attribute for a chosen sweep
        point.

        :param sweep_dict: an entry of the sweep_dicts_list described in
            build(). Alternatively, a dict of the form {param_n: val_n},
            in which case ind is ignored.
        :param ind: The index of the desired sweep point in sweep_dict.
            None is only allowed in the case where ind is ignored (see
            above).
        :param op_code: (optional str) the op_code of the pulse to allow
            resolving a parametric expression in the op_code

        :return: the resolved numerical value. If op_code was provided,
            the resolved op_code is returned in addition.
        """
        d = sweep_dict[self.param]
        if not isinstance(d, list) and not isinstance(d, dict) and not \
                isinstance(d, tuple):
            v = d
        elif isinstance(sweep_dict[self.param], dict) and 'values' in \
                sweep_dict[self.param]:  # convention in old sweep_dicts
            v = d['values'][ind]
        else: # convention in SweepPoints class
            v = d[0][ind]
        v_processed = v if self.func is None else self.func(v)
        if op_code is not None:
            if f'[{self.param}]' in op_code:
                # if the is a cached splitted version, use that one instead
                # in order to allow for correct op_code resolution in cases
                # of in a mathematical expression in an op_code.
                # FIXME: remove op_code caching as soon as a new op_code
                #  concept (e.g. tuples instead of space-separated strings)
                #  makes it obsolete
                op_split = [s for s in self.op_split] if self.op_split is not \
                            None else op_code.split(' ')
                param_start = op_split[0].find(':')
                v_code = eval(op_split[0][(param_start + 1):].replace(
                    f'[{self.param}]', f"{v}"))
                op_split[0] = f"{op_split[0][:param_start]}{v_code}"
                op_code = ' '.join(op_split)
            else:
                op_code = op_code.replace(f':{self.param} ', f"{v} ")
            return v_processed, op_code
        else:
            return v_processed
<|endoftext|>


<commit_before>"""The plug in to run on batlab.

This module is available as an polyphemus plugin by the name ``polyphemus.swchook``.

SWC Hook API
============
"""
from __future__ import print_function
import os
import shutil
import sys
import subprocess

try:
    import simplejson as json
except ImportError:
    import json

import github3

from .utils import RunControl, NotSpecified, PersistentCache
from .plugins import Plugin
from .event import Event, runfor
from .githubbase import set_pull_request_status

if sys.version_info[0] >= 3:
    basestring = str

clone_template = """git clone {url} {dir}"""

checkout_template = """git checkout {commit}"""

rem_add_template = """git remote add {branch} {url}"""

fetch_template = """git fetch {branch}"""

merge_template = """git merge {branch}/{commit}"""

html_diff_template = """htmldiff {file1} {file2}"""

build_html = """make clean; make cache; make check;"""

def clone_repo(url, dir):
    subprocess.check_call(
        clone_template.format(url=url, dir=dir).split(),
        cwd=os.getcwd(), shell=(os.name == 'nt'))

def checkout_commit(commit, cwd=None):
    if cwd is None:
        cwd = os.getcwd()
    subprocess.check_call(
        checkout_template.format(commit=commit).split(),
        cwd=cwd, shell=(os.name == 'nt'))

def add_fetch_remote(rem_branch, rem_url, cwd=None):
    if cwd is None:
        cwd = os.getcwd()
    subprocess.check_call(
        rem_add_template.format(branch=rem_branch, url=rem_url).split(),
        cwd=cwd, shell=(os.name == 'nt'))
    subprocess.check_call(
        fetch_template.format(branch=rem_branch).split(),
        cwd=cwd, shell=(os.name == 'nt'))

def merge_commit(merge_branch, merge_ref, cwd=None):
    subprocess.check_call(
        merge_template.format(branch=merge_branch,
                              commit=merge_ref).split(),
        cwd=cwd, shell=(os.name == 'nt'))

class PolyphemusPlugin(Plugin):
    """This class provides functionality for comparing SWC website PRs.
    """

    requires = ('polyphemus.swcbase',)

    def __init__(self):
        self._files = []
        self._home_dir = os.path.abspath(os.getcwd())

    def _build_base_html(self, base):
        base_repo = github3.repository(*base.repo)

        if os.path.exists(self._base_dir):
            shutil.rmtree(self._base_dir)

        self._updater.update(
            status='pending', description="Getting base repository.")
        clone_repo(base_repo.clone_url, self._base_dir)
        checkout_commit(base.ref, cwd=self._base_dir)

        self._updater.update(
            status='pending', description="Building base website.")
        subprocess.check_call(build_html, cwd=self._base_dir, shell=True)

    def _build_head_html(self, base, head):
        head_repo = github3.repository(*head.repo)
        base_repo = github3.repository(*base.repo)

        if os.path.exists(self._head_dir):
            shutil.rmtree(self._head_dir)

        self._updater.update(
            status='pending', description="Getting head repository.")
        clone_repo(head_repo.clone_url, self._head_dir)
        add_fetch_remote("upstream", base_repo.clone_url,
                         cwd=self._head_dir)
        checkout_commit(base.ref, cwd=self._head_dir)
        merge_commit("origin", head.ref, cwd=self._head_dir)

        self._updater.update(
            status='pending', description="Building head website.")
        subprocess.check_call(build_html, shell=True, cwd=self._head_dir)

    def _generate_diffs(self):
        if os.path.exists(self._diff_dir):
            shutil.rmtree(self._diff_dir)

        self._updater.update(
            status='pending',
            description="Creating head and base website diffs.")

        for f in self._files:
            fpath, fname = os.path.split(f)
            d = os.path.join(self._diff_dir, fpath)
            os.makedirs(d)

            head = os.path.join(self._head_dir, f)
            base = os.path.join(self._base_dir, f)
            diff = os.path.join(self._diff_dir, f)

            diff_txt = subprocess.check_output(
                html_diff_template.format(file1=base,
                                          file2=head).split(),
                shell=(os.name == 'nt'))

            with open(diff, 'w') as f:
                f.write(diff_txt)

    def _dump_state(self):
        with open('swc_state.json', 'w') as outfile:
            json.dumps({'base': self._base_dir,
                        'head': self._head_dir,
                        'diff': self._diff_dir,
                        'files': self._files},
                       outfile, indent=4, separators=(',', ': '))

    def execute(self, rc):
        event_name = rc.event.name
        pr = rc.event.data  # pull request object

        rc.event = Event(name='swc-status',
                         data={'status': 'error',
                               'number': pr.number,
                               'description': ''})
        self._updater = rc.event.data

        if not pr.mergeable:
            msg = "Error, PR #{0} is not mergeable.".format(pr.number)
            warn(msg, RuntimeError)
            self._updater['description'] = msg
            return

        self._files = [os.path.join(*f.filename.split("/"))
                       for f in pr.iter_files()]

        self._base_dir = os.path.join(self._home_dir,
                                      str(pr.number), "base")
        self._head_dir = os.path.join(self._home_dir,
                                      str(pr.number), "head")
        self._diff_dir = os.path.join(self._home_dir,
                                      str(pr.number), "diff")

        self._build_head_html(pr.base, pr.head)
        self._build_base_html(pr.base)
        self._generate_diffs()
        self._dump_state()
<commit_msg>tries htmldiff and catches error if not install<commit_after>"""The plug in to run on batlab.

This module is available as an polyphemus plugin by the name ``polyphemus.swchook``.

SWC Hook API
============
"""
from __future__ import print_function
import os
import shutil
import sys
import subprocess

try:
    import simplejson as json
except ImportError:
    import json

import github3

from .utils import RunControl, NotSpecified, PersistentCache
from .plugins import Plugin
from .event import Event, runfor
from .githubbase import set_pull_request_status

if sys.version_info[0] >= 3:
    basestring = str

clone_template = """git clone {url} {dir}"""

checkout_template = """git checkout {commit}"""

rem_add_template = """git remote add {branch} {url}"""

fetch_template = """git fetch {branch}"""

merge_template = """git merge {branch}/{commit}"""

html_diff_template = """htmldiff {file1} {file2}"""

build_html = """make clean; make cache; make check;"""

def clone_repo(url, dir):
    subprocess.check_call(
        clone_template.format(url=url, dir=dir).split(),
        cwd=os.getcwd(), shell=(os.name == 'nt'))

def checkout_commit(commit, cwd=None):
    if cwd is None:
        cwd = os.getcwd()
    subprocess.check_call(
        checkout_template.format(commit=commit).split(),
        cwd=cwd, shell=(os.name == 'nt'))

def add_fetch_remote(rem_branch, rem_url, cwd=None):
    if cwd is None:
        cwd = os.getcwd()
    subprocess.check_call(
        rem_add_template.format(branch=rem_branch, url=rem_url).split(),
        cwd=cwd, shell=(os.name == 'nt'))
    subprocess.check_call(
        fetch_template.format(branch=rem_branch).split(),
        cwd=cwd, shell=(os.name == 'nt'))

def merge_commit(merge_branch, merge_ref, cwd=None):
    subprocess.check_call(
        merge_template.format(branch=merge_branch,
                              commit=merge_ref).split(),
        cwd=cwd, shell=(os.name == 'nt'))

class PolyphemusPlugin(Plugin):
    """This class provides functionality for comparing SWC website PRs.
    """

    requires = ('polyphemus.swcbase',)

    def __init__(self):
        self._files = []
        self._home_dir = os.path.abspath(os.getcwd())

    def _build_base_html(self, base):
        base_repo = github3.repository(*base.repo)

        if os.path.exists(self._base_dir):
            shutil.rmtree(self._base_dir)

        self._updater.update(
            status='pending', description="Getting base repository.")
        clone_repo(base_repo.clone_url, self._base_dir)
        checkout_commit(base.ref, cwd=self._base_dir)

        self._updater.update(
            status='pending', description="Building base website.")
        subprocess.check_call(build_html, cwd=self._base_dir, shell=True)

    def _build_head_html(self, base, head):
        head_repo = github3.repository(*head.repo)
        base_repo = github3.repository(*base.repo)

        if os.path.exists(self._head_dir):
            shutil.rmtree(self._head_dir)

        self._updater.update(
            status='pending', description="Getting head repository.")
        clone_repo(head_repo.clone_url, self._head_dir)
        add_fetch_remote("upstream", base_repo.clone_url,
                         cwd=self._head_dir)
        checkout_commit(base.ref, cwd=self._head_dir)
        merge_commit("origin", head.ref, cwd=self._head_dir)

        self._updater.update(
            status='pending', description="Building head website.")
        subprocess.check_call(build_html, shell=True, cwd=self._head_dir)

    def _generate_diffs(self):
        if os.path.exists(self._diff_dir):
            shutil.rmtree(self._diff_dir)

        self._updater.update(
            status='pending',
            description="Creating head and base website diffs.")

        for f in self._files:
            fpath, fname = os.path.split(f)
            d = os.path.join(self._diff_dir, fpath)
            os.makedirs(d)

            head = os.path.join(self._head_dir, f)
            base = os.path.join(self._base_dir, f)
            diff = os.path.join(self._diff_dir, f)

            try:
                diff_txt = subprocess.check_output(
                    html_diff_template.format(file1=base,
                                              file2=head).split(),
                    shell=(os.name == 'nt'))
            catch OSError:
                msg = "Error, htmldiff not installed on the server."
                warn(msg, RuntimeError)
                self._updater['description'] = msg
                return

            with open(diff, 'w') as f:
                f.write(diff_txt)

    def _dump_state(self):
        with open('swc_state.json', 'w') as outfile:
            json.dumps({'base': self._base_dir,
                        'head': self._head_dir,
                        'diff': self._diff_dir,
                        'files': self._files},
                       outfile, indent=4, separators=(',', ': '))

    def execute(self, rc):
        event_name = rc.event.name
        pr = rc.event.data  # pull request object

        rc.event = Event(name='swc-status',
                         data={'status': 'error',
                               'number': pr.number,
                               'description': ''})
        self._updater = rc.event.data

        if not pr.mergeable:
            msg = "Error, PR #{0} is not mergeable.".format(pr.number)
            warn(msg, RuntimeError)
            self._updater['description'] = msg
            return

        self._files = [os.path.join(*f.filename.split("/"))
                       for f in pr.iter_files()]

        self._base_dir = os.path.join(self._home_dir,
                                      str(pr.number), "base")
        self._head_dir = os.path.join(self._home_dir,
                                      str(pr.number), "head")
        self._diff_dir = os.path.join(self._home_dir,
                                      str(pr.number), "diff")

        self._build_head_html(pr.base, pr.head)
        self._build_base_html(pr.base)
        self._generate_diffs()
        self._dump_state()
<|endoftext|>


<commit_before>from rest_framework import serializers as ser

from modularodm.exceptions import ValidationValueError

from api.base.exceptions import InvalidModelValueError
from api.base.serializers import AllowMissing, JSONAPIRelationshipSerializer, HideIfDisabled
from website.models import User

from api.base.serializers import (
    JSONAPISerializer, LinksField, RelationshipField, DevOnly, IDField, TypeField
)
from api.base.utils import absolute_reverse

class UserSerializer(JSONAPISerializer):
    filterable_fields = frozenset([
        'full_name',
        'given_name',
        'middle_names',
        'family_name',
        'id'
    ])
    non_anonymized_fields = ['type']
    id = IDField(source='_id', read_only=True)
    type = TypeField()
    full_name = ser.CharField(source='fullname', required=True, label='Full name', help_text='Display name used in the general user interface')
    given_name = ser.CharField(required=False, allow_blank=True, help_text='For bibliographic citations')
    middle_names = ser.CharField(required=False, allow_blank=True, help_text='For bibliographic citations')
    family_name = ser.CharField(required=False, allow_blank=True, help_text='For bibliographic citations')
    suffix = HideIfDisabled(ser.CharField(required=False, allow_blank=True, help_text='For bibliographic citations'))
    date_registered = HideIfDisabled(ser.DateTimeField(read_only=True))
    active = HideIfDisabled(ser.BooleanField(read_only=True, source='is_active'))

    # Social Fields are broken out to get around DRF complex object bug and to make API updating more user friendly.
    github = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.github',
                                                          allow_blank=True, help_text='GitHub Handle'), required=False, source='social.github')))
    scholar = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.scholar',
                                                           allow_blank=True, help_text='Google Scholar Account'), required=False, source='social.scholar')))
    personal_website = DevOnly(HideIfDisabled(AllowMissing(ser.URLField(required=False, source='social.personal',
                                                                   allow_blank=True, help_text='Personal Website'), required=False, source='social.personal')))
    twitter = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.twitter',
                                                           allow_blank=True, help_text='Twitter Handle'), required=False, source='social.twitter')))
    linkedin = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.linkedIn',
                                                            allow_blank=True, help_text='LinkedIn Account'), required=False, source='social.linkedIn')))
    impactstory = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.impactStory',
                                                               allow_blank=True, help_text='ImpactStory Account'), required=False, source='social.impactStory')))
    orcid = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.orcid',
                                                         allow_blank=True, help_text='ORCID'), required=False, source='social.orcid')))
    researcherid = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.researcherId',
                                                      allow_blank=True, help_text='ResearcherId Account'), required=False, source='social.researcherId')))
    researchgate = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.researchGate',
                                                      allow_blank=True, help_text='ResearchGate Account'), required=False, source='social.researchGate')))
    academia_institution = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.academiaInstitution',
                                                      allow_blank=True, help_text='AcademiaInstitution Field'), required=False, source='social.academiaInstitution')))
    academia_profile_id = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.academiaProfileID',
                                                      allow_blank=True, help_text='AcademiaProfileID Field'), required=False, source='social.academiaProfileID')))
    baiduscholar = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.baiduScholar',
                                                           allow_blank=True, help_text='Baidu Scholar Account'), required=False, source='social.baiduScholar')))
    timezone = HideIfDisabled(ser.CharField(required=False, help_text="User's timezone, e.g. 'Etc/UTC"))
    locale = HideIfDisabled(ser.CharField(required=False, help_text="User's locale, e.g.  'en_US'"))

    links = HideIfDisabled(LinksField(
        {
            'html': 'absolute_url',
            'profile_image': 'profile_image_url',
        }
    ))

    nodes = HideIfDisabled(RelationshipField(
        related_view='users:user-nodes',
        related_view_kwargs={'user_id': '<pk>'},
    ))

    registrations = DevOnly(HideIfDisabled(RelationshipField(
        related_view='users:user-registrations',
        related_view_kwargs={'user_id': '<pk>'},
    )))

    institutions = HideIfDisabled(RelationshipField(
        related_view='users:user-institutions',
        related_view_kwargs={'user_id': '<pk>'},
        self_view='users:user-institutions-relationship',
        self_view_kwargs={'user_id': '<pk>'},
    ))

    class Meta:
        type_ = 'users'

    def absolute_url(self, obj):
        if obj is not None:
            return obj.absolute_url
        return None

    def get_absolute_url(self, obj):
        return absolute_reverse('users:user-detail', kwargs={'user_id': obj._id})

    def profile_image_url(self, user):
        size = self.context['request'].query_params.get('profile_image_size')
        return user.profile_image_url(size=size)

    def update(self, instance, validated_data):
        assert isinstance(instance, User), 'instance must be a User'
        for attr, value in validated_data.items():
            if 'social' == attr:
                for key, val in value.items():
                    instance.social[key] = val
            else:
                setattr(instance, attr, value)
        try:
            instance.save()
        except ValidationValueError as e:
            raise InvalidModelValueError(detail=e.message)
        return instance

class UserAddonSettingsSerializer(JSONAPISerializer):
    """
    Overrides UserSerializer to make id required.
    """
    id = ser.CharField(source='config.short_name', read_only=True)
    user_has_auth = ser.BooleanField(source='has_auth', read_only=True)

    links = LinksField({
        'self': 'get_absolute_url',
        'accounts': 'account_links'
    })

    class Meta:
        type_ = 'user_addons'

    def get_absolute_url(self, obj):
        user_id = self.context['request'].parser_context['kwargs']['user_id']
        return absolute_reverse(
            'users:user-addon-detail',
            kwargs={
                'user_id': user_id,
                'provider': obj.config.short_name
            }
        )

    def account_links(self, obj):
        # TODO: [OSF-4933] remove this after refactoring Figshare
        if hasattr(obj, 'external_accounts'):
            return {
                account._id: {
                    'account': absolute_reverse('users:user-external_account-detail', kwargs={'user_id': obj.owner._id, 'provider': obj.config.short_name, 'account_id': account._id}),
                    'nodes_connected': [n.absolute_api_v2_url for n in obj.get_attached_nodes(account)]
                }
                for account in obj.external_accounts
            }
        return {}

class UserDetailSerializer(UserSerializer):
    """
    Overrides UserSerializer to make id required.
    """
    id = IDField(source='_id', required=True)

class RelatedInstitution(JSONAPIRelationshipSerializer):
    id = ser.CharField(required=False, allow_null=True, source='_id')
    class Meta:
        type_ = 'institutions'

    def get_absolute_url(self, obj):
        return obj.absolute_api_v2_url

class UserInstitutionsRelationshipSerializer(ser.Serializer):

    data = ser.ListField(child=RelatedInstitution())
    links = LinksField({'self': 'get_self_url',
                        'html': 'get_related_url'})

    def get_self_url(self, obj):
        return absolute_reverse('users:user-institutions-relationship', kwargs={'user_id': obj['self']._id})

    def get_related_url(self, obj):
        return absolute_reverse('users:user-institutions', kwargs={'user_id': obj['self']._id})

    def get_absolute_url(self, obj):
        return obj.absolute_api_v2_url

    class Meta:
        type_ = 'institutions'
<commit_msg>Add UserCreateSerializer<commit_after>from rest_framework import serializers as ser

from modularodm.exceptions import ValidationValueError

from api.base.exceptions import InvalidModelValueError, JSONAPIException, Conflict
from api.base.serializers import AllowMissing, JSONAPIRelationshipSerializer, HideIfDisabled
from website.models import User

from api.base.serializers import (
    JSONAPISerializer, LinksField, RelationshipField, DevOnly, IDField, TypeField
)
from api.base.utils import absolute_reverse, has_admin_scope

from framework.auth.views import send_confirm_email

class UserSerializer(JSONAPISerializer):
    filterable_fields = frozenset([
        'full_name',
        'given_name',
        'middle_names',
        'family_name',
        'id'
    ])
    non_anonymized_fields = ['type']
    id = IDField(source='_id', read_only=True)
    type = TypeField()
    full_name = ser.CharField(source='fullname', required=True, label='Full name', help_text='Display name used in the general user interface')
    given_name = ser.CharField(required=False, allow_blank=True, help_text='For bibliographic citations')
    middle_names = ser.CharField(required=False, allow_blank=True, help_text='For bibliographic citations')
    family_name = ser.CharField(required=False, allow_blank=True, help_text='For bibliographic citations')
    suffix = HideIfDisabled(ser.CharField(required=False, allow_blank=True, help_text='For bibliographic citations'))
    date_registered = HideIfDisabled(ser.DateTimeField(read_only=True))
    active = HideIfDisabled(ser.BooleanField(read_only=True, source='is_active'))

    # Social Fields are broken out to get around DRF complex object bug and to make API updating more user friendly.
    github = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.github',
                                                          allow_blank=True, help_text='GitHub Handle'), required=False, source='social.github')))
    scholar = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.scholar',
                                                           allow_blank=True, help_text='Google Scholar Account'), required=False, source='social.scholar')))
    personal_website = DevOnly(HideIfDisabled(AllowMissing(ser.URLField(required=False, source='social.personal',
                                                                   allow_blank=True, help_text='Personal Website'), required=False, source='social.personal')))
    twitter = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.twitter',
                                                           allow_blank=True, help_text='Twitter Handle'), required=False, source='social.twitter')))
    linkedin = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.linkedIn',
                                                            allow_blank=True, help_text='LinkedIn Account'), required=False, source='social.linkedIn')))
    impactstory = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.impactStory',
                                                               allow_blank=True, help_text='ImpactStory Account'), required=False, source='social.impactStory')))
    orcid = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.orcid',
                                                         allow_blank=True, help_text='ORCID'), required=False, source='social.orcid')))
    researcherid = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.researcherId',
                                                      allow_blank=True, help_text='ResearcherId Account'), required=False, source='social.researcherId')))
    researchgate = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.researchGate',
                                                      allow_blank=True, help_text='ResearchGate Account'), required=False, source='social.researchGate')))
    academia_institution = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.academiaInstitution',
                                                      allow_blank=True, help_text='AcademiaInstitution Field'), required=False, source='social.academiaInstitution')))
    academia_profile_id = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.academiaProfileID',
                                                      allow_blank=True, help_text='AcademiaProfileID Field'), required=False, source='social.academiaProfileID')))
    baiduscholar = DevOnly(HideIfDisabled(AllowMissing(ser.CharField(required=False, source='social.baiduScholar',
                                                           allow_blank=True, help_text='Baidu Scholar Account'), required=False, source='social.baiduScholar')))
    timezone = HideIfDisabled(ser.CharField(required=False, help_text="User's timezone, e.g. 'Etc/UTC"))
    locale = HideIfDisabled(ser.CharField(required=False, help_text="User's locale, e.g.  'en_US'"))

    links = HideIfDisabled(LinksField(
        {
            'html': 'absolute_url',
            'profile_image': 'profile_image_url',
        }
    ))

    nodes = HideIfDisabled(RelationshipField(
        related_view='users:user-nodes',
        related_view_kwargs={'user_id': '<pk>'},
    ))

    registrations = DevOnly(HideIfDisabled(RelationshipField(
        related_view='users:user-registrations',
        related_view_kwargs={'user_id': '<pk>'},
    )))

    institutions = HideIfDisabled(RelationshipField(
        related_view='users:user-institutions',
        related_view_kwargs={'user_id': '<pk>'},
        self_view='users:user-institutions-relationship',
        self_view_kwargs={'user_id': '<pk>'},
    ))

    class Meta:
        type_ = 'users'

    def absolute_url(self, obj):
        if obj is not None:
            return obj.absolute_url
        return None

    def get_absolute_url(self, obj):
        return absolute_reverse('users:user-detail', kwargs={'user_id': obj._id})

    def profile_image_url(self, user):
        size = self.context['request'].query_params.get('profile_image_size')
        return user.profile_image_url(size=size)

    def update(self, instance, validated_data):
        assert isinstance(instance, User), 'instance must be a User'
        for attr, value in validated_data.items():
            if 'social' == attr:
                for key, val in value.items():
                    instance.social[key] = val
            else:
                setattr(instance, attr, value)
        try:
            instance.save()
        except ValidationValueError as e:
            raise InvalidModelValueError(detail=e.message)
        return instance

class UserCreateSerializer(UserSerializer):
    username = ser.EmailField(required=True)

    def create(self, validated_data):
        username = validated_data.get('username')
        full_name = validated_data.get('fullname')
        if not username and full_name:
            raise JSONAPIException('Both a `username` and `full_name` are required to create a user.')
        user = User.create_unregistered(full_name, email=username)
        user.registered_by = self.context['request'].user
        user.add_unconfirmed_email(user.username)
        try:
            user.save()
        except ValidationValueError:
            raise Conflict('User with specified username already exists.')

        if self.context['request'].GET.get('send_email', False) and has_admin_scope(self.context['request']):
            send_confirm_email(user, user.username)

        return user

class UserAddonSettingsSerializer(JSONAPISerializer):
    """
    Overrides UserSerializer to make id required.
    """
    id = ser.CharField(source='config.short_name', read_only=True)
    user_has_auth = ser.BooleanField(source='has_auth', read_only=True)

    links = LinksField({
        'self': 'get_absolute_url',
        'accounts': 'account_links'
    })

    class Meta:
        type_ = 'user_addons'

    def get_absolute_url(self, obj):
        user_id = self.context['request'].parser_context['kwargs']['user_id']
        return absolute_reverse(
            'users:user-addon-detail',
            kwargs={
                'user_id': user_id,
                'provider': obj.config.short_name
            }
        )

    def account_links(self, obj):
        # TODO: [OSF-4933] remove this after refactoring Figshare
        if hasattr(obj, 'external_accounts'):
            return {
                account._id: {
                    'account': absolute_reverse('users:user-external_account-detail', kwargs={'user_id': obj.owner._id, 'provider': obj.config.short_name, 'account_id': account._id}),
                    'nodes_connected': [n.absolute_api_v2_url for n in obj.get_attached_nodes(account)]
                }
                for account in obj.external_accounts
            }
        return {}

class UserDetailSerializer(UserSerializer):
    """
    Overrides UserSerializer to make id required.
    """
    id = IDField(source='_id', required=True)

class RelatedInstitution(JSONAPIRelationshipSerializer):
    id = ser.CharField(required=False, allow_null=True, source='_id')
    class Meta:
        type_ = 'institutions'

    def get_absolute_url(self, obj):
        return obj.absolute_api_v2_url

class UserInstitutionsRelationshipSerializer(ser.Serializer):

    data = ser.ListField(child=RelatedInstitution())
    links = LinksField({'self': 'get_self_url',
                        'html': 'get_related_url'})

    def get_self_url(self, obj):
        return absolute_reverse('users:user-institutions-relationship', kwargs={'user_id': obj['self']._id})

    def get_related_url(self, obj):
        return absolute_reverse('users:user-institutions', kwargs={'user_id': obj['self']._id})

    def get_absolute_url(self, obj):
        return obj.absolute_api_v2_url

    class Meta:
        type_ = 'institutions'
<|endoftext|>


<commit_before>#!/usr/bin/env python3
'''
compendium_to_card - convert D&D compendium XML files into JSON for rpgcard

Reference:

Keys & Sub-keys in compendium:

item
====
{'modifier', 'property', 'weight', 'stealth', 'text', 'range', 'name', 'strength',
 'type', 'ac', 'roll', 'dmg2', 'dmg1', 'dmgType'}

race
====
{'size', 'name', 'ability', 'trait', 'speed', 'proficiency'}

background
==========
{'name', 'trait', 'proficiency'}

feat
====
{'text', 'prerequisite', 'name', 'modifier'}

class
=====
{'hd', 'name', 'spellAbility', 'autolevel', 'proficiency', 'SpellAbility'}

spell
=====
{'school', 'time', 'duration', 'classes', 'text', 'name', 'components', 'ritual',
 'roll', 'level', 'range'}

monster
=======
{'languages', 'vulnerable', 'immune', 'description', 'cha', 'skill', 'wis', 'save',
 'spells', 'action', 'cr', 'name', 'dex', 'type', 'senses', 'int', 'passive', 'str',
 'hp', 'reaction', 'alignment', 'resist', 'speed', 'legendary', 'conditionImmune',
 'size', 'con', 'trait', 'ac'}

'''
# pylint: disable=superfluous-parens

import argparse
import json
import re
import sys
from string import capwords
import xmltodict

CATEGORY_MAP = {}
#     'item': Item,
#     'race': Race,
#     'background': Background,
#     'feat': Feat,
#     'class': Class,
#     'spell': Spell,
#     'monster': Monster,
# }

class SimpleDescriptor(object):
    '''
    TODO

    '''
    # pylint: disable=too-few-public-methods
    def __init__(self, key_name, default=None):
        self.key_name = key_name
        self.default = default

    def __get__(self, instance, dummy_cls):
        value = instance.data.get(self.key_name, self.default)

        return value

    def __set__(self, dummy_instance, dummy_value):
        print('Updating compendium values is not allowed')

    def __delete__(self, dummy_instance):
        print('Deleting compendium values is not allowed')

class LookupDescriptor(object):
    '''
    TODO

    '''
    # pylint: disable=too-few-public-methods
    def __init__(self, key_name, lookup_dict):
        self.key_name = key_name
        self.lookup = lookup_dict

    def __get__(self, instance, dummy_cls):
        code = instance.data.get(self.key_name)
        if code is not None:
            try:
                value = self.lookup[code]
            except KeyError:
                print(sys.stderr, "Unrecognized %s code: '%s'" % (self.key_name, code))
                value = value

            return value

    def __set__(self, dummy_instance, dummy_value):
        print('Updating compendium values is not allowed')

    def __delete__(self, dummy_instance):
        print('Deleting compendium values is not allowed')

class LookupListDescriptor(object):
    '''
    TODO

    '''
    # pylint: disable=too-few-public-methods
    def __init__(self, key_name, lookup_dict):
        self.key_name = key_name
        self.lookup = lookup_dict

    def __get__(self, instance, dummy_cls):
        result = []
        codes = instance.data.get(self.key_name)
        if codes is not None:
            codes = codes.split(',')
            for code in codes:
                try:
                    result.append(self.lookup[code])
                except KeyError:
                    print(sys.stderr, "Unrecognized %s code: '%s'" % (self.key_name, code))
                    result.append(code)

        return result

    def __set__(self, dummy_instance, dummy_value):
        print('Updating compendium values is not allowed')

    def __delete__(self, dummy_instance):
        print('Deleting compendium values is not allowed')

class CompendiumMeta(type):
    def __init__(cls, name, bases, cdict):
        super(CompendiumMeta, cls).__init__(name, bases, cdict)
        if object not in bases:
            # We only want to add the CompendiumCategory subclasses, not
            # CompendiumCategory itself
            CATEGORY_MAP[name.lower()] = dict(cls=cls)

class CompendiumCategory(object, metaclass=CompendiumMeta):
    '''
    TODO

    '''
    name = SimpleDescriptor('name')
    text = None

    def __init__(self, item_dict):
        self.data = item_dict

    def __str__(self):
        return '{}: {}'.format(self.__class__.__name__, self.name)

    @property
    def source(self):
        '''
        TODO

        '''
        # pylint: disable=not-an-iterable
        source = None
        if self.text is not None:
            for line in self.text:
                if line is not None:
                    if 'Source:' in line:
                        source = line.replace('Source:', '').strip()

        return source

    @property
    def sourcebook(self):
        '''
        TODO

        '''
        book = None
        if self.source is not None:
            book = self.source.split(',')[0]

        return book

    def to_dict(self):
        '''
        TODO

        '''
        print('Not yet implemented', file=sys.stderr)

class Race(CompendiumCategory):
    pass

class Monster(CompendiumCategory):
    pass

class Spell(CompendiumCategory):
    pass

class Class(CompendiumCategory):
    pass

class Background(CompendiumCategory):
    pass

class Feat(CompendiumCategory):
    pass

class Item(CompendiumCategory):
    '''
        Item Keys & Sub-keys in compendium:
        {
            'modifier',
            'property',
            'weight',
            'stealth',
            'text',
            'range',
            'name',
            'strength',
            'type',
            'ac',
            'roll',
            'dmg2',
            'dmg1',
            'dmgType'
        }

    '''
    item_codes = dict(
        HA='Heavy Armor',
        MA='Medium Armor',
        LA='Light Armor',
        S='Shield',
        M='Melee weapon',
        R='Ranged weapon',
        A='Ammunition',
        RD='Rod',
        ST='Staff',
        WD='Wand',
        RG='Ring',
        P='Potion',
        SC='Scroll',
        W='Wondrous item',
        G='Adventuring gear',
    )
    # Handle special code names that break dict() constructors
    item_codes['$'] = 'Money'

    property_codes = dict(
        V="Versatile",
        L="Light",
        T="Thrown",
        A="Ammunition",
        F="Finesse",
        H="Heavy",
        LD="Loading",
        R="Reach",
        S="Special",
    )
    # Handle special code names that break dict() constructors
    property_codes['2H'] = "Two-handed"

    damage_codes = dict(
        S="Slashing",
        B="Bludgeoning",
        P="Piercing",
    )

    weight = SimpleDescriptor('weight')
    text = SimpleDescriptor('text')
    range = SimpleDescriptor('range')
    strength = SimpleDescriptor('strength')
    ac = SimpleDescriptor('ac')     # pylint: disable=invalid-name
    roll = SimpleDescriptor('roll')
    dmg1 = SimpleDescriptor('dmg1')
    dmg2 = SimpleDescriptor('dmg2')
    dmgType = LookupDescriptor('dmgType', damage_codes)
    type = LookupDescriptor('type', item_codes)
    properties = LookupListDescriptor('property', property_codes)

    @property
    def modifier(self):
        '''
        Here we always ensure that an item's modifier returns a list

        '''
        _modifier = self.data.get('modifier', [])
        if isinstance(_modifier, dict):
            _modifier = [_modifier]

        return _modifier

    @property
    def stealth(self):
        '''
        Indicates whether an item triggers disadvantage on stealth rolls

        '''
        value = self.data.get('stealth', 'NO')

        return value.lower() == 'yes'

    @property
    def dmg(self):
        '''
        TODO

        '''
        damage = [dmg for dmg in [self.dmg1, self.dmg2] if dmg is not None]

        return ", ".join(damage)

    def to_dict(self):
        rpgdict = dict()
        contents = [
            "Type | {}".format(self.type),
        ]
        tags = []

        if self.type is not None:
            tags.append(self.type.lower())

        if self.sourcebook is not None:
            tags.append(self.sourcebook.lower())

        if self.properties:
            tags.extend([x.lower() for x in self.properties])
            contents.append('Property | {}'.format(', '.join(self.properties)))

        for mod in self.modifier:
            contents.append(capwords("{m[@category]} | {m[#text]}".format(m=mod)))

        if self.ac is not None:
            contents.append('AC | {}'.format(self.ac))

        if self.dmg1 is not None:
            contents.append('Damage 1H | {}'.format(self.dmg1))

        if self.dmg2 is not None:
            contents.append('Damage 2H | {}'.format(self.dmg2))

        if self.dmgType is not None:
            contents.append('DmgType | {}'.format(self.dmgType))

        if self.range is not None:
            contents.append('Range | {}'.format(self.range))

        if self.stealth:
            contents.append('Stealth | Disadvantage')

        if self.strength is not None:
            contents.append('Strength | {}'.format(self.strength))

        contents = ["property | {}".format(x) for x in contents]
        contents.append('text | {}'.format(self.text))

        rpgdict.update(
            title=self.name,
            contents=contents,
            tags=tags,
        )
        return rpgdict

class CategoryDescriptor(object):
    '''
    TODO

    '''
    # pylint: disable=too-few-public-methods
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, dummy_cls):
        category = instance.contents.get(self.name, [])
        category_class = CATEGORY_MAP[self.name].get('cls', CompendiumCategory)
        if isinstance(category, list):
            for comp_def in category:
                yield category_class(comp_def)
        else:
            yield category_class(category)

    def __set__(self, dummy_instance, dummy_value):
        print('Updating compendium categories is not allowed')

    def __delete__(self, dummy_instance):
        print('Deleting compendium categories is not allowed')

class Compendium(object):
    '''
    TODO

    '''
    compatible_versions = ('5',)

    def __init__(self, xml_file):
        self._xml_file = xml_file
        self._contents = None

    def parse_xml_compendium(self):
        '''
        TODO

        '''
        with open(self._xml_file) as infile:
            return xmltodict.parse(infile.read())['compendium']

    @property
    def contents(self):
        '''
        TODO

        '''
        if self._contents is None:
            self._contents = self.parse_xml_compendium()
        return self._contents

    @property
    def version(self):
        '''
        TODO

        '''
        return self.contents.get('@version')

    def carddump(self, categories=None):
        '''
        TODO

        '''
        if categories is None:
            categories = list()

        dump = []
        for obj_list in self._dump(categories):
            for obj in obj_list:
                obj_dict = obj.to_dict()
                if obj_dict is not None:
                    dump.append(obj_dict)

        #     obj.to_dict() for obj_list in self._dump(categories)
        #     for obj in obj_list if obj is not None
        # ]

        return dump

    def _dump(self, categories):
        '''
        TODO

        '''
        if not categories:
            # default to all known categories
            categories = CATEGORY_MAP.keys()

        for category in categories:
            attr_name = CATEGORY_MAP[category]['property']
            if hasattr(self, attr_name):
                attr = getattr(self, attr_name)
                yield [x for x in attr]

    def search_names(self, name):
        '''
        TODO

        '''
        if name == name.lower():
            regex = re.compile('.*%s.*' % name, re.I)
        else:
            regex = re.compile('.*%s.*' % name)
        for item in self.items:
            result = regex.search(item.name)
            if result is not None:
                yield item

    # Create properties for compendium categories and update the map so we
    # can cross reference a category to a Compendium property
    items = CategoryDescriptor('item')
    CATEGORY_MAP['item']['property'] = 'items'
    races = CategoryDescriptor('race')
    CATEGORY_MAP['race']['property'] = 'races'
    backgrounds = CategoryDescriptor('background')
    CATEGORY_MAP['background']['property'] = 'backgrounds'
    feats = CategoryDescriptor('feat')
    CATEGORY_MAP['feat']['property'] = 'feats'
    classes = CategoryDescriptor('class')
    CATEGORY_MAP['class']['property'] = 'classes'
    spells = CategoryDescriptor('spell')
    CATEGORY_MAP['spell']['property'] = 'spells'
    monsters = CategoryDescriptor('monster')
    CATEGORY_MAP['monster']['property'] = 'monsters'

def parse_args():
    '''
    TODO

    '''
    parser = argparse.ArgumentParser(
        description=(
            "Convert Roll20 compendium XML documents into JSON objects "
            "compatible with rpg-card.\n"
            "See:\n"
            "\thttps://github.com/ceryliae/DnDAppFiles\n"
            "\thttps://github.com/crobi/rpg-cards"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "input_file", type=str,
        help="Roll20 Compendium XML file to read"
    )
    parser.add_argument(
        "output_file", type=str, default=None, nargs='?',
        help="rpg-card JSON file to write; or STDOUT if omitted"
    )
    parser.add_argument(
        '-c', dest='categories', action='append', default=[],
        choices=CATEGORY_MAP.keys(),
        help=(
            "Chose the compendium categories you want to convert, default is "
            "everything. Repeat this option for multiple selections."
        )
    )

    return parser.parse_args()

def main():
    '''
    TODO

    '''
    args = parse_args()

    compendium = Compendium(args.input_file)

    if compendium.version not in compendium.compatible_versions:
        print(
            "%s is not compatible with version %s compendiums!" %
            (__file__, compendium.version)
        )
        return 1

    rpgcard_data = json.dumps(
        compendium.carddump(args.categories),
        indent=4,
    )

    if args.output_file is None:
        print(rpgcard_data)
    else:
        with open(args.output_file, 'w') as outfile:
            outfile.write(rpgcard_data)

    return 0

if __name__ == '__main__':
    sys.exit(main())
<commit_msg>Completed some docstrings<commit_after>#!/usr/bin/env python3
'''
compendium_to_card - convert D&D compendium XML files into JSON for rpgcard

Reference:

Keys & Sub-keys in compendium:

item
====
{'modifier', 'property', 'weight', 'stealth', 'text', 'range', 'name', 'strength',
 'type', 'ac', 'roll', 'dmg2', 'dmg1', 'dmgType'}

race
====
{'size', 'name', 'ability', 'trait', 'speed', 'proficiency'}

background
==========
{'name', 'trait', 'proficiency'}

feat
====
{'text', 'prerequisite', 'name', 'modifier'}

class
=====
{'hd', 'name', 'spellAbility', 'autolevel', 'proficiency', 'SpellAbility'}

spell
=====
{'school', 'time', 'duration', 'classes', 'text', 'name', 'components', 'ritual',
 'roll', 'level', 'range'}

monster
=======
{'languages', 'vulnerable', 'immune', 'description', 'cha', 'skill', 'wis', 'save',
 'spells', 'action', 'cr', 'name', 'dex', 'type', 'senses', 'int', 'passive', 'str',
 'hp', 'reaction', 'alignment', 'resist', 'speed', 'legendary', 'conditionImmune',
 'size', 'con', 'trait', 'ac'}

'''
# pylint: disable=superfluous-parens

import argparse
import json
import re
import sys
from string import capwords
import xmltodict

CATEGORY_MAP = {}
#     'item': Item,
#     'race': Race,
#     'background': Background,
#     'feat': Feat,
#     'class': Class,
#     'spell': Spell,
#     'monster': Monster,
# }

class SimpleDescriptor(object):
    '''
    A simple descriptor used for creating class properties that pull a key
    value from an instance dictionary named 'data'.

    :param str key_name: Key of the value you want to return
    :param default: A value to return if key_name is not found in data
    :type default: int, str, bool, float, etc.

    '''
    # pylint: disable=too-few-public-methods
    def __init__(self, key_name, default=None):
        self.key_name = key_name
        self.default = default

    def __get__(self, instance, dummy_cls):
        value = instance.data.get(self.key_name, self.default)

        return value

    def __set__(self, dummy_instance, dummy_value):
        print('Updating compendium values is not allowed')

    def __delete__(self, dummy_instance):
        print('Deleting compendium values is not allowed')

class LookupDescriptor(object):
    '''
    Very similiar to SimpleDescriptor in operation, with the exception that
    this descriptor doesn't return the key's value directly.  Instead, it uses
    value as a key into another dict, and returns that instead.  If the first
    value is not found in lookup_dict, then that is returned instead.

    :param str key_name: this key's value's value will be returned
    :param dict lookup_dict: key_name's value is used as a key into this

    '''
    # pylint: disable=too-few-public-methods
    def __init__(self, key_name, lookup_dict):
        self.key_name = key_name
        self.lookup = lookup_dict

    def __get__(self, instance, dummy_cls):
        code = instance.data.get(self.key_name)
        if code is not None:
            try:
                value = self.lookup[code]
            except KeyError:
                print(sys.stderr, "Unrecognized %s code: '%s'" % (self.key_name, code))
                value = value

            return value

    def __set__(self, dummy_instance, dummy_value):
        print('Updating compendium values is not allowed')

    def __delete__(self, dummy_instance):
        print('Deleting compendium values is not allowed')

class LookupListDescriptor(object):
    '''
    Nearly identical to LookupDescriptor.  Here's key_name's value is expected
    to map to a comma-separated sequence within lookup_dict.  Even if no
    sequence is found, we always return a list object.

    :param str key_name: key into instance's data dict
    :param dict lookup_dict: map of key_name's value to another value

    '''
    # pylint: disable=too-few-public-methods
    def __init__(self, key_name, lookup_dict):
        self.key_name = key_name
        self.lookup = lookup_dict

    def __get__(self, instance, dummy_cls):
        result = []
        codes = instance.data.get(self.key_name)
        if codes is not None:
            codes = codes.split(',')
            for code in codes:
                try:
                    result.append(self.lookup[code])
                except KeyError:
                    print(sys.stderr, "Unrecognized %s code: '%s'" % (self.key_name, code))
                    result.append(code)

        return result

    def __set__(self, dummy_instance, dummy_value):
        print('Updating compendium values is not allowed')

    def __delete__(self, dummy_instance):
        print('Deleting compendium values is not allowed')

class CompendiumMeta(type):
    '''
    This metaclass is used to populate CATEGORY_MAP with the
    CompendiumCategory classes and what category they map to.  The point of
    which is to eliminate manual creation and updates to said map to avoid
    inconsistencies.

    '''
    def __init__(cls, name, bases, cdict):
        super(CompendiumMeta, cls).__init__(name, bases, cdict)
        if object not in bases:
            # We only want to add the CompendiumCategory subclasses, not
            # CompendiumCategory itself
            CATEGORY_MAP[name.lower()] = dict(cls=cls)

class CompendiumCategory(object, metaclass=CompendiumMeta):
    '''
    This is the base class for all compendium categories.  The current list of
    categories is [item, feat, background, spell, class, monster, race]

    :param dict item_dict: The output straight from xmltodict[category]

    '''
    name = SimpleDescriptor('name')
    text = None

    def __init__(self, item_dict):
        self.data = item_dict

    def __str__(self):
        return '{}: {}'.format(self.__class__.__name__, self.name)

    @property
    def source(self):
        '''
        TODO

        '''
        # pylint: disable=not-an-iterable
        source = None
        if self.text is not None:
            for line in self.text:
                if line is not None:
                    if 'Source:' in line:
                        source = line.replace('Source:', '').strip()

        return source

    @property
    def sourcebook(self):
        '''
        TODO

        '''
        book = None
        if self.source is not None:
            book = self.source.split(',')[0]

        return book

    def to_dict(self):
        '''
        TODO

        '''
        print('Not yet implemented', file=sys.stderr)

class Race(CompendiumCategory):
    pass

class Monster(CompendiumCategory):
    pass

class Spell(CompendiumCategory):
    pass

class Class(CompendiumCategory):
    pass

class Background(CompendiumCategory):
    pass

class Feat(CompendiumCategory):
    pass

class Item(CompendiumCategory):
    '''
        Item Keys & Sub-keys in compendium:
        {
            'modifier',
            'property',
            'weight',
            'stealth',
            'text',
            'range',
            'name',
            'strength',
            'type',
            'ac',
            'roll',
            'dmg2',
            'dmg1',
            'dmgType'
        }

    '''
    item_codes = dict(
        HA='Heavy Armor',
        MA='Medium Armor',
        LA='Light Armor',
        S='Shield',
        M='Melee weapon',
        R='Ranged weapon',
        A='Ammunition',
        RD='Rod',
        ST='Staff',
        WD='Wand',
        RG='Ring',
        P='Potion',
        SC='Scroll',
        W='Wondrous item',
        G='Adventuring gear',
    )
    # Handle special code names that break dict() constructors
    item_codes['$'] = 'Money'

    property_codes = dict(
        V="Versatile",
        L="Light",
        T="Thrown",
        A="Ammunition",
        F="Finesse",
        H="Heavy",
        LD="Loading",
        R="Reach",
        S="Special",
    )
    # Handle special code names that break dict() constructors
    property_codes['2H'] = "Two-handed"

    damage_codes = dict(
        S="Slashing",
        B="Bludgeoning",
        P="Piercing",
    )

    weight = SimpleDescriptor('weight')
    text = SimpleDescriptor('text')
    range = SimpleDescriptor('range')
    strength = SimpleDescriptor('strength')
    ac = SimpleDescriptor('ac')     # pylint: disable=invalid-name
    roll = SimpleDescriptor('roll')
    dmg1 = SimpleDescriptor('dmg1')
    dmg2 = SimpleDescriptor('dmg2')
    dmgType = LookupDescriptor('dmgType', damage_codes)
    type = LookupDescriptor('type', item_codes)
    properties = LookupListDescriptor('property', property_codes)

    @property
    def modifier(self):
        '''
        Here we always ensure that an item's modifier returns a list

        '''
        _modifier = self.data.get('modifier', [])
        if isinstance(_modifier, dict):
            _modifier = [_modifier]

        return _modifier

    @property
    def stealth(self):
        '''
        Indicates whether an item triggers disadvantage on stealth rolls

        '''
        value = self.data.get('stealth', 'NO')

        return value.lower() == 'yes'

    @property
    def dmg(self):
        '''
        TODO

        '''
        damage = [dmg for dmg in [self.dmg1, self.dmg2] if dmg is not None]

        return ", ".join(damage)

    def to_dict(self):
        rpgdict = dict()
        contents = [
            "Type | {}".format(self.type),
        ]
        tags = []

        if self.type is not None:
            tags.append(self.type.lower())

        if self.sourcebook is not None:
            tags.append(self.sourcebook.lower())

        if self.properties:
            tags.extend([x.lower() for x in self.properties])
            contents.append('Property | {}'.format(', '.join(self.properties)))

        for mod in self.modifier:
            contents.append(capwords("{m[@category]} | {m[#text]}".format(m=mod)))

        if self.ac is not None:
            contents.append('AC | {}'.format(self.ac))

        if self.dmg1 is not None:
            contents.append('Damage 1H | {}'.format(self.dmg1))

        if self.dmg2 is not None:
            contents.append('Damage 2H | {}'.format(self.dmg2))

        if self.dmgType is not None:
            contents.append('DmgType | {}'.format(self.dmgType))

        if self.range is not None:
            contents.append('Range | {}'.format(self.range))

        if self.stealth:
            contents.append('Stealth | Disadvantage')

        if self.strength is not None:
            contents.append('Strength | {}'.format(self.strength))

        contents = ["property | {}".format(x) for x in contents]
        contents.append('text | {}'.format(self.text))

        rpgdict.update(
            title=self.name,
            contents=contents,
            tags=tags,
        )
        return rpgdict

class CategoryDescriptor(object):
    '''
    TODO

    '''
    # pylint: disable=too-few-public-methods
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, dummy_cls):
        category = instance.contents.get(self.name, [])
        category_class = CATEGORY_MAP[self.name].get('cls', CompendiumCategory)
        if isinstance(category, list):
            for comp_def in category:
                yield category_class(comp_def)
        else:
            yield category_class(category)

    def __set__(self, dummy_instance, dummy_value):
        print('Updating compendium categories is not allowed')

    def __delete__(self, dummy_instance):
        print('Deleting compendium categories is not allowed')

class Compendium(object):
    '''
    TODO

    '''
    compatible_versions = ('5',)

    def __init__(self, xml_file):
        self._xml_file = xml_file
        self._contents = None

    def parse_xml_compendium(self):
        '''
        TODO

        '''
        with open(self._xml_file) as infile:
            return xmltodict.parse(infile.read())['compendium']

    @property
    def contents(self):
        '''
        TODO

        '''
        if self._contents is None:
            self._contents = self.parse_xml_compendium()
        return self._contents

    @property
    def version(self):
        '''
        TODO

        '''
        return self.contents.get('@version')

    def carddump(self, categories=None):
        '''
        TODO

        '''
        if categories is None:
            categories = list()

        dump = []
        for obj_list in self._dump(categories):
            for obj in obj_list:
                obj_dict = obj.to_dict()
                if obj_dict is not None:
                    dump.append(obj_dict)

        #     obj.to_dict() for obj_list in self._dump(categories)
        #     for obj in obj_list if obj is not None
        # ]

        return dump

    def _dump(self, categories):
        '''
        TODO

        '''
        if not categories:
            # default to all known categories
            categories = CATEGORY_MAP.keys()

        for category in categories:
            attr_name = CATEGORY_MAP[category]['property']
            if hasattr(self, attr_name):
                attr = getattr(self, attr_name)
                yield [x for x in attr]

    def search_names(self, name):
        '''
        TODO

        '''
        if name == name.lower():
            regex = re.compile('.*%s.*' % name, re.I)
        else:
            regex = re.compile('.*%s.*' % name)
        for item in self.items:
            result = regex.search(item.name)
            if result is not None:
                yield item

    # Create properties for compendium categories and update the map so we
    # can cross reference a category to a Compendium property
    items = CategoryDescriptor('item')
    CATEGORY_MAP['item']['property'] = 'items'
    races = CategoryDescriptor('race')
    CATEGORY_MAP['race']['property'] = 'races'
    backgrounds = CategoryDescriptor('background')
    CATEGORY_MAP['background']['property'] = 'backgrounds'
    feats = CategoryDescriptor('feat')
    CATEGORY_MAP['feat']['property'] = 'feats'
    classes = CategoryDescriptor('class')
    CATEGORY_MAP['class']['property'] = 'classes'
    spells = CategoryDescriptor('spell')
    CATEGORY_MAP['spell']['property'] = 'spells'
    monsters = CategoryDescriptor('monster')
    CATEGORY_MAP['monster']['property'] = 'monsters'

def parse_args():
    '''
    TODO

    '''
    parser = argparse.ArgumentParser(
        description=(
            "Convert Roll20 compendium XML documents into JSON objects "
            "compatible with rpg-card.\n"
            "See:\n"
            "\thttps://github.com/ceryliae/DnDAppFiles\n"
            "\thttps://github.com/crobi/rpg-cards"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "input_file", type=str,
        help="Roll20 Compendium XML file to read"
    )
    parser.add_argument(
        "output_file", type=str, default=None, nargs='?',
        help="rpg-card JSON file to write; or STDOUT if omitted"
    )
    parser.add_argument(
        '-c', dest='categories', action='append', default=[],
        choices=CATEGORY_MAP.keys(),
        help=(
            "Chose the compendium categories you want to convert, default is "
            "everything. Repeat this option for multiple selections."
        )
    )

    return parser.parse_args()

def main():
    '''
    TODO

    '''
    args = parse_args()

    compendium = Compendium(args.input_file)

    if compendium.version not in compendium.compatible_versions:
        print(
            "%s is not compatible with version %s compendiums!" %
            (__file__, compendium.version)
        )
        return 1

    rpgcard_data = json.dumps(
        compendium.carddump(args.categories),
        indent=4,
    )

    if args.output_file is None:
        print(rpgcard_data)
    else:
        with open(args.output_file, 'w') as outfile:
            outfile.write(rpgcard_data)

    return 0

if __name__ == '__main__':
    sys.exit(main())
<|endoftext|>