"""
Services, i.e. combinations of a core and at least one renderer.
See the __init__.py docstring for a bit more on the general
architecture.
"""
#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 functools
import os
import urllib.request, urllib.parse, urllib.error
from twisted.web.template import tags as T #noflake: namespace registration
from gavo import base
from gavo import rsc
from gavo import rscdef
from gavo import utils
from gavo.base import meta
from gavo.formal import nevowc
from gavo.rsc import table
from gavo.rscdef import rmkdef
from gavo.svcs import common
from gavo.svcs import core
from gavo.svcs import inputdef
from gavo.svcs import outputdef
from gavo.svcs import renderers
MS = base.makeStruct
[docs]class Publication(base.Structure, base.ComputedMetaMixin):
"""A specification of how a service should be published.
This contains most of the metadata for what is an interface in
registry speak.
"""
name_ = "publish"
_rd = rscdef.RDAttribute()
_render = base.UnicodeAttribute("render", default=base.Undefined,
description="The renderer the publication will point at.",
copyable=True)
_sets = base.StringSetAttribute("sets",
description="Comma-separated list of sets this service will be"
" published in. Predefined are: local=publish on front page,"
" ivo_managed=register with the VO registry. If you leave it"
" empty, 'local' publication is assumed.",
copyable="True")
_service = base.ReferenceAttribute("service", default=base.NotGiven,
description="Reference for a service actually implementing the"
" capability corresponding to this publication. This is"
" mainly when there is a vs:WebBrowser service accompanying a VO"
" protocol service, and this other service should be published"
" in the same resource record; you would say something like"
' ``<publish render="form" sets="..." service="web"/>``.',
copyable="True")
_auxiliary = base.BooleanAttribute("auxiliary", default=False,
description="Auxiliary publications are for capabilities"
" not intended to be picked up for all-VO queries, typically"
" because they are already registered with other services."
" This is mostly used internally; you probably have no reason"
" to touch it.")
def __repr__(self):
return "<%sPublication of %s in %s>"%(
"Auxiliary " if self.auxiliary else "",
self.service.getFullId(),
self.sets)
def _completeMetadataFromResRow(self, resRow):
"""fiddles publication dates from a dc.resources row for the parent
service.
(see rscdef.rdjinj for where this comes from).
"""
if resRow.get("rectimestamp"):
self.service.setMeta("_metadataUpdated", resRow["rectimestamp"])
# we ignore dateupdated here, assuming that the info coming from
# the RD is more current.
[docs] def completeElement(self, ctx):
if self.render is base.Undefined:
self.render = "form"
if not self.sets:
self.sets.add("local")
if self.service is base.NotGiven:
self.service = self.parent
self.setMetaParent(self.service)
if (ctx is not None
# RD-less services are probably testing-only, and they certainly
# won't be registered.
and self.rd is not None):
self._completeMetadataFromResRow(
ctx.getInjected("resprop:%s#%s"%(
self.service.rd.sourceId, self.service.id), {}))
super().completeElement(ctx)
[docs] def validate(self):
super().validate()
try:
renderers.getRenderer(self.render)
except KeyError:
raise base.StructureError("Unknown renderer: %s"%self.render)
unknownSets = self.sets-set(base.getConfig("ivoa", "validoaisets"))
if unknownSets:
raise base.StructureError("Invalid OAI set(s) %s"%
", ".join(unknownSets),
hint="If you really want to use custom sets, declare add them"
" using the [ivoa]validOAISets config item.")
def _meta_accessURL(self):
return self.service.getURL(self.render, canonical=True)
def _meta_urlUse(self):
return renderers.getRenderer(self.render).urlUse
def _meta_requestMethod(self):
return renderers.getRenderer(self.render).preferredMethod
def _meta_resultType(self):
return renderers.getRenderer(self.render).resultType
class _inevowFake(object):
"""A minimal fake for DaCHS1 imp.nevow.inevow.
DaCHS 1 docs instructed users to get the request object using
inevow.IRequest(ctx). This thing lets people run RDs doing that.
I guess we won't remove it any time soon -- it's not expensive, and
removing it would probably break quite a few RDs.
"""
@classmethod
def IRequest(cls, ctx):
return ctx.request
[docs]class CustomPageFunction(base.Structure, base.RestrictionMixin):
"""An abstract base for t.w.resource.Resource-related functions on services.
"""
_name = base.UnicodeAttribute("name", default=base.Undefined,
description="Name of the render or data function (use this in the"
" n:render or n:data attribute in custom templates).",
copyable=True, strip=True)
_code = base.DataContent(description="Function body of the renderer"
" or data function; the"
" arguments are named ``request`` and ``tag``, but you can also use"
" legacy ctx.tag and ctx.request. If data has been set on a tag,"
" you will see it as ``data``.", copyable=True)
[docs] def onElementComplete(self):
super().onElementComplete()
vars = globals().copy()
vars["service"] = self.parent
vars["_FakeContext"] = _FakeContext
vars["inevow"] = _inevowFake
exec("\n".join([
"def %s(self, request, tag):"%(self.name),
" ctx = _FakeContext(tag, request)",
" data = tag.slotData",
utils.fixIndentation(self.content_, newIndent=" ",
governingLine=1).rstrip()]), vars)
self.func = vars[self.name]
class _FakeContext(object):
"""A compatibility wrapper for legacy nevow contexts.
These don't exist any more, but I didn't want to break all custom
render and data functions in RDs, so I fake enough to give people
the standard ctx.tag. There's even a fake invevow above that's just
enough to pull the request from here.
"""
def __init__(self, tag, request):
self.tag, self.request = tag, request
[docs]class CustomRF(CustomPageFunction):
"""A custom render function for a service.
Custom render functions can be used to expose certain aspects of a service
to Nevow templates. Thus, their definition usually only makes sense with
custom templates, though you could, in principle, override built-in
render functions.
In the render functions, you have the names ctx for a context and
data for the "current data" (i.e., what's last been set using n:data).
In ctx, only use ctx.tag (the tag on which the n:render attribute
sits) and, if necessary ctx.request (the t.w request object).
Also, the active renderer is visible as self; the one thing you
might want to see from there is self.queryMeta, which contains,
for instance, the input parameters (but be careful: the inputTable
will be None when input errors are rendered, so better to code
using it like this::
if self.queryMeta["inputTable"] and self.queryMeta["inputTable"]...:
You can return anything that can be in a stan DOM. Usually, this will be
a string. To return HTML, use the stan DOM available under the T namespace.
As an example, the following code returns the current data as a link::
return ctx.tag[T.a(href=data)[data]]
You can access the embedding service as service, the embedding
RD as service.rd.
"""
name_ = "customRF"
[docs]class CustomDF(CustomPageFunction):
"""A custom data function for a service.
Custom data functions can be used to expose certain aspects of a service
to Nevow templates. Thus, their definition usually only makes sense with
custom templates, though you could, in principle, override built-in
render functions.
In the data functions, you have the names ctx for a context and
data for the "current data" (i.e., what's last been set using n:data).
In ctx, only use ctx.tag (the tag on which the n:render attribute
sits) and, if necessary ctx.request (the t.w request object).
Also, the active renderer is visible as self; the one thing you
might want to see from there is self.queryMeta, which contains,
for instance, the input parameters.
You can access the embedding service as service, the embedding
RD as service.rd.
You can return arbitrary python objects -- whatever the render functions
can deal with. You could, e.g., write::
<customDF name="now">
return datetime.datetime.utcnow()
</customDF>
You can use the request to fetch request parameters. Within DaCHS,
in addition to the clumsy request.args (mapping bytes to bytes), there
is also request.strargs (mapping strings to strings). So, access a
query parameter ``order`` like this::
sortOrder = ctx.request.strargs.get("order", ["authors"])
"""
name_ = "customDF"
[docs]class CoreAttribute(base.ReferenceAttribute):
def __init__(self):
base.ReferenceAttribute.__init__(self, "core",
description="The core that does the computations for this service."
" Instead of a reference, you can use an immediate element"
" of some registered core.",
forceType=core.Core, copyable=True, aliases=list(core.CORE_REGISTRY.keys()))
def _makeChild(self, name, parent):
return core.getCore(name)(parent)
[docs]@functools.lru_cache(1)
def getDALIServiceKeys():
"""returns a list of the service keys defined in //pql#DALIPars.
This is always the same object, so if you really have to change anything
in here, be sure to make copies before touching either the list or
the items.
"""
nullSvc = base.parseFromString(Service,
"""<service><nullCore/><FEED source="//pql#DALIPars"/></service>""")
res = []
for inputKey in nullSvc.serviceKeys:
# let's orphan them in a way that they won't be reparented
inputKey.parent_ = base.NotGiven
res.append(inputKey)
return res
[docs]class Service(base.Structure, base.ComputedMetaMixin,
base.StandardMacroMixin, rscdef.IVOMetaMixin):
"""A service definition.
A service is a combination of a core and one or more renderers. They
can be published, and they carry the metadata published into the VO.
You can set the defaultSort property on the service to a name of an
output column to preselect a sort order. Note again that this will
slow down responses for all but the smallest tables unless there is
an index on the corresponding column.
Properties evaluated:
* votableRespectsOutputTable -- usually, VOTable output puts in
all columns from the underlying database table with low enough
verbLevel (essentially). When this property is "True" (case-sensitive),
that's not done and only the service's output table is evaluated.
* fixedFormat -- if this is set to "True", the form renderer will
not produce an output format selector (and we shouldn't produce
DALI RESPONSEFORMAT metadata).
"""
name_ = "service"
_core = CoreAttribute()
_templates = base.DictAttribute("templates", description="Custom"
' nevow templates for this service; use key "form" to replace the Form'
" renderer's standard template; qp uses resulttable and resultline"
" depending on whether there's many result lines or just one."
" Start the path with two slashes to access system templates.",
itemAttD=rscdef.ResdirRelativeAttribute(
"template", description="resdir-relative path to a nevow template"
" used for the function given in key."), copyable=True)
_publications = base.StructListAttribute("publications",
childFactory=Publication, description="Sets and renderers this service"
" is published with.")
_limitTo = base.UnicodeAttribute("limitTo", default=None,
description="Limit access to the group given; the empty default disables"
" access control.", copyable="True")
_customPage = rscdef.ResdirRelativeAttribute("customPage", default=None,
description="resdir-relative path to custom page code. It is used"
" by the 'custom' renderer", copyable="True")
_allowedRenderers = base.StringSetAttribute("allowed",
description="Names of renderers allowed on this service; leave empty"
" to allow the form renderer only.", copyable=True)
_customRF = base.StructListAttribute("customRFs",
description="Custom render functions for use in custom templates.",
childFactory=CustomRF, copyable=True)
_customDF = base.StructListAttribute("customDFs",
description="Custom data functions for use in custom templates.",
childFactory=CustomDF, copyable=True)
_outputTable = base.StructAttribute("outputTable", default=None,
childFactory=outputdef.OutputTableDef, copyable=True, description=
"The output fields of this service.")
_serviceKeys = base.UniquedStructListAttribute("serviceKeys",
uniqueAttribute="name", policy="drop",
childFactory=inputdef.InputKey, description="Input widgets for"
" processing by the service, e.g. output sets.", copyable=True)
_defaultRenderer = base.UnicodeAttribute("defaultRenderer",
default=None, description="A name of a renderer used when"
" none is provided in the URL (lets you have shorter URLs).")
_rd = rscdef.RDAttribute()
_props = base.PropertyAttribute()
_original = base.OriginalAttribute()
metaModel = ("title(1), creationDate(1), description(1),"
"subject, referenceURL(1), shortName(!)")
# formats that should query the same fields as HTML (the others behave
# like VOTables and offer a "verbosity" widget in forms).
htmlLikeFormats = ["HTML", "tar"]
####################### Housekeeping methods
def __repr__(self):
return "<Service %s at %s>"%(self.id, self.getSourcePosition())
[docs] def completeElement(self, ctx):
super().completeElement(ctx)
if not self.allowed:
self.allowed.add("form")
if self.core is base.Undefined:
raise base.StructureError("Services must have cores (add <nullCore/>"
" if you really do not want a core, e.g., with fixed renderers).")
# if there's only one renderer on this service, make it the default
if self.defaultRenderer is None and len(self.allowed)==1:
self.defaultRenderer = list(self.allowed)[0]
# if there's a DALI-compliant renderer on this service, declare
# its service parameters (MAXREC, RESPONSEFORMAT, etc).
# Of course, other renderers might ignore them, but presumably
# the metadata generated from this are only really evaluated
# by VO/DALI components.
# "DALI-complicance" is currently determined by a parameter
# style of dali or pql.
for rendName in self.allowed:
try:
ps = renderers.getRenderer(rendName).parameterStyle
except base.NotFoundError:
# letting this exc through could lead to confusing error messages
# on cross-RD reference, so we just warn:
base.ui.notifyWarning("Invalid renderer spec '%s' on service at %s"%
(rendName, self.getSourcePosition()))
continue
if ps in ["dali", "pql"]:
for inputKey in getDALIServiceKeys():
self._serviceKeys.addStruct(self, inputKey)
break
# cache all kinds of things expensive to create and parse
self._coresCache = {}
self._loadedTemplates = {}
# Schedule the capabilities to be added when the parse is
# done (i.e., the RD is complete)
ctx.addExitFunc(lambda rd, ctx: self._addAutomaticCapabilities())
[docs] def onElementComplete(self):
super().onElementComplete()
# Index custom render/data functions
self.nevowRenderers = {}
for customRF in self.customRFs:
self.nevowRenderers[customRF.name] = customRF.func
self.nevowDataFunctions = {}
for customDF in self.customDFs:
self.nevowDataFunctions[customDF.name] = customDF.func
self._compileCustomPage()
self._computeResourceType()
def _compileCustomPage(self):
if self.customPage:
try:
modNs, moddesc = utils.loadPythonModule(self.customPage)
modNs.RD = self.rd
getattr(modNs, "initModule", lambda: None)()
page = modNs.MainPage
except ImportError:
raise base.ui.logOldExc(
base.LiteralParseError("customPage", self.customPage,
hint="This means that an exception was raised while DaCHS"
" tried to import the renderer module. If DaCHS ran"
" with --debug, the original traceback is available"
" in the logs."))
self.customPageCode = page, (os.path.basename(self.customPage),moddesc)
[docs] def getTemplate(self, key, reload=False):
"""returns the nevow template for the function key on this service.
Pass reload to force a reload of templates already parsed.
"""
if reload or key not in self._loadedTemplates:
tp = self.templates[key]
if tp.startswith("//"):
self._loadedTemplates[key] = common.loadSystemTemplate(tp[2:])
else:
self._loadedTemplates[key] = nevowc.XMLFile(
os.path.join(self.rd.resdir, tp))
return self._loadedTemplates[key]
[docs] def getUWS(self):
"""returns the UWS worker system for this service.
This is a service for the DALIAsyncRenderer.
"""
if not hasattr(self, "uws"):
if hasattr(self.core, "workerSystem"):
self.uws = self.core.workerSystem
else:
# todo: we don't want that any more. People wanting user UWS
# should attach their worker systems to their cores in the future.
from gavo.protocols import useruws
self.uws = useruws.makeUWSForService(self)
return self.uws
################### Registry and related methods.
@property
def isVOPublished(self, renderer=None):
"""is true if there is any ivo_managed publication on this
service.
If renderer is non-None, only publications with this renderer name
count.
"""
for pub in self.publications:
if "ivo_managed" in pub.sets:
if renderer:
if pub.render==renderer:
return True
else:
return True
return False
def _computeResourceType(self):
"""sets the resType attribute.
Services are resources, and the registry code wants to know what kind.
This method ventures a guess. You can override this decision by setting
the resType meta item.
"""
if (self.core.outputTable.columns
or "dali" in self.allowed):
self.resType = "catalogService"
else: # no output table defined, we're a plain service
self.resType = "nonTabularService"
def _iterAutomaticCapabilities(self):
"""helps _addAutomaticCapabilities.
Actually, we also use it to generate VOSI capabilities for
unpublished services.
"""
vosiSet = set(["ivo_managed"])
# All actual services get VOSI caps
if not isinstance(self.core, core.getCore("nullCore")):
yield base.makeStruct(Publication,
render="availability",
sets=vosiSet,
parent_=self)
yield base.makeStruct(Publication,
render="capabilities",
sets=vosiSet,
parent_=self)
yield base.makeStruct(Publication,
render="tableMetadata",
sets=vosiSet,
parent_=self)
# things querying tables get a TAP relationship if
# their table is adql-queriable
if isinstance(self.core, core.getCore("dbCore")):
if self.core.queriedTable.adql:
tapService = base.resolveCrossId("//tap#run")
yield base.makeStruct(Publication,
render="dali",
sets=vosiSet,
auxiliary=True,
service=tapService,
parent_=self)
# things with examples meta get an examples capability
try:
self.getMeta("_example", raiseOnFail=True)
yield base.makeStruct(Publication,
render="examples",
sets=utils.AllEncompassingSet(),
parent_=self)
except base.NoMetaKey:
pass
def _addAutomaticCapabilities(self):
"""adds some publications that are automatic for certain types
of services.
For services with ivo_managed publications and with useful cores
(this keeps out doc-like publications, which shouldn't have VOSI
resources), artificial VOSI publications are added.
If there is _example meta, an examples publication is added.
If this service exposes a table (i.e., a DbCore with a queriedTable)
and that table is adql-readable, also add an auxiliary TAP publication
if going to the VO.
This is being run as an exit function from the parse context as
we want the RD to be complete at this point (e.g., _examples
meta might come from it). This also lets us liberally resolve
references anywhere.
"""
if not self.isVOPublished:
return
for pub in self._iterAutomaticCapabilities():
self._publications.feedObject(self, pub)
# This is no longer a good place to do this, but TAP-published
# tables need a isServedBy, and we since it's automatic, let's
# do it here.
# According to the "discovering dependent" note, we don't
# do the reverse relationship lest the TAP service
# gets too related...
if isinstance(self.core, core.getCore("dbCore")):
if self.core.queriedTable.adql:
tapService = base.resolveCrossId("//tap#run")
self.addMeta("isServedBy",
base.getMetaText(tapService, "title"),
ivoId=base.getMetaText(tapService, "identifier"))
def _iterAssociatedDatalinkServices(self):
"""yields instances for datalink services appropriate for this
service.
The datalink services are taken from the core's queriedTable
attribute, if available. As a legacy fallback, for now
the datalink property on the service is supported as well for now,
but I'll try to get rid of that.
"""
queriedTable = getattr(self.core, "queriedTable", None)
# if there's not queried table, there's not much point in
# having a datalink service.
linkGenerated = False
if queriedTable:
for svcRef in queriedTable.iterMeta(
"_associatedDatalinkService.serviceId"):
yield base.resolveId(self.rd, str(svcRef))
linkGenerated = True
# Legacy fallback in case no datalink services are found in the table:
# the datalink property
if not linkGenerated:
svcRef = self.getProperty("datalink", None)
if svcRef:
yield self.rd.getById(svcRef)
[docs] def getPublicationsForSet(self, names, includeDatalink=True):
"""returns publications for a set of set names (the names argument).
In the special case names=None, all allowed renderers are treated
as published.
"""
addAllAllowed = False
if names is None:
names = utils.AllEncompassingSet()
addAllAllowed = True
# the or in the list comprehension is because I can't see a way
# to make AllEmcompassingSet work on the right side of the
# operand, but it can occur on both sides.
result = [pub for pub in self.publications
if pub.sets & names or names & pub.sets]
# for ivo_managed, also return a datalink endpoints if they're
# there; the specs imply that might be useful some day.
# (btw., I think we shouldn't do that but instead extend
# publish so people can ask for other services to be included
# as capabilities).
if includeDatalink and "ivo_managed" in names:
for dlSvc in self._iterAssociatedDatalinkServices():
if "dlget" in dlSvc.allowed:
result.append(base.makeStruct(Publication,
render="dlget",
sets=set(["ivo_managed"]),
service=dlSvc))
if "dlasync" in dlSvc.allowed:
result.append(base.makeStruct(Publication,
render="dlasync",
sets=set(["ivo_managed"]),
service=dlSvc))
if "dlmeta" in dlSvc.allowed:
result.append(base.makeStruct(Publication,
render="dlmeta",
sets=set(["ivo_managed"]),
service=dlSvc))
if addAllAllowed: # name=None was passed in,
# add publications for everything allowed but not yet marked
# as published (for VOSI)
alreadyPublished = set(p.render for p in result
) | frozenset(["sync", "async"]) # these never get capabilities
for rendName in self.allowed:
if not rendName in alreadyPublished:
result.append(base.makeStruct(Publication,
render=rendName, sets=set(["vosi"]), service=self))
# these will only be added here for unpublished services
for pub in self._iterAutomaticCapabilities():
if not pub.render in alreadyPublished:
result.append(pub)
return result
[docs] def getURL(self, rendName, absolute=True, canonical=False, **kwargs):
"""returns the full canonical access URL of this service together
with renderer.
rendName is the name of the intended renderer in the registry
of renderers.
With absolute, a fully qualified URL is being returned.
Further keyword arguments are translated into URL parameters in the
query part.
"""
basePath = "%s%s/%s"%(base.getConfig("web", "nevowRoot"),
self.rd.sourceId, self.id)
if absolute:
basePath = base.makeAbsoluteURL(basePath, canonical=canonical)
res = renderers.getRenderer(rendName
).makeAccessURL(basePath)
if kwargs:
res = res+"?"+urllib.parse.urlencode(kwargs)
return res
# used by getBrowserURL; keep external higher than form as long as
# we have mess like Potsdam CdC.
_browserScores = {"form": 10, "external": 12, "fixed": 15,
"custom": 8, "img.jpeg": 7, "static": 5, "hips": 3, "examples": 2}
[docs] def getBrowserURL(self, fq=True):
"""returns a published URL that's suitable for a web browser or None if
no such URL can be guessed.
If you pass fq=False, you will get a path rather than a URL.
"""
# There can be multiple candidates for browser URLs (like when a service
# has both form, static, and external renderers). If so, we select
# by plain scores.
browseables = []
for rendName in self.allowed:
if self.isBrowseableWith(rendName):
browseables.append((self._browserScores.get(rendName, -1), rendName))
if browseables:
return self.getURL(max(browseables)[1], absolute=fq)
else:
return None
[docs] def isBrowseableWith(self, rendName):
"""returns true if rendering this service through rendName results
in something pretty in a web browser.
"""
try:
return bool(renderers.getRenderer(rendName).isBrowseable(self))
except base.NotFoundError: # renderer name not known
return False
[docs] def getTableSet(self):
"""returns a list of table definitions that have something to do with
this service.
This is for VOSI-type requests.
Basically, we're leaving the decision to the core, except when
we have an output Table of our own.
"""
if self.outputTable and self.outputTable.columns:
return [self.outputTable]
return self.core.getRelevantTables()
[docs] def declareServes(self, data):
"""adds meta to self and data indicating that data is served by
this service.
This is used by table/@adql and the publish element on data.
"""
if data.registration:
self.addMeta("isServiceFor",
base.getMetaText(data, "title", default="Anonymous"),
ivoId=base.getMetaText(data, "identifier"))
data.addMeta("isServedBy",
base.getMetaText(self, "title"),
ivoId=base.getMetaText(self, "identifier"))
# Since this is always initiated by the data, the dependency
# must show up in its RD to be properly added on publication
# and to be removed when the data is removed.
data.rd.addDependency(self.rd, data.rd)
########################## Output field selection
_allSet = set(["ALL"])
def _getFilteredColumns(self, columnSource, queryMeta, verbLevel):
"""filters columns in columnSource according to verbosity and column
set given in queryMeta.
Actually, we only evaluate verbosity and requireSet.
"""
if queryMeta["columnSet"]:
columnSource = [f for f in columnSource
if f.sets==self._allSet or queryMeta["columnSet"]&f.sets]
return [f for f in columnSource
if f.verbLevel<=verbLevel and not f.hidden]
def _getVOTableOutputFields(self, queryMeta):
"""returns a list of OutputFields suitable for a VOTable
response described by queryMeta.
This is the set of all columns in the source table below
the verbosity defined in queryMeta, except that columns
with a displayHint of noxml present are thrown out, too.
When the service sets the votableRespectsOutputTable property to
"True", the column source is the service's output table rather
than the core's one.
"""
verbLevel = queryMeta.get("verbosity")
if verbLevel=="HTML":
# SAMP transfers, typically: Pretend we're HTML
columnSource = self.getHTMLOutputFields(queryMeta)
verbLevel = 100 # column selection done by HTML, just filter noxml below
elif (self.getProperty("votableRespectsOutputTable", "").lower()=="true"
or queryMeta["columnSet"]):
columnSource = self.outputTable
else:
columnSource = self.getAllOutputFields()
fields = self._getFilteredColumns(
[f for f in columnSource
if f.displayHint.get("noxml")!="true"],
queryMeta,
verbLevel)
return rscdef.ColumnList(fields)
[docs] def getHTMLOutputFields(self, queryMeta, ignoreAdditionals=False,
raiseOnUnknown=True):
"""returns a list of OutputFields suitable for an HTML response described
by queryMeta.
This is the service's output table if given, else the core's output
table at verbLevel 2. Additional fields can be set by the user.
raiseOnUnknown is used by customwidgets to avoid exceptions because of
bad additional fields during form construction (when they aren't
properly caught).
"""
if self.outputTable:
columnSource, verbLevel = self.outputTable.columns, 20
else:
columnSource, verbLevel = self.core.outputTable.columns, 20
if queryMeta["verbosity"]!='HTML':
verbLevel = queryMeta["verbosity"]
fields = self._getFilteredColumns(columnSource, queryMeta, verbLevel)
# add user-selected fields
if not ignoreAdditionals and queryMeta["additionalFields"]:
try:
for fieldName in queryMeta["additionalFields"]:
col = self.core.outputTable.getColumnByName(fieldName)
if isinstance(col, outputdef.OutputField):
fields.append(col)
else:
fields.append(outputdef.OutputField.fromColumn(col))
except base.NotFoundError as msg:
if raiseOnUnknown:
raise base.ValidationError("The additional field %s you requested"
" does not exist"%repr(msg.lookedFor), colName="_OUTPUT")
return rscdef.ColumnList(fields)
[docs] def getCurOutputFields(self, queryMeta=None, raiseOnUnknown=True):
"""returns a list of desired output fields for query meta.
This is for both the core and the formatter to figure out the
structure of the tables passed.
"""
queryMeta = queryMeta or common.emptyQueryMeta
# "renderer" in queryMeta needs to be set in the run method below;
# the net effect is that format will come from RESPONSEFORMAT, _FORMAT,
# and the renderer default in sequence. If none of that is available,
# blindly go for default HTML (which in retrospect perhaps wasn't the
# best choice for DaCHS, but changing this now to VOTable would introduce
# needless breakage).
renderer = queryMeta.get("renderer")
format = queryMeta.get("format",
getattr(renderer, "defaultOutputFormat", None)) or "HTML"
if format in self.htmlLikeFormats:
return self.getHTMLOutputFields(queryMeta, raiseOnUnknown=raiseOnUnknown)
else:
return self._getVOTableOutputFields(queryMeta)
[docs] def getAllOutputFields(self):
"""Returns a sequence of all available output fields.
This is what the core gives, and this is what will be declared
to the registry. Depending on the output format, the verbosity
level and perhaps other user settings, the actual columns produced
will be different.
"""
return self.core.outputTable.columns
################### running and input computation.
[docs] def getCoreFor(self, renderer, queryMeta):
"""returns a core tailored for renderer.
See svcs.core's module docstring.
The argument can be a renderer or a renderer name.
"""
if isinstance(renderer, str):
renderer = renderers.getRenderer(renderer)
# non-checked renderers use the core for info purposes only; don't
# bother for those
if not renderer.checkedRenderer:
return self.core
if renderer.name not in self._coresCache:
# Bad Hack: Tell datalink core what renderers are allowed on
# this service
allowedRendsForStealing = self.allowed #noflake: for stealVar downstack
res = self.core.adaptForRenderer(renderer, queryMeta)
# Hack: let the polymorphous datalink core suppress caching
if getattr(res, "nocache", False):
return res
self._coresCache[renderer.name] = res
return self._coresCache[renderer.name]
[docs] def getContextGrammarFor(self, renderer, queryMeta, core=None):
"""returns an ContextGrammar appropriate for this renderer.
Pass in the core if you already have it as an optimisation (in
particular for datalink, where cores aren't automatically cached);
if you don't the core will be computed from the renderer.
In either case, the context grammar simply is built from the core's
inputTable.
"""
if isinstance(renderer, str):
renderer = renderers.getRenderer(renderer)
if core is None:
core = self.getCoreFor(renderer, queryMeta)
serviceKeys = list(inputdef.filterInputKeys(self.serviceKeys,
renderer.name, inputdef.getRendererAdaptor(renderer)))
return MS(inputdef.ContextGrammar,
inputTD=core.inputTable,
inputKeys=serviceKeys)
[docs] def adaptCoreRes(self, coreRes, queryMeta):
"""adapts a core result to the service's output table if appropriate.
Frankly, this was an early mis-design that complicates matters
unnecessarily, but I can't really drop it as long as some interesting
things are based on service's outputTables.
The adaptation works like this:
(0) if coreRes isn't a table, or that table has a noPostprocess
attribute, return coreRes and let the renderer worry about it.
(1) if names and units of newColumns the same as coreRes.columns,
accept coreRes as the new table
(2) if names of newColumns are a subset of coreRes.columns match
but one or more units don't, set up a conversion routine and create
new rows, combining them with newColumns to the result.
(3) else raise an error
(4) eventually, wrap everything up in a rsc.Data instance for
compatibility with cores actually returning data items.
"""
if not isinstance(coreRes, rsc.BaseTable):
return coreRes
newTable = coreRes
if hasattr(coreRes, "noPostprocess"):
colDiffs, swallowColumns = None, None
else:
newColumns = self.getCurOutputFields(queryMeta)
swallowColumns = {c.name for c in coreRes.tableDef
}-{c.name for c in newColumns}
colDiffs = base.computeColumnConversions(
newColumns, coreRes.tableDef.columns)
if colDiffs or swallowColumns:
newTd = coreRes.tableDef.change(columns=newColumns)
newTd.copyMetaFrom(coreRes.tableDef)
if colDiffs:
rmk = rscdef.RowmakerDef(None)
for col in newColumns:
exprStart = ""
if col.name in colDiffs:
exprStart = "%s*"%colDiffs[col.name]
rmk.feedObject("map", rmkdef.MapRule(rmk, dest=col.name,
content_="%svars[%s]"%(exprStart, repr(col.name))
).finishElement(None))
newTable = table.InMemoryTable(newTd, validate=False)
mapper = rmk.finishElement(None).compileForTableDef(newTd)
for r in coreRes:
newTable.addRow(mapper(r, newTable))
else:
newTable = rsc.TableForDef(newTd, rows=coreRes.rows)
newTable._params = coreRes._params
return rsc.wrapTable(newTable, rdSource=coreRes.tableDef)
def _runWithInputTable(self, core, inputTable, queryMeta):
"""runs the core and returns a service result.
This is an internal method.
"""
queryMeta["inputTable"] = inputTable
coreRes = core.run(self, inputTable, queryMeta)
res = self.adaptCoreRes(coreRes, queryMeta)
return res
[docs] def run(self, renderer, args, queryMeta):
"""runs the service, returning a service result.
This is the main entry point for protocol renderers; args is
a dict of lists as provided by request.strargs.
"""
if isinstance(renderer, str):
renderer = renderers.getRenderer(renderer)
core = self.getCoreFor(renderer, queryMeta)
coreArgs = inputdef.CoreArgs.fromRawArgs(
core.inputTable, args, self.getContextGrammarFor(
renderer, queryMeta, core))
queryMeta["renderer"] = renderer
return self._runWithInputTable(core, coreArgs, queryMeta)
#################### meta and such
def _meta_available(self):
# XXX TODO: have this ask the core
return "true"
def _meta_sets(self):
# this is the union of the sets of the publications; this
# is a bit lame, but it's a consequence of our mismatch
# betwenn publications, capabilities, and services.
# In practice, these sets are meaningless where they are
# pulled from meta (everywhere except in service selection
# and in list identifiers, both of which don't use this).
# There's a copy of this in registry.nonservice.
sets = set()
for p in self.publications:
sets |= p.sets
if sets:
return meta.MetaItem.fromSequence(
[meta.MetaValue(s) for s in sets])
else:
return None
[docs] def macro_tablesForTAP(self):
"""returns a list of table names available for TAP querying.
This, really, is an implementation detail for the TAP service and
might go away anytime.
"""
# this is only used by tap.rd -- maybe it
# should go there?
from gavo.protocols import tap
schemas = {}
for qname in tap.getAccessibleTables():
try:
schema, name = qname.split(".")
except: # weird name
continue
schemas.setdefault(schema, []).append(name)
return ", ".join("%s from the %s schema"%(", ".join(tables), schema)
for schema, tables in schemas.items())
def _meta_examplesLink(self):
"""returns a link to a examples for this service if any
are available.
"""
try:
self.getMeta("_example", raiseOnFail=True)
return base.META_CLASSES_FOR_KEYS["_related"](
self.getURL("examples", False),
title="DALI examples")
except base.NoMetaKey:
return None
def _meta_howtociteLink(self):
"""returns a link to a how-to-cite page for this service as an URL
meta.
"""
if self.getMeta("creationDate"):
# depend on creationDate because to filter out totally whacko
# RDs that will never make it to the Registry.
return base.META_CLASSES_FOR_KEYS["_related"](
self.getURL("howtocite", absolute=True),
title="Advice on citing this resource")