Source code for gavo.svcs.service

"""
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 PreparsedInput(dict): """a sentinel class signalling to the service that its input already is parsed. This is for for stuff coming from nevow formal rather than request.strargs, and to be fed into service.run. Construct with a dictionary. """
[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 getInputKeysFor(self, renderer, queryMeta=common.emptyQueryMeta): """returns a sequence of input keys, adapted for renderer. The renderer argument may either be a renderer name, a renderer class or a renderer instance. This is the main interface for external entities to discover. service metadata. """ if isinstance(renderer, str): renderer = renderers.getRenderer(renderer) return list(self.getContextGrammarFor( renderer, queryMeta).iterInputKeys())
[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")