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
22
23
24
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
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 """
58 if factories is None:
59 self.factories = []
60 else:
61 self.factories = factories[:]
62
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
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
79 self.factories.insert(0, factory)
80
82 self.factories.append(factory)
83
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
108
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
134 if colDesc["dbtype"]=="bytea":
135
136 def _(val):
137 return str(val)
138 return _
139 registerDefaultMF(_byteaMapperFactory)
140
141
142 GEOMETRY_ARRAY_TYPES = set(["spoint", "spoly", "scircle", "sbox"])
143
145 """A factory for functions turning pgsphere types to DALI arrays.
146 """
147
148
149
150
151
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
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
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
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
216
217
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):
271
273 type, size, xtype = typeconversions.sqltypeToVOTable(self.original.type)
274
275
276
277
278
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
299 "id": None,
300 "ref": None,
301 }
302
305
308
311
312 - def get(self, key, default=None):
314