Package gavo :: Package svcs :: Module inputdef
[frames] | no frames]

Source Code for Module gavo.svcs.inputdef

  1  """ 
  2  Description and handling of inputs to services. 
  3   
  4  This module in particular describes the InputKey, the primary means 
  5  of describing input widgets and their processing. 
  6   
  7  They are collected in contextGrammars, entities creating inputArgs 
  8  items. 
  9  """ 
 10   
 11  #c Copyright 2008-2019, the GAVO project 
 12  #c 
 13  #c This program is free software, covered by the GNU GPL.  See the 
 14  #c COPYING file in the source distribution. 
 15   
 16   
 17  import itertools 
 18   
 19  from gavo import base 
 20  from gavo import grammars 
 21  from gavo import rscdef 
 22  from gavo import votable 
 23  from gavo import utils 
 24  from gavo.rscdef import column 
 25  from gavo.svcs import dalipars 
 26  from gavo.svcs import pql 
 27  from gavo.svcs import vizierexprs 
 28   
 29  MS = base.makeStruct 
 30   
 31   
 32  _RENDERER_ADAPTORS = { 
 33          'form': vizierexprs.adaptInputKey, 
 34          'pql': pql.adaptInputKey, 
 35          'dali': dalipars.adaptInputKey, 
 36  } 
37 38 -def getRendererAdaptor(renderer):
39 """returns a function that returns input keys adapted for renderer. 40 41 The function returns None if no adapter is necessary. This 42 only takes place for inputKeys within a buildFrom condDesc. 43 """ 44 return _RENDERER_ADAPTORS.get(renderer.parameterStyle)
45
46 47 -class InputKey(column.ParamBase):
48 """A description of a piece of input. 49 50 Think of inputKeys as abstractions for input fields in forms, though 51 they are used for services not actually exposing HTML forms as well. 52 53 Some of the DDL-type attributes (e.g., references) only make sense here 54 if columns are being defined from the InputKey. 55 56 Properties evaluated: 57 58 * defaultForForm -- a value entered into form fields by default 59 (be stingy with those; while it's nice to not have to set things 60 presumably right for almost everyone, having to delete stuff 61 you don't want over and over is really annoying). 62 * adaptToRenderer -- a true boolean literal here causes the param 63 to be adapted for the renderer (e.g., float could become vizierexpr-float). 64 You'll usually not want this, because the expressions are 65 generally evaluated by the database, and the condDescs do the 66 adaptation themselves. This is mainly for rare situations like 67 file uploads in custom cores. 68 * notForRenderer -- a renderer name for which this inputKey is suppressed 69 * onlyForRenderer -- a renderer name for which this inputKey will be 70 preserved; it will be dropped for all others. 71 """ 72 name_ = "inputKey" 73 74 # XXX TODO: make widgetFactory and showItems properties. 75 _widgetFactory = base.UnicodeAttribute("widgetFactory", default=None, 76 description="A python expression for a custom widget" 77 " factory for this input," 78 " e.g., 'Hidden' or 'widgetFactory(TextArea, rows=15, cols=30)'", 79 copyable=True) 80 _showItems = base.IntAttribute("showItems", default=3, 81 description="Number of items to show at one time on selection widgets.", 82 copyable=True) 83 _inputUnit = base.UnicodeAttribute("inputUnit", default=None, 84 description="Override unit of the table column with this.", 85 copyable=True) 86 _std = base.BooleanAttribute("std", default=False, 87 description="Is this input key part of a standard interface for" 88 " registry purposes?", 89 copyable=True) 90 _multiplicity = base.UnicodeAttribute("multiplicity", default=None, 91 copyable=True, 92 description="Set" 93 " this to single to have an atomic value (chosen at random" 94 " if multiple input values are given)," 95 " forced-single to have an atomic value" 96 " and raise an exception if multiple values come in, or" 97 " multiple to receive lists. On the form renderer, this is" 98 " ignored, and the values are what nevow formal passes in." 99 " If not given, it is single unless there is a values element with" 100 " options, in which case it's multiple.") 101 102 # Don't validate meta for these -- while they are children 103 # of validated structures (services), they don't need any 104 # meta at all. This should go as soon as we have a sane 105 # inheritance hierarchy for tables. 106 metaModel = None 107
108 - def completeElement(self, ctx):
109 self._completeElementNext(InputKey, ctx) 110 if self.restrictedMode and self.widgetFactory: 111 raise base.RestrictedElement("widgetFactory") 112 113 if self.multiplicity is None: 114 if self.isEnumerated(): 115 self.multiplicity = "multiple" 116 else: 117 self.multiplicity = "single"
118
119 - def onElementComplete(self):
120 self._onElementCompleteNext(InputKey) 121 # compute scaling if an input unit is given 122 self.scaling = None 123 if self.inputUnit: 124 self.scaling = base.computeConversionFactor(self.inputUnit, self.unit)
125
126 - def onParentComplete(self):
127 if self.parent and hasattr(self.parent, "required"): 128 # children of condDescs inherit their requiredness 129 # (unless defaulted) 130 self.required = self.parent.required 131 # but if there's a default, never require an input 132 if self.value: 133 self.required = False
134
135 - def validateValue(self, literal):
136 """raises a ValidationError if literal cannot be deserialised into 137 an acceptable value for self. 138 """ 139 self._parse(literal)
140
141 - def computeCoreArgValue(self, inputList):
142 """parses some input for this input key. 143 144 This takes into account multiplicities, type conversions, and 145 all the remaining horrors. 146 147 It will return a list or a single value, depending on multiplity. 148 """ 149 if not inputList: 150 if self.value is not None: 151 inputList = [self.value] 152 153 elif self.values and self.values.default: 154 inputList = [self.values.default] 155 156 elif self.required: 157 raise base.ValidationError( 158 "Required parameter %s missing."%self.name, 159 self.name) 160 161 else: 162 return None 163 164 if self.multiplicity=="forced-single" and len(inputList)>1: 165 raise base.MultiplicityError( 166 "Inputs for the parameter %s must not have more than" 167 " one value; hovever, %s was passed in."%( 168 self.name, 169 str(inputList)), 170 colName=self.name) 171 172 if self.multiplicity=="multiple": 173 vals = [v for v in (self._parse(val) for val in inputList) 174 if v is not None] 175 return vals or None 176 177 else: 178 if inputList: 179 return self._parse(inputList[-1]) 180 else: 181 return None
182 183 @classmethod
184 - def fromColumn(cls, column, **kwargs):
185 """returns an InputKey for query input to column. 186 """ 187 if isinstance(column, InputKey): 188 if kwargs: 189 return column.change(**kwargs) 190 else: 191 return column 192 193 instance = cls(None) 194 instance.feedObject("original", column) 195 196 for k,v in kwargs.iteritems(): 197 instance.feed(k, v) 198 if not "required" in kwargs: 199 instance.feedObject("required", False) 200 instance.dmRoles = rscdef.OldRoles(column.dmRoles) 201 return instance.finishElement(None)
202
203 204 -def filterInputKeys(keys, rendName, adaptor=None):
205 """filters inputKeys in key, only returning those compatible with 206 rendName. 207 208 adaptor is is a function taking and returning an inputKey that is used 209 for input keys with an adaptToRenderer property. 210 """ 211 for key in keys: 212 if key.getProperty("onlyForRenderer", None) is not None: 213 if key.getProperty("onlyForRenderer")!=rendName: 214 continue 215 if key.getProperty("notForRenderer", None) is not None: 216 if key.getProperty("notForRenderer")==rendName: 217 continue 218 219 if base.parseBooleanLiteral(key.getProperty("adaptToRenderer", "False") 220 ) and adaptor: 221 key = adaptor(key) 222 223 yield key
224
225 226 -class InputTD(base.Structure, base.StandardMacroMixin):
227 """an input for a core. 228 229 These aren't actually proper tables but actually just collection of 230 the param-like inputKeys. They serve as input declarations for cores 231 and services (where services derive their inputTDs from the cores' ones by 232 adapting them to the current renderer. Their main use is for the derivation 233 of contextGrammars. 234 235 They can carry metadata, though, which is sometimes convenient when 236 transporting information from the parameter parsers to the core. 237 238 For the typical dbCores (and friends), these are essentially never 239 explicitly defined but rather derived from condDescs. 240 241 Do *not* read input values by using table.getParam. This will only 242 give you one value when a parameter has been given multiple times. 243 Instead, use the output of the contextGrammar (inputParams in condDescs). 244 Only there you will have the correct multiplicities. 245 """ 246 name_ = "inputTable" 247 248 _inputKeys = rscdef.ColumnListAttribute("inputKeys", 249 childFactory=InputKey, 250 description='Input parameters for this table.', 251 copyable=True) 252 253 _groups = base.StructListAttribute("groups", 254 childFactory=rscdef.Group, 255 description="Groups of inputKeys (this is used for form UI formatting).", 256 copyable=True) 257 258 _exclusive = base.BooleanAttribute("exclusive", 259 description="If true, context grammars built from this will" 260 " raise an error if contexts passed in have keys not defined" 261 " by this table", 262 default=False, 263 copyable=True) 264 _rd = rscdef.RDAttribute() 265
266 - def __iter__(self):
267 return iter(self.inputKeys)
268
269 - def adaptForRenderer(self, renderer):
270 """returns an inputTD tailored for renderer. 271 272 This is discussed in svcs.core's module docstring. 273 """ 274 newKeys = list( 275 filterInputKeys(self.inputKeys, renderer.name, 276 getRendererAdaptor(renderer))) 277 278 if newKeys!=self.inputKeys: 279 return self.change(inputKeys=newKeys, parent_=self) 280 else: 281 return self
282
283 - def resolveName(self, ctx, name):
284 """returns a column name from a queried table of the embedding core, 285 if available. 286 287 This is a convenicence hack that lets people reference columns from 288 a TableBasedCore by their simple, non-qualified names. 289 """ 290 if self.parent and hasattr(self.parent, "queriedTable"): 291 return self.parent.queriedTable.resolveName(ctx, name) 292 raise base.NotFoundError(id, "Element with id or name", "name path", 293 hint="There is not queried table this name could be resolved in.")
294
295 296 -class CoreArgs(base.MetaMixin):
297 """A container for core arguments. 298 299 There's inputTD, which reference the renderer-adapted input table, 300 and args, the ContextGrammar processed input. For kicks, we also 301 have rawArgs, which is the contextGrammar's input (if you find 302 you're using it, tell us; that's pointing to a problem on our side). 303 304 getParam(name) -> value and getParamDict() -> dict methods are 305 present for backward compatibility. 306 """
307 - def __init__(self, inputTD, args, rawArgs):
308 self.inputTD, self.args, self.rawArgs = inputTD, args, rawArgs 309 base.MetaMixin.__init__(self)
310
311 - def getParam(self, name):
312 return self.args.get(name)
313
314 - def getParamDict(self):
315 return self.args
316 317 @classmethod
318 - def fromRawArgs(cls, inputTD, rawArgs, contextGrammar=None):
319 """returns a CoreArgs instance built from an inputDD and 320 ContextGrammar-parseable rawArgs. 321 322 contextGrammar can be overridden, e.g., to cache or to add 323 extra, non-core keys. 324 """ 325 if contextGrammar is None: 326 contextGrammar = MS(ContextGrammar, inputTD=inputTD) 327 ri = contextGrammar.parse(rawArgs) 328 _ = list(ri) #noflake: ignored value 329 return cls(inputTD, ri.getParameters(), rawArgs)
330
331 332 -class ContextRowIterator(grammars.RowIterator):
333 """is a row iterator over "contexts", i.e. single dictionary-like objects. 334 335 The source token expected here can be a request.args style dictionary 336 with lists of strings as arguments, or a parsed dictionary from nevow 337 formal. Non-list literals in the latter are packed into a list to ensure 338 consistent behaviour. 339 """
340 - def __init__(self, grammar, sourceToken, **kwargs):
341 self.locator = "(internal)" 342 grammars.RowIterator.__init__(self, grammar, 343 utils.CaseSemisensitiveDict(sourceToken), 344 **kwargs)
345
346 - def _ensureListValues(self):
347 """this turns lonely values in the sourceToken into lists so we 348 don't have to special-case nevow formal dicts. 349 350 Do not rely on this any more; it's deeply broken (which becomes 351 obvious when we actually have array-valued input) and it's scheduled 352 for removal. 353 """ 354 for key, value in self.sourceToken.iteritems(): 355 if value is None: 356 self.sourceToken[key] = [] 357 else: 358 if not isinstance(value, list): 359 base.ui.notifyWarning("Non-list input to ContextGrammar: %s"%key) 360 self.sourceToken[key] = [value]
361
362 - def _ensureNoExtras(self):
363 """makes sure sourceToken has no keys not mentioned in the grammar. 364 365 Here, we assume case-insensitive arguments for now. If we ever 366 want to change that... oh, my. 367 368 This is only exectued for grammars with rejectExtras. 369 """ 370 inKeys = set(n.lower() for n in self.sourceToken) 371 expectedKeys = set(k.name.lower() for k in self.grammar.iterInputKeys()) 372 if inKeys-expectedKeys: 373 raise base.ValidationError("The following parameter(s) are" 374 " not accepted by this service: %s"%",".join( 375 sorted(inKeys-expectedKeys)), 376 "(various)")
377
378 - def _iterRows(self):
379 # we don't really return any rows, but this is where our result 380 # dictionary is built. 381 self._ensureListValues() 382 if self.grammar.rejectExtras: 383 self._ensureNoExtras() 384 385 self.coreArgs = {} 386 387 for ik in self.grammar.iterInputKeys(): 388 self.locator = "param %s"%ik.name 389 try: 390 self.coreArgs[ik.name] = ik.computeCoreArgValue( 391 self.sourceToken.get(ik.name)) 392 except votable.BadVOTableLiteral as ex: 393 raise base.ValidationError("Bad parameter literal %s (%s)"%( 394 self.sourceToken.get(ik.name), ex), 395 ik.name) 396 397 if False: 398 yield {} # contextGrammars yield no rows.
399
400 - def getParameters(self):
401 return self.coreArgs
402
403 - def getLocator(self):
404 return self.locator
405
406 407 -class ContextGrammar(grammars.Grammar):
408 """A grammar for web inputs. 409 410 The source tokens for context grammars are dictionaries; these 411 are either typed dictionaries from nevow formal, where the values 412 usually are atomic, or, preferably, the dictionaries of lists 413 from request.args. 414 415 ContextGrammars never yield rows, so they're probably fairly useless 416 in normal cirumstances. 417 418 In normal usage, they just yield a single parameter row, 419 corresponding to the source dictionary possibly completed with 420 defaults, where non-requried input keys get None defaults where not 421 given. Missing required parameters yield errors. 422 423 This parameter row honors the multiplicity specification, i.e., single or 424 forced-single are just values, multiple are lists. The content are 425 *parsed* values (using the InputKeys' parsers). 426 427 Since most VO protocols require case-insensitive matching of parameter 428 names, matching of input key names and the keys of the input dictionary 429 is attempted first literally, then disregarding case. 430 """ 431 name_ = "contextGrammar" 432 433 _inputTD = base.ReferenceAttribute("inputTD", 434 default=base.NotGiven, 435 description="The input table from which to take the input keys", 436 copyable=True) 437 438 _inputKeys = base.StructListAttribute("inputKeys", 439 childFactory=InputKey, 440 description="Extra input keys not defined in the inputTD. This" 441 " is used when services want extra input processed by them rather" 442 " than their core.", 443 copyable=True) 444 445 _original = base.OriginalAttribute("original") 446 447 rowIterator = ContextRowIterator 448 449 rejectExtras = False 450
451 - def onElementComplete(self):
452 self._onElementCompleteNext(ContextGrammar) 453 self.rejectExtras = self.inputTD.exclusive
454
455 - def iterInputKeys(self):
456 return itertools.chain(iter(self.inputTD), iter(self.inputKeys))
457 458 459 _OPTIONS_FOR_MULTIS = { 460 "forced-single": ", single=True, forceUnique=True", 461 "single": ", single=True", 462 "multiple": "", 463 } 464