Source code for gavo.web.serviceresults

"""
"Output drivers" for various formats, for the use of form-like renderers.

TODO: Tar and the output format widget should go somewhere else; the
rest should be done by RESPONSEFORMAT and formats.  Then this module
should die.
"""

#c Copyright 2008-2023, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL.  See the
#c COPYING file in the source distribution.


import os

from twisted.internet import threads
from twisted.web.template import tags as T

from gavo import base
from gavo import formats
from gavo import utils
from gavo.formats import csvtable #noflake: format registration
from gavo.formats import jsontable #noflake: format registration
from gavo.formats import fitstable #noflake: format registration
from gavo.formats import texttable #noflake: format registration
from gavo.formats import geojson #noflake: format registration
from gavo.formal import types as formaltypes
from gavo.formal.util import render_cssid
from gavo.protocols import products
from gavo.svcs import customwidgets
from gavo.svcs import streaming
from gavo.web import producttar

__docformat__ = "restructuredtext en"


[docs]class ServiceResult(object): """A base class for objects producing formatted output. They are basically wrappers for _formatOutput, which will usually be used as sources for streaming.streamOut. All methods on these objects are class methods -- they are never instantiated. Deriving classes can override - _formatOutput(result, request, queryMeta) -- receives the service result and arranges for it to be written. - canWrite(colSeq) -- a that returns true when a seq of columns can be serialized by this service result. - code -- an identifier used in HTTP query strings to select the format - label (optional) -- a string that is used in HTML forms to select the format (defaults to label). - compute -- if False, at least the form renderer will not run the service (this is when you just return a container). """ compute = True code = None label = None @classmethod def _formatOutput(cls, res, request, queryMeta): return b""
[docs] @classmethod def getLabel(cls): if cls.label is not None: return cls.label return cls.code
[docs] @classmethod def canWrite(cls, colSeq): return True
[docs]class VOTableResult(ServiceResult): """A ResultFormatter for VOTables. The VOTables come as attachments, i.e., if all goes well the form will just stand as it is. """ code = "VOTable" @classmethod def _formatOutput(cls, data, request, queryMeta): if base.getMetaText(data.getPrimaryTable(), "_queryStatus" )=="OVERFLOW": fName = "truncated_votable.xml" else: fName = "votable.xml" request.setHeader("content-type", base.votableType) request.setHeader('content-disposition', 'attachment; filename=%s'%fName) return streaming.streamVOTable(request, data, queryMeta)
[docs]class FITSTableResult(ServiceResult): """returns data as a FITS binary table. """ code = "FITS" label = "FITS table"
[docs] @classmethod def getTargetName(cls, data): if base.getMetaText(data.getPrimaryTable(), "_queryStatus" )=="OVERFLOW": return "truncated_data.fits", "application/x-fits" else: return "data.fits", "application/x-fits"
@classmethod def _formatOutput(cls, data, request, queryMeta): return threads.deferToThread(fitstable.makeFITSTableFile, data ).addCallback(cls._serveFile, data, request) @classmethod def _serveFile(cls, filePath, data, request): name, mime = cls.getTargetName(data) request.setHeader("content-type", mime) request.setHeader('content-disposition', 'attachment; filename=%s'%name) def writeData(dest): try: with open(filePath, "rb") as f: utils.cat(f, dest) finally: os.unlink(filePath) return streaming.streamOut(writeData, request)
[docs]class TSVResponse(ServiceResult): code = "TSV" label = "Text (with Tabs)" @classmethod def _formatOutput(cls, data, request, queryMeta): request.setHeader("content-type", "text/tab-separated-values") def produceData(destFile): content = texttable.getAsText(data) destFile.write(content) return streaming.streamOut(produceData, request, queryMeta)
[docs]class TextResponse(ServiceResult): code = "txt" label = "Text (fixed columns)" @classmethod def _formatOutput(cls, data, request, queryMeta): request.setHeader("content-type", "text/plain") def produceData(destFile): formats.formatData("txt", data, request, acquireSamples=False) return streaming.streamOut(produceData, request, queryMeta)
[docs]class CSVResponse(ServiceResult): code = "CSV" label = "CSV" @classmethod def _formatOutput(cls, data, request, queryMeta): request.setHeader('content-disposition', 'attachment; filename=table.csv') request.setHeader("content-type", "text/csv;header=present") def produceData(destFile): formats.formatData("csv", data, request, acquireSamples=False) return streaming.streamOut(produceData, request, queryMeta)
[docs]class JsonResponse(ServiceResult): code = "JSON" label = "JSON" @classmethod def _formatOutput(cls, data, request, queryMeta): request.setHeader('content-disposition', 'attachment; filename=table.json') request.setHeader("content-type", "application/json") def produceData(destFile): formats.formatData("json", data, request, acquireSamples=False) return streaming.streamOut(produceData, request, queryMeta)
[docs]class TarResponse(ServiceResult): """delivers a tar of products requested. """ code = "tar" @classmethod def _formatOutput(cls, data, request, queryMeta): return producttar.getTarMaker().deliverProductTar( data, request, queryMeta)
[docs] @classmethod def canWrite(cls, colSeq): if products.getProductColumns(colSeq): return True return False
################# Helpers _getFormat = utils.buildClassResolver(ServiceResult, list(globals().values()), key=lambda obj: obj.code)
[docs]def getFormat(formatName): try: return _getFormat(formatName) except KeyError: raise base.ValidationError("Unknown format '%s'."%formatName, "_OUTPUT")
[docs]class OutputFormat(object): """A widget that offers various output options in close cooperation with gavo.js and QueryMeta. The javascript provides options for customizing output that non-javascript users will not see. Also, formal doesn't see any of these. See gavo.js for details. This widget probably only makes sense in the Form renderer and thus should probably go there. """ def __init__(self, typeOb, service, queryMeta): self.service = service self.typeOb = typeOb self._computeAvailableFields(queryMeta) self._computeAvailableFormats(queryMeta) def _computeAvailableFormats(self, queryMeta): """sets the availableFormats property. This is a helper for the constructor, inspecting serviceresults' globals(). """ outputFields = self.service.getCurOutputFields( queryMeta, raiseOnUnknown=False) self.availableFormats = [ (code, format.getLabel()) for code, format in _getFormat.registry.items() if format.canWrite(outputFields)] def _computeAvailableFields(self, queryMeta): """computes the fields a Core provides but are not output by the service by default. This of course only works if the core defines its output table. Otherwise, availableFields is an empty list. """ self.availableFields = [] core = self.service.core if (not core.outputTable or self.service.getProperty("noAdditionals", False)): return coreNames = set(f.name for f in core.outputTable) defaultNames = set([f.name for f in self.service.getHTMLOutputFields( queryMeta, ignoreAdditionals=True, raiseOnUnknown=False)]) for key in coreNames-defaultNames: try: self.availableFields.append((core.outputTable.getColumnByName(key), key in queryMeta["additionalFields"])) except KeyError: # Core returns fields not in its table, # probably computes them pass def _makeAdditionalSelector(self): """returns an ul element containing form material for additional output columns. """ checkLiterals = {True: "checked", False: None} fields = [] for column, checked in sorted( self.availableFields, key=lambda p:p[0].name): fields.append(T.tr[ T.td[ T.input(type="checkbox", name="_ADDITEM", value=column.key, style="width:auto", checked=checkLiterals[checked])], T.td(style="vertical-align:middle")[ " %s -- %s"%(column.name, column.description)]]) return T.table(id="addSelection")[fields]
[docs] def render(self, request, key, args, errors): res = T.div(id=render_cssid("_OUTPUT"), style="position:relative")[ customwidgets.SelectChoice(formaltypes.String(), options=self.availableFormats, noneOption=("HTML", "HTML")).render(request, "_FORMAT", args, errors)( onchange="output_broadcast(this.value)")] if self.availableFields: res[ T.div(title="Additional output column selector", id=render_cssid("_ADDITEMS"), style="display:none")[ self._makeAdditionalSelector()]] return res
renderImmutable = render # This is a lost case
[docs] def processInput(self, request, key, args): return args.get("_FORMAT", ["HTML"])[0]