Module tf.browser.serve

Dress TF nodes up for serving on the web

When the TF kernel has retrieved data, it comes in the form of nodes.

But the kernel is the one that is able to dress those nodes up with meaningful data.

That dressing up is happening in this module, it has the higher level functions for composing tables and passages.

Expand source code Browse git
"""
# Dress TF nodes up for serving on the web

When the TF kernel has retrieved data, it comes in the form of nodes.

But the kernel is the one that is able to dress those nodes up with
meaningful data.

That dressing up is happening in this module, it has the higher level
functions for composing tables and passages.
"""

import markdown
from textwrap import dedent

from flask import jsonify, redirect, render_template, make_response

from ..core.helpers import console, wrapMessages
from ..core.files import writeJson
from ..core.text import DEFAULT_FORMAT
from ..advanced.helpers import RESULT
from ..advanced.text import specialCharacters
from .wrap import (
    pageLinks,
    passageLinks,
    wrapColorMap,
    wrapEColorMap,
    wrapOptions,
    wrapSelect,
    wrapProvenance,
)
from .servelib import getAbout, getFormData, zipTables, BATCH


TIMEOUT = 180


def serveTable(web, kind, getx=None, asDict=False):
    kernelApi = web.kernelApi
    aContext = web.context
    interfaceDefaults = aContext.interfaceDefaults

    form = getFormData(interfaceDefaults)
    textFormat = form["textFormat"] or None
    task = form[kind].strip()
    openedKey = f"{kind}Opened"
    openedSet = (
        {int(n) for n in form[openedKey].split(",")} if form[openedKey] else set()
    )

    method = dict if asDict else jsonify

    messages = ""
    table = None
    if task:
        options = {
            k: form.get(k, v) for (k, v) in interfaceDefaults.items() if v is not None
        }
        options["colorMap"] = form.get("colorMap", {})
        options["edgeHighlights"] = form.get("edgeHighlights", {})

        (table, messages) = kernelApi.table(
            kind,
            task,
            form["features"],
            opened=openedSet,
            fmt=textFormat,
            baseTypes=form["baseTypes"],
            hiddenTypes=form["hiddenTypes"],
            edgeFeatures=form["edgeFeatures"],
            getx=int(getx) if getx else None,
            **options,
        )

        if messages:
            (status, messages) = wrapMessages(messages)

    return method(table=table, messages=messages)


def serveQuery(web, getx=None, asDict=False):
    kernelApi = web.kernelApi
    aContext = web.context
    interfaceDefaults = aContext.interfaceDefaults
    wildQueries = web.wildQueries

    kind = "query"
    form = getFormData(interfaceDefaults)
    task = form[kind]
    condenseType = form["condenseType"] or None
    resultKind = condenseType if form["condensed"] else RESULT
    textFormat = form["textFormat"] or None
    openedKey = f"{kind}Opened"
    openedSet = (
        {int(n) for n in form[openedKey].split(",")} if form[openedKey] else set()
    )

    pages = ""
    features = ""

    method = dict if asDict else jsonify
    total = 0

    if task:
        messages = ""
        table = None
        status = True
        if task in wildQueries:
            messages = (
                f"Aborted because query is known to take longer than {TIMEOUT} second"
                + ("" if TIMEOUT == 1 else "s")
            )
            status = False
        else:
            options = {
                k: form.get(k, v)
                for (k, v) in interfaceDefaults.items()
                if v is not None
            }
            options["colorMap"] = form.get("colorMap", {})
            options["edgeHighlights"] = form.get("edgeHighlights", {})

            try:
                (table, status, messages, features, start, total) = kernelApi.search(
                    task,
                    form["batch"],
                    position=form["position"],
                    opened=openedSet,
                    condenseType=condenseType,
                    fmt=textFormat,
                    baseTypes=form["baseTypes"],
                    hiddenTypes=form["hiddenTypes"],
                    edgeFeatures=form["edgeFeatures"],
                    getx=int(getx) if getx else None,
                    **options,
                )
            except TimeoutError:
                messages = (
                    f"Aborted because query takes longer than {TIMEOUT} second"
                    + ("" if TIMEOUT == 1 else "s")
                )
                console(f"{task}\n{messages}", error=True)
                wildQueries.add(task)
                total = 0
                status = False

        if status and table is not None:
            pages = pageLinks(total, form["position"])
    else:
        table = f"no {resultKind}s"
        messages = ""
        status = True

    return method(
        pages=pages,
        table=table,
        nResults=total,
        status=status,
        messages=messages.strip(),
        features=features,
    )


def servePassage(web, getx=None):
    kernelApi = web.kernelApi
    aContext = web.context
    interfaceDefaults = aContext.interfaceDefaults

    form = getFormData(interfaceDefaults)
    textFormat = form["textFormat"] or None

    passages = ""

    openedKey = "passageOpened"
    openedSet = set(form[openedKey].split(",")) if form[openedKey] else set()

    sec0 = form["sec0"]
    sec1 = form["sec1"]
    sec2 = form["sec2"]
    options = {
        k: form.get(k, v) for (k, v) in interfaceDefaults.items() if v is not None
    }
    options["colorMap"] = form.get("colorMap", {})
    options["edgeHighlights"] = form.get("edgeHighlights", {})

    (table, sec0Type, passages, browseNavLevel) = kernelApi.passage(
        form["features"],
        form["query"],
        sec0,
        sec1=sec1,
        sec2=sec2,
        opened=openedSet,
        fmt=textFormat,
        baseTypes=form["baseTypes"],
        hiddenTypes=form["hiddenTypes"],
        edgeFeatures=form["edgeFeatures"],
        getx=getx,
        **options,
    )
    passages = passageLinks(passages, sec0Type, sec0, sec1, browseNavLevel)
    return jsonify(table=table, passages=passages)


def serveExport(web):
    aContext = web.context
    interfaceDefaults = aContext.interfaceDefaults
    appName = aContext.appName
    kernelApi = web.kernelApi
    app = kernelApi.app

    sectionsData = serveTable(web, "sections", asDict=True)
    tuplesData = serveTable(web, "tuples", asDict=True)
    queryData = serveQuery(web, asDict=True)

    form = getFormData(interfaceDefaults)

    (colophon, header, appLogo, tfLogo) = app.header()
    css = kernelApi.css()
    provenance = kernelApi.provenance()
    setNames = kernelApi.setNames()
    setNamesRep = ", ".join(setNames)
    setNameHtml = (
        f'<p class="setnames">Sets: <span class="setnames">{setNamesRep}</span></p>'
        if setNames
        else ""
    )
    (provenanceHtml, provenanceMd) = wrapProvenance(form, provenance, setNames)

    descriptionMd = markdown.markdown(
        form["description"],
        extensions=["markdown.extensions.tables", "markdown.extensions.fenced_code"],
    )

    sectionsMessages = sectionsData["messages"]
    sectionsTable = sectionsData["table"]
    tuplesMessages = tuplesData["messages"]
    tuplesTable = tuplesData["table"]
    queryMessages = queryData["messages"]
    queryTable = queryData["table"]

    # maybe this is a hack. Needed to prevent appName from specified twice

    form["appName"] = appName

    return render_template(
        "export.html",
        # appName=appName,
        css=css,
        descriptionMd=descriptionMd,
        sectionsTable=(
            sectionsMessages
            if sectionsMessages or sectionsTable is None
            else sectionsTable
        ),
        tuplesTable=(
            tuplesMessages if tuplesMessages or tuplesTable is None else tuplesTable
        ),
        queryTable=(
            queryMessages if queryMessages or queryTable is None else queryTable
        ),
        colophon=f"{appLogo}{colophon}{tfLogo}",
        provenance=provenanceHtml,
        setNames=setNameHtml,
        **form,
    )


def serveDownload(web, jobOnly):
    aContext = web.context
    interfaceDefaults = aContext.interfaceDefaults
    form = getFormData(interfaceDefaults)

    if jobOnly:
        appName = form["appName"]
        jobName = form["jobName"]
        fileName = f"{appName}-{jobName}.json"

        headers = {
            "Expires": "0",
            "Cache-Control": "no-cache, no-store, must-revalidate",
            "Content-Type": "application/json",
            "Content-Disposition": f'attachment; filename="{fileName}"',
            "Content-Encoding": "identity",
        }

        buffer = writeJson(
            {k: v for (k, v) in form.items() if k not in {"edgeHighlights", "colorMap"}}
        ).encode("utf8")
        return make_response(buffer, headers)

    kernelApi = web.kernelApi
    app = kernelApi.app
    wildQueries = web.wildQueries

    task = form["query"]
    condensed = form["condensed"]
    condenseType = form["condenseType"] or None
    textFormat = form["textFormat"] or None
    csvs = None
    queryStatus = True
    tupleResultsX = None
    queryResultsX = None
    messages = ""

    if task in wildQueries:
        messages = (
            f"Aborted because query is known to take longer than {TIMEOUT} second"
            + ("" if TIMEOUT == 1 else "s")
        )
    else:
        try:
            (
                queryStatus,
                queryMessages,
                csvs,
                tupleResultsX,
                queryResultsX,
            ) = kernelApi.csvs(
                task,
                form["tuples"],
                form["sections"],
                condensed=condensed,
                condenseType=condenseType,
                fmt=textFormat,
            )
        except TimeoutError:
            queryStatus = False
            messages = f"Aborted because query takes longer than {TIMEOUT} second" + (
                "" if TIMEOUT == 1 else "s"
            )
            console(f"{task}\n{messages}", error=True)
            wildQueries.add(task)
            return jsonify(messages=messages)

    if not queryStatus:
        redirect("/")
        return jsonify(status=queryStatus, messages=queryMessages)

    (colophon, header, appLogo, tfLogo) = app.header()
    provenance = kernelApi.provenance()
    setNames = kernelApi.setNames()
    (provenanceHtml, provenanceMd) = wrapProvenance(form, provenance, setNames)

    about = getAbout(colophon, header, provenanceMd, form)
    (fileName, zipBuffer) = zipTables(csvs, tupleResultsX, queryResultsX, about, form)

    headers = {
        "Expires": "0",
        "Cache-Control": "no-cache, no-store, must-revalidate",
        "Content-Type": "application/octet-stream",
        "Content-Disposition": f'attachment; filename="{fileName}"',
        "Content-Encoding": "identity",
    }
    return make_response(zipBuffer, headers)


def serveAll(web, anything):
    aContext = web.context
    interfaceDefaults = aContext.interfaceDefaults
    appName = aContext.appName
    defaultCondenseType = aContext.condenseType
    defaultTextFormat = aContext.textFormat
    exampleSection = aContext.exampleSection
    exampleSectionHtml = aContext.exampleSectionHtml
    allowedValues = aContext.allowedValues
    showMath = aContext.interfaceDefaults["showMath"]

    mathjax = (
        dedent(
            """
        <script>
        globalThis.MathJax = {
            tex: {
                inlineMath: [['$', '$']],
                displayMath: [['$$', '$$']],
            }
        };
        </script>
        <script
            src="/browser/static/mathjax/tex-chtml.js"
            id="MathJax-script"
            async
        ></script>
        """
        )
        if showMath
        else ""
    )

    kernelApi = web.kernelApi
    app = kernelApi.app

    form = getFormData(interfaceDefaults)
    resetForm = form["resetForm"]

    pages = ""
    passages = ""

    (colophon, header, appLogo, tfLogo) = app.header()
    css = kernelApi.css()
    provenance = kernelApi.provenance()
    setNames = kernelApi.setNames()
    setNamesRep = ", ".join(setNames)
    setNameHtml = (
        f'<p class="setnames">Sets: <span class="setnames">{setNamesRep}</span></p>'
        if setNames
        else ""
    )
    (provenanceHtml, provenanceMd) = wrapProvenance(form, provenance, setNames)

    chooser = {}
    typeCss = ("cline", "ctype")
    formatCss = ("tfline", "ttext")

    for (option, group, item, multiple) in (
        ("baseTypes", "bcheck", typeCss, True),
        ("condenseType", "cradio", typeCss, False),
        ("hiddenTypes", "hcheck", typeCss, True),
        ("edgeFeatures", "echeck", typeCss, True),
        ("textFormat", "tradio", formatCss, False),
    ):
        value = aContext.get(option, None) if resetForm else form[option]
        options = wrapSelect(option, allowedValues, value, group, item, multiple)
        chooser[option] = options

    (options, optionsMoved, optionsHelp) = wrapOptions(aContext, form)
    colorMapHtml = wrapColorMap(form)
    eColorMapHtml = wrapEColorMap(form)

    characters = specialCharacters(app, fmt=form.get("textFormat", DEFAULT_FORMAT), _browse=True)

    templateData = dict(
        css=css,
        mathjax=mathjax,
        characters=characters,
        colorMapHtml=colorMapHtml,
        eColorMapHtml=eColorMapHtml,
        colophon=f"{appLogo}{colophon}{tfLogo}",
        header=header,
        setNames=setNameHtml,
        options=options,
        optionsHelp=optionsHelp,
        chooser=chooser,
        condensedOption=optionsMoved["condensed"],
        forceEdgesOption=optionsMoved["forceEdges"],
        hideTypesOption=optionsMoved["hideTypes"],
        defaultCondenseType=defaultCondenseType,
        defaultTextFormat=defaultTextFormat,
        exampleSectionHtml=exampleSectionHtml,
        exampleSection=exampleSection,
        pages=pages,
        passages=passages,
        author="",
        title="",
        description="",
        messages="",
        sections="",
        tuples="",
        query="",
        position=1,
        batch=BATCH,
        passageOpened="",
        sectionsOpened="",
        tuplesOpened="",
        queryOpened="",
    )
    for (k, v) in form.items():
        if not resetForm or k not in templateData:
            templateData[k] = v
    templateData["appName"] = appName
    templateData["resetForm"] = ""
    return render_template(
        "index.html",
        **templateData,
    )

Functions

def serveAll(web, anything)
def serveDownload(web, jobOnly)
def serveExport(web)
def servePassage(web, getx=None)
def serveQuery(web, getx=None, asDict=False)
def serveTable(web, kind, getx=None, asDict=False)