"""
The SSAP core and supporting code.
"""
#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 itertools
from gavo import base
from gavo import rsc
from gavo import rscdef
from gavo import svcs
from gavo.protocols import datalink
from gavo.svcs import outputdef
MS = base.makeStruct
[docs]class SSADescriptor(datalink.ProductDescriptor):
"""SSA descriptors have ssaRow and limits attributes.
These both reference SSA results. ssaRow is the first result of
the query, which also provides the accref. limits is a table.Limits
instance for the total result set.
Warning: limits will be None if this is constructed with fromSSARow.
"""
ssaRow = None
[docs] @classmethod
def fromSSARow(cls, ssaRow, paramDict):
"""returns a descriptor from a row in an ssa table and
the params of that table.
Don't use this; the limits attribute will be {} for these.
"""
paramDict.update(ssaRow)
ssaRow = paramDict
res = cls.fromAccref(ssaRow["ssa_pubDID"], ssaRow['accref'])
res.ssaRow = ssaRow
res.pubDID = ssaRow["ssa_pubDID"]
res.limits = {}
return res
[docs] @classmethod
def fromSSAResult(cls, ssaResult):
"""returns a descriptor from an SSA query result (an InMemoryTable
instance).
"""
if not ssaResult.rows:
raise base.NotFoundError("any", "row", "ssa result for SODA block"
" generation")
res = cls.fromSSARow(ssaResult.rows[0], ssaResult.getParamDict())
res.limits = ssaResult.getLimits()
res.pubDID = None
return res
def _combineRowIntoOne(ssaRows):
"""makes a "total row" from ssaRows.
In the resulting row, minima and maxima are representative of the
whole result set, and enumerated columns are set-valued.
This is useful when generating parameter metadata.
"""
# XXX TODO: shouldn't this be made more generic by using something like
# in user.info?
if not ssaRows:
raise base.ReportableError("Datalink meta needs at least one result row")
totalRow = ssaRows[0].copy()
totalRow["mime"] = set([totalRow["mime"]])
calibs = set()
for row in ssaRows[1:]:
if row["ssa_specstart"]<totalRow["ssa_specstart"]:
totalRow["ssa_specstart"] = row["ssa_specstart"]
if row["ssa_specend"]>totalRow["ssa_specend"]:
totalRow["ssa_specend"] = row["ssa_specend"]
totalRow["mime"].add(row["mime"])
calibs.add(row.get("ssa_fluxcalib", None))
totalRow["collect_calibs"] = set(c for c in calibs if c is not None)
return totalRow
[docs]def getDatalinkCore(dlSvc, ssaTable):
"""returns a datalink core adapted for ssaTable.
dlSvc is the datalink service, ssaTable a non-empty SSA result table.
"""
allowedRendsForStealing = ["dlget"] #noflake: for stealVar downstack
desc = SSADescriptor.fromSSAResult(ssaTable)
return dlSvc.core.adaptForDescriptors(
svcs.getRenderer("dlget"), [desc], None)
[docs]class SSAPCore(svcs.DBCore):
"""A core doing SSAP queries.
This core knows about metadata queries, version negotiation, and
dispatches on REQUEST. Thus, it may return formatted XML data
under certain circumstances.
Interpreted Properties:
* previews: If set to "auto", the core will automatically add a preview
column and fill it with the URL of the products-based preview. Other
values are not defined.
"""
name_ = "ssapCore"
outputTableXML = """
<outputTable verbLevel="30">
<property name="virtual">True</property>
<FEED source="//ssap#coreOutputAdditionals"/>
</outputTable>"""
previewColumn = base.parseFromString(svcs.OutputField,
'<outputField name="preview" type="text"'
' ucd="meta.ref.url;meta.preview" tablehead="Preview"'
' description="URL of a preview for the dataset"'
' select="NULL" displayHint="type=product" verbLevel="15"/>')
############### Implementation of the service operations
[docs] def onElementComplete(self):
super().onElementComplete()
if self.getProperty("previews", default=False)=="auto":
self.outputTable.columns.append(self.previewColumn)
def _run_getTargetNames(self, service, inputTable, queryMeta):
with base.getTableConn() as conn:
table = rsc.TableForDef(self.queriedTable, create=False,
connection=conn)
destTD = base.makeStruct(outputdef.OutputTableDef,
parent_=self.queriedTable.parent,
id="result", onDisk=False,
columns=[self.queriedTable.getColumnByName("ssa_targname")])
res = table.getTableForQuery(destTD, "", distinct=True)
res.noPostprocess = True
return res
def _addPreviewLinks(self, resultTable):
for row in resultTable:
if "preview" in row and row["preview"] is None:
row["preview"] = row["accref"]+"?preview=True"
def _run_queryData(self, service, inputTable, queryMeta):
limitThroughTOP = inputTable.getParam("TOP")
if limitThroughTOP and limitThroughTOP<queryMeta["dbLimit"]:
queryMeta["dbLimit"] = limitThroughTOP
res = svcs.DBCore.run(self, service, inputTable, queryMeta)
if self.getProperty("previews", default=False)=="auto":
self._addPreviewLinks(res)
if service.hasProperty("datalink"):
# backwards compatibility: The datalink property on the service
# can name a datalink service. We don't want that any more, but
# for now copy it to the queried table (where it should be)
# XXX Deprecate around version 1.3
try:
res.getMeta("_associatedDatalinkService", raiseOnFail=True)
except base.NoMetaKey:
# No datalink metadata on the table yet, generate one from the property.
res.addMeta(
"_associatedDatalinkService.serviceId",
service.getProperty("datalink"))
res.addMeta(
"_associatedDatalinkService.idColumn",
"ssa_pubDID")
return res
################ the main dispatcher
[docs] def run(self, service, inputTable, queryMeta):
defaultRequest = service.getProperty("defaultRequest", "")
requestType = (inputTable.getParam("REQUEST") or defaultRequest).upper()
if requestType=="QUERYDATA":
return self._run_queryData(service, inputTable, queryMeta)
elif requestType=="GETTARGETNAMES":
return self._run_getTargetNames(service, inputTable, queryMeta)
else:
raise base.ValidationError("Missing or invalid value for REQUEST.",
"REQUEST",
hint="According to SSAP, you can only have queryData here;"
" as an extension, this service also understands getTargetNames.")
_VIEW_COLUMNS_CACHE = []
[docs]def iterViewColumns(context):
"""returns a list of column objects for building the SSA view
mixin's columns.
The argument is the DaCHS RD parse context.
This is probably only useful for the //ssap#view mixin. The argument
is that mixin's context. This could go if we drop the hcd and mixc
mixins and instead have a normal STREAM with the columns, as it's really
only necessary to make columns from the stupid params and remove their
defaults.
This will always return the same column objects -- don't change them.
"""
if not _VIEW_COLUMNS_CACHE:
dontCopyAtts = frozenset(["value", "content_"])
protoTable = context.getById("instance")
_VIEW_COLUMNS_CACHE.append([MS(rscdef.Column,
**c.getCopyableAttributes(ignoreKeys=dontCopyAtts))
for c in itertools.chain(
protoTable.columns, protoTable.params)])
return _VIEW_COLUMNS_CACHE[0]