Package gavo :: Package utils :: Module serializers
[frames] | no frames]

Source Code for Module gavo.utils.serializers

  1  """ 
  2  A framework for pluggable serialisation of (python) values. 
  3   
  4  This module collects a set of basic (looking primarily towards 
  5  VOTables) serialiser factories.  These are just functions receiving 
  6  AnnotatedColumn objects and returning either None ("not responsible") 
  7  or a function taking a value and returning a string.  They may change 
  8  the AnnotatedColumn objects, for instance, when an MJD (float) 
  9  becomes a datetime. 
 10   
 11  These factories are registered in ValueMapperFactoryRegistry classes; 
 12  the one used for "normal" VOTables is the defaultMFRegistry. 
 13   
 14  Most factories are created here.  However, some depend on advance  
 15  functionality not available here; they will be registered on import of the 
 16  respective modules (for instance, stc). 
 17   
 18  In DaCHS, a second such factory registry is created in web.htmltable. 
 19  """ 
 20   
 21  #c Copyright 2008-2019, the GAVO project 
 22  #c 
 23  #c This program is free software, covered by the GNU GPL.  See the 
 24  #c COPYING file in the source distribution. 
 25   
 26   
 27  import datetime 
 28  import re 
 29   
 30  from gavo.utils import algotricks 
 31  from gavo.utils import typeconversions 
 32   
 33  __docformat__ = "restructuredtext en" 
 34   
 35   
36 -class ValueMapperFactoryRegistry(object):
37 """An object clients can ask for functions fixing up values 38 for encoding. 39 40 A mapper factory is just a function that takes an AnnotatedColumn instance. 41 It must return either None (for "I don't know how to make a function for this 42 combination these column properties") or a callable that takes a value 43 of the given type and returns a mapped value. 44 45 To add a mapper, call registerFactory. To find a mapper for a 46 set of column properties, call getMapper -- column properties should 47 be an instance of AnnotatedColumn, but for now a dictionary with the 48 right keys should mostly do. 49 50 Mapper factories are tried in the reverse order of registration, 51 and the first that returns non-None wins, i.e., you should 52 register more general factories first. If no registred mapper declares 53 itself responsible, getMapper returns an identity function. If 54 you want to catch such a situation, you can use somthing like 55 res = vmfr.getMapper(...); if res is utils.identity ... 56 """
57 - def __init__(self, factories=None):
58 if factories is None: 59 self.factories = [] 60 else: 61 self.factories = factories[:]
62
63 - def clone(self):
64 """returns a clone of the factory. 65 66 This is a copy, i.e., factories added will not change the original. 67 """ 68 return self.__class__(self.factories)
69
70 - def getFactories(self):
71 """returns the list of factories. 72 73 This is *not* a copy. It may be manipulated to remove or add 74 factories. 75 """ 76 return self.factories
77
78 - def registerFactory(self, factory):
79 self.factories.insert(0, factory)
80
81 - def appendFactory(self, factory):
82 self.factories.append(factory)
83
84 - def getMapper(self, colDesc):
85 """returns a mapper for values with the python value instance, 86 according to colDesc. 87 88 This method may change colDesc. 89 90 We do a linear search here, so you shouldn't call this function too 91 frequently. 92 """ 93 for factory in self.factories: 94 mapper = factory(colDesc) 95 if mapper: 96 colDesc["winningFactory"] = factory 97 break 98 else: 99 mapper = algotricks.identity 100 return mapper
101 102 103 defaultMFRegistry = ValueMapperFactoryRegistry() 104 registerDefaultMF = defaultMFRegistry.registerFactory 105 106
107 -def _timeMapperFactory(annCol):
108 # XXX TODO: Unify with analogous code in web.htmltable 109 if (annCol["dbtype"]=="time" 110 or annCol["displayHint"].get("type")=="humanTime"): 111 sf = int(annCol["displayHint"].get("sf", 0)) 112 fmtStr = "%%02d:%%02d:%%0%d.%df"%(sf+3, sf) 113 114 def mapper(val): 115 if val is None: 116 return val 117 elif isinstance(val, (datetime.time, datetime.datetime)): 118 res = fmtStr%(val.hour, val.minute, val.second) 119 elif isinstance(val, datetime.timedelta): 120 hours = val.seconds//3600 121 minutes = (val.seconds-hours*3600)//60 122 seconds = (val.seconds-hours*3600-minutes*60)+val.microseconds/1e6 123 res = fmtStr%(hours, minutes, seconds) 124 else: 125 return val 126 annCol["datatype"], annCol["arraysize"] = "char", "*" 127 return res
128 129 return mapper 130 registerDefaultMF(_timeMapperFactory) 131 132
133 -def _byteaMapperFactory(colDesc):
134 if colDesc["dbtype"]=="bytea": 135 # psycopg2 here returns buffers which are painful in some situations. 136 def _(val): 137 return str(val)
138 return _ 139 registerDefaultMF(_byteaMapperFactory) 140 141 142 GEOMETRY_ARRAY_TYPES = set(["spoint", "spoly", "scircle", "sbox"]) 143
144 -def _pgSphereMapperFactory(colDesc):
145 """A factory for functions turning pgsphere types to DALI arrays. 146 """ 147 # even though the VOTable library can and will do this natively, 148 # we still map this manually right now in order to help non-VOTable 149 # output formats; this will not work (properly) when the output 150 # format doesn't have array support. For these, just create 151 # a new mapper factory for GEOMETRY_ARRAY_TYPES. 152 if not colDesc["dbtype"] in GEOMETRY_ARRAY_TYPES: 153 return 154 155 def mapper(val): 156 if val is None: 157 return None 158 return val.asDALI()
159 160 colDesc["datatype"], colDesc["arraysize"], colDesc["xtype" 161 ] = typeconversions.sqltypeToVOTable(colDesc["dbtype"]) 162 163 return mapper 164 registerDefaultMF(_pgSphereMapperFactory) 165 166
167 -def _legacyGeometryMapperFactory(colDesc):
168 """A factory to support TAP 1.0-style (STC-S) geometry maps. 169 170 These are requested through the semi-custom, legacy adql:REGION xtype. 171 """ 172 if not colDesc["xtype"] in ["adql:REGION", "adql:POINT"]: 173 return 174 175 systemString = None 176 if colDesc.original.stc: 177 systemString = colDesc.original.stc.astroSystem.spaceFrame.refFrame 178 if systemString is None: 179 systemString = "UNKNOWNFrame" 180 181 def mapper(val): 182 if val is None: 183 return None 184 return val.asSTCS(systemString)
185 186 colDesc["datatype"], colDesc["arraysize"] = "char", "*" 187 return mapper 188 registerDefaultMF(_legacyGeometryMapperFactory) 189 190
191 -def _castMapperFactory(colDesc):
192 """is a factory that picks up castFunctions set up by user casts. 193 """ 194 if "castFunction" in colDesc: 195 return colDesc["castFunction"]
196 registerDefaultMF(_castMapperFactory) 197 198
199 -def _htmlScrubMapperFactory(colDesc):
200 if colDesc["displayHint"].get("type")!="keephtml": 201 return 202 tagPat = re.compile("<[^>]*>") 203 def coder(data): 204 if data: 205 return tagPat.sub("", data) 206 return ""
207 return coder 208 registerDefaultMF(_htmlScrubMapperFactory) 209 210
211 -def getMapperRegistry():
212 """returns a copy of the default value mapper registry. 213 """ 214 return ValueMapperFactoryRegistry( 215 defaultMFRegistry.getFactories())
216 217
218 -class AnnotatedColumn(object):
219 """A collection of annotations for a column. 220 221 ColumnAnntotations are constructed with columns and retain a 222 reference to them ("original"). 223 224 In addition, they provide a getitem/setitem interface to a 225 dictionary that contains "digested" information on the column. 226 This dictionary serves as an accumulator for information useful 227 during the serialization process. 228 229 The main reason for this class is that Columns are supposed to be 230 immutable; thus, any ephemeral information needs to be kept in a 231 different place. In particular, the mapper factories receive such 232 annotations. 233 234 As a special service to coerce internal tables to external standards, 235 you can pass a votCast dictionary to AnnotatedColumn. Give any 236 key/value pairs in there to override what AnnotatedColumn guesses 237 or infers. This is used to force the sometimes a bit funky 238 SCS/SIAP types to standard values. 239 240 The castMapperFactory enabled by default checks for the presence of 241 a castFunction in an AnnotatedColumn. If it is there, it will be used 242 for mapping the values, so this is another thing you can have in votCast. 243 244 The SerManager tries to obtain votCasts from a such-named 245 attribute on the table passed in. 246 247 Though of course clients can access original, the mapping facets should 248 only be accessed through getitem/setitem since they may be updated 249 wrt what is in original. 250 251 Attributes available via the setitem/getitem interface include: 252 253 - nullvalue -- a suitable nullvalue for this column, if provided by the 254 column's values or otherwise obtained 255 - name -- a name for the column 256 - dbtype -- the column's database type 257 - xtype -- the column's xtype (e.g., "timestamp") 258 - datatype, arraysize -- a VOTable type for the column 259 - displayHint -- a parsed display hint 260 - note -- a reference to a table not (these get entered by SerManager) 261 - ucd, utype, unit, description -- as for column 262 - id -- a string suitable as XML id (externally managed) 263 - votablewrite would evaluate min and max (but right now nothing adds 264 this) 265 """
266 - def __init__(self, column, votCast=None):
267 self.original = column 268 self._initAnnotation() 269 if votCast is not None: 270 self.annotations.update(votCast)
271
272 - def _initAnnotation(self):
273 type, size, xtype = typeconversions.sqltypeToVOTable(self.original.type) 274 275 # the interval xtype clashes with other xtypes (right now, in particular 276 # timestamp; the others aren't orderable). Hence, we need to 277 # be clever with the computation of the new xtype (analogous 278 # code in rscdef.column; we should fix that) 279 if self.original.xtype=="interval": 280 xtype = xtype or self.original.xtype 281 else: 282 xtype = self.original.xtype or xtype 283 284 self.annotations = { 285 "nullvalue": self.original.values and 286 self.original.values.nullLiteral, 287 "name": self.original.key, 288 "dbtype": self.original.type, 289 "xtype": xtype, 290 "datatype": type, 291 "arraysize": size, 292 "displayHint": self.original.displayHint, 293 "note": None, 294 "ucd": self.original.ucd, 295 "utype": self.original.utype, 296 "unit": self.original.unit, 297 "description": self.original.description, 298 # id is managed by SerManager 299 "id": None, 300 "ref": None, # used for legacy COOSYS only 301 }
302
303 - def __getitem__(self, key):
304 return self.annotations[key]
305
306 - def __setitem__(self, key, value):
307 self.annotations[key] = value
308
309 - def __contains__(self, key):
310 return key in self.annotations
311
312 - def get(self, key, default=None):
313 return self.annotations.get(key, default)
314