def exercise_callback_factory(match, url_template,
                              mc_client=None, token=None, mml_url=None):
    """Create a callback function to replace an exercise by fetching from
    a server."""

    def _replace_exercises(elem):
        item_code = elem.get('href')[len(match):]
        url = url_template.format(itemCode=item_code)
        exercise = {}
        if mc_client:
            mc_key = item_code + (token or '')
            exercise = json.loads(mc_client.get(mc_key) or '{}')

        if not exercise:
            if token:
                headers = {'Authorization': 'Bearer {}'.format(token)}
                res = requests.get(url, headers=headers)
            else:
                res = requests.get(url)
            if res:
                # grab the json exercise, run it through Jinja2 template,
                # replace element w/ it
                exercise = res.json()
                if mc_client:
                    mc_client.set(mc_key, res.text)

        if exercise['total_count'] == 0:
            logger.warning('MISSING EXERCISE: {}'.format(url))

            XHTML = '{{{}}}'.format(HTML_DOCUMENT_NAMESPACES['xhtml'])
            missing = etree.Element(XHTML + 'div',
                                    {'class': 'missing-exercise'},
                                    nsmap=HTML_DOCUMENT_NAMESPACES)
            missing.text = 'MISSING EXERCISE: tag:{}'.format(item_code)
            nodes = [missing]
        else:
            html = EXERCISE_TEMPLATE.render(data=exercise)
            try:
                nodes = etree.fromstring('<div>{}</div>'.format(html))
            except etree.XMLSyntaxError:  # Probably HTML
                nodes = etree.HTML(html)[0]  # body node

            if mml_url:
                for node in nodes.xpath('//*[@data-math]'):
                    mathml = _replace_tex_math(node, mml_url, mc_client)
                    if mathml is not None:
                        mparent = node.getparent()
                        mparent.replace(node, mathml)
                    else:
                        mathtext = node.get('data-math') or node.text or ''
                        logger.warning('BAD TEX CONVERSION: "%s" URL: %s'
                                       % (mathtext.encode('utf-8'), url))

        parent = elem.getparent()
        if etree.QName(parent.tag).localname == 'p':
            elem = parent
            parent = elem.getparent()

        parent.remove(elem)  # Special case - assumes single wrapper elem
        for child in nodes:
            parent.append(child)

    xpath = '//xhtml:a[contains(@href, "{}")]'.format(match)
    return (xpath, _replace_exercises)
Create a callback function to replace an exercise by fetching from
    a server.