1 """
2 Generating VOTables from internal data representations.
3
4 This is glue code to the more generic GAVO votable library. In particular,
5 it governs the application of base.SerManagers and their column descriptions
6 (which are what is passed around as colDescs in this module to come up with
7 VOTable FIELDs and the corresponding values.
8
9 You should access this module through formats.votable.
10 """
11
12
13
14
15
16
17
18 import contextlib
19 import functools
20 import itertools
21 from cStringIO import StringIO
22 import warnings
23
24 from gavo import base
25 from gavo import dm
26 from gavo import rsc
27 from gavo import stc
28 from gavo import utils
29 from gavo import votable
30 from gavo.base import meta
31 from gavo.base import valuemappers
32 from gavo.formats import common
33 from gavo.votable import V
34 from gavo.votable import modelgroups
35
36
37 -class Error(base.Error):
39
40
41 tableEncoders = {
42 "td": V.TABLEDATA,
43 "binary": V.BINARY,
44 "binary2": V.BINARY2,
45 }
46
47
48 -class VOTableContext(utils.IdManagerMixin):
49 """A context object for writing VOTables.
50
51 The constructor arguments work as keyword arguments to ``getAsVOTable``.
52 Some other high-level functions accept finished contexts.
53
54 This class provides management for unique ID attributes, the value mapper
55 registry, and possibly additional services for writing VOTables.
56
57 VOTableContexts optionally take
58
59 - a value mapper registry (by default, valuemappers.defaultMFRegistry)
60 - the tablecoding (currently, td, binary, or binary2)
61 - version=(1,1) to order a 1.1-version VOTable, (1,2) for 1.2.
62 (default is now 1.3).
63 - acquireSamples=False to suppress reading some rows to get
64 samples for each column
65 - suppressNamespace=False to leave out a namespace declaration
66 (mostly convenient for debugging)
67 - overflowElement (see votable.tablewriter.OverflowElement)
68
69 There's also an attribute produceVODML that will automatically be
70 set for VOTable 1.4; you can set it to true manually, but the
71 resulting VOTables will probably be invalid.
72
73 If VO-DML processing is enabled, the context also manages models declared;
74 that's the modelsUsed dictionary, mapping prefix -> dm.Model instances
75 """
76 - def __init__(self, mfRegistry=valuemappers.defaultMFRegistry,
77 tablecoding='binary', version=None, acquireSamples=True,
78 suppressNamespace=False, overflowElement=None):
79 self.mfRegistry = mfRegistry
80 self.tablecoding = tablecoding
81 self.version = version or (1,3)
82 self.acquireSamples = acquireSamples
83 self.suppressNamespace = suppressNamespace
84 self.overflowElement = overflowElement
85 self._containerStack = []
86 self._tableStack = []
87 self._pushedRefs = {}
88
89
90 self.produceVODML = self.version[0]>1 or self.version[1]>3
91
92
93
94 self.groupIdsInTree = set()
95 self.modelsUsed = {}
96 self.rootVODML = V.VODML()
97 self.vodmlTemplates = V.TEMPLATES()
98 self.rootVODML[self.vodmlTemplates]
99
100 - def addVODMLPrefix(self, prefix):
101 """arranges the DM with prefix to be included in modelsUsed.
102 """
103 if prefix not in self.modelsUsed:
104 self.modelsUsed[prefix] = dm.getModelForPrefix(prefix)
105
106 - def addVODMLMaterial(self, stuff):
107 """adds VODML annotation to this VOTable.
108
109 Note that it will only be rendered if produceVODML is true
110 (in general, for target versions >1.3).
111 """
112 self.vodmlTemplates[stuff]
113
114 - def makeTable(self, table):
115 """returns xmlstan for a table.
116
117 This is exposed as a method of context as the dm subpackage
118 needs it, but I don't want to import formats there (yet).
119
120 This may go away as I fix the interdependence of dm, votable, and
121 format.
122 """
123 return makeTable(self, table)
124
126 """returns the xmlstan element of the table currently built.
127
128 This returns a ValueError if the context isn't aware of a table
129 being built.
130
131 (This depends builders using activeContainer)
132 """
133 for el in reversed(self._containerStack):
134 if el.name_=="TABLE":
135 return el
136 raise ValueError("Not currently building a table.")
137
139 """returns the xmlstan element of the resource currently built.
140
141 This returns a ValueError if the context isn't aware of a resource
142 being built.
143
144 (This depends builders using activeContainer)
145 """
146 for el in reversed(self._containerStack):
147 if el.name_=="RESOURCE":
148 return el
149 raise ValueError("Not currently building a table.")
150
152 """returns the innermost container element the builders have declared.
153 """
154 return self._containerStack[-1]
155
156 @property
157 - def currentTable(self):
158 """the DaCHS table object from which things are currently built.
159
160 If no builder has declared a table being built (using buildingFromTable),
161 it's a value error.
162 """
163 if not self._tableStack:
164 raise ValueError("No table being processed.")
165 return self._tableStack[-1]
166
167 @contextlib.contextmanager
168 - def activeContainer(self, container):
169 """a context manager to be called by VOTable builders when
170 they open a new TABLE or RESOURCE.
171 """
172 self._containerStack.append(container)
173 try:
174 yield
175 finally:
176 self._containerStack.pop()
177
178 @contextlib.contextmanager
179 - def buildingFromTable(self, table):
180 """a context manager to control code that works on a DaCHS table.
181 """
182 self._tableStack.append(table)
183 try:
184 yield
185 finally:
186 self._tableStack.pop()
187
188 - def pushRefFor(self, rdEl, refVal):
189 """orders refVal to be set as ref on rdEl's VOTable representation
190 if such a thing is being serialised.
191
192 This currently is more a hack for PARAMs with COOSYS than something
193 that should be really used; if this were to become a general pattern,
194 we should work out a way to assign the ref if rdEl's representation
195 already is in the tree...
196 """
197 self._pushedRefs[id(rdEl)] = refVal
198
199 - def addID(self, rdEl, votEl):
200 """adds an ID attribute to votEl if rdEl has an id managed by self.
201
202 Also, if a ref has been noted for rdEl, a ref attribute is being
203 added, too. This is a special hack for params and coosys; and I suspect
204 we shouldn't go beyond that.
205 """
206 try:
207 votEl.ID = self.getIdFor(rdEl)
208 except base.NotFoundError:
209
210 pass
211
212 if id(rdEl) in self._pushedRefs:
213 votEl.ref = self._pushedRefs[id(rdEl)]
214
215 return votEl
216
221 """returns a sequence of V.INFO items from the info meta of dataSet.
222 """
223 for infoItem in dataSet.getMeta("info", default=[]):
224 name, value, id = infoItem.infoName, infoItem.infoValue, infoItem.infoId
225 yield V.INFO(name=name, value=value, ID=id)[infoItem.getContent()]
226
235
238 """yields RESOURCE elements for datalink services defined for tables
239 we have.
240
241 This needs to be called before the tables are serialised because we
242 put ids on fields.
243 """
244 for table in dataSet.tables.values():
245 for svcMeta in table.iterMeta("_associatedDatalinkService"):
246 try:
247 service = base.resolveId(table.tableDef.rd,
248 base.getMetaText(svcMeta, "serviceId"))
249
250
251
252
253
254 from gavo.protocols import datalink
255 yield datalink.makeDatalinkServiceDescriptor(
256 ctx, service, table.tableDef,
257 base.getMetaText(svcMeta, "idColumn"))
258
259
260
261
262
263
264 while getattr(dataSet, "sodaGenerators", []):
265 yield dataSet.sodaGenerators.pop()(ctx)
266
267 except Exception, ex:
268 base.ui.notifyWarning("RD %s: request for datalink service"
269 " could not be satisfied (%s)"%(
270 getattr(table.tableDef.rd, "sourceId", "<internal>"),
271 ex))
272
314
328
337
338
339 _linkBuilder = meta.ModelBasedBuilder([
340 ('votlink', _makeLinkForMeta, (), {
341 "href": "href",
342 "content_role": "role",
343 "content_type": "contentType",
344 "name": "linkname",})])
350 """returns a VALUES element for a column description.
351
352 This just stringifies whatever is in colDesc's respective columns,
353 so for anything fancy pass in byte strings to begin with.
354 """
355 valEl = V.VALUES()
356 if colDesc.get("min") is None:
357 colDesc["min"] = getattr(colDesc.original.values, "min", None)
358 if colDesc.get("max") is None:
359 colDesc["max"] = getattr(colDesc.original.values, "max", None)
360
361 if colDesc["max"] is utils.Infimum:
362 colDesc["max"] = None
363 if colDesc["min"] is utils.Supremum:
364 colDesc["min"] = None
365
366 if colDesc["min"] is not None:
367 valEl[V.MIN(value=str(colDesc["min"]))]
368 if colDesc["max"] is not None:
369 valEl[V.MAX(value=str(colDesc["max"]))]
370 if colDesc["nullvalue"] is not None:
371 valEl(null=colDesc["nullvalue"])
372
373 for option in getattr(colDesc.original.values, "options", []):
374 valEl[V.OPTION(value=option.content_ or "", name=option.title)]
375
376 return valEl
377
378
379
380 _voFieldCopyKeys = ["name", "datatype", "ucd", "utype", "ref"]
383 """adds attributes and children to element from colDesc.
384
385 element can be a V.FIELD or a V.PARAM *instance* and is changed in place.
386
387 This function returns None to remind people we're changing in place
388 here.
389 """
390
391
392
393 assert not isinstance(element, type), ("Got FIELD/PARAM element"
394 " instead of instance in VOTable defineField")
395
396 if colDesc["arraysize"]!='1':
397 element(arraysize=colDesc["arraysize"])
398
399 if colDesc["datatype"]=='char' and colDesc["arraysize"]=='1':
400 element(arraysize='1')
401
402 if colDesc["unit"]:
403 element(unit=colDesc["unit"])
404 element(ID=colDesc["id"])
405
406
407 xtype = colDesc.get("xtype")
408 if ctx.version>(1,1):
409 element(xtype=xtype)
410
411 if isinstance(element, V.PARAM):
412 if hasattr(colDesc.original, "getStringValue"):
413 try:
414 element(value=str(colDesc.original.getStringValue()))
415 except:
416
417 pass
418
419 if colDesc.original:
420 rscCol = colDesc.original
421 if rscCol.hasProperty("targetType"):
422 element[V.LINK(
423 content_type=rscCol.getProperty("targetType"),
424 title=rscCol.getProperty("targetTitle", "Link"))]
425
426 element(**dict((key, colDesc.get(key) or None)
427 for key in _voFieldCopyKeys))[
428 V.DESCRIPTION[colDesc["description"]],
429 _makeValuesForColDesc(colDesc),
430 _linkBuilder.build(colDesc.original)
431 ]
432
435 """returns a VOTable colType for a rscdef column-type thing.
436
437 This function lets you make PARAM and FIELD elements (colType) from
438 column or param instances.
439 """
440 instance = colType()
441 defineField(ctx, instance, valuemappers.AnnotatedColumn(rscCol))
442 return instance
443
446 """iterates over V.FIELDs based on serManger's columns.
447 """
448 for colDesc in serManager:
449 el = V.FIELD()
450 defineField(ctx, el, colDesc)
451 yield el
452
472
475 """iterates over V.PARAMs based on the table's param elements.
476 """
477 for param in serManager.table.iterParams():
478 votEl = _makeVOTParam(ctx, param)
479 if votEl is not None:
480 ctx.addID(param, votEl)
481 yield votEl
482
485 """iterates over the entries in the parameters table of dataSet.
486 """
487
488
489
490
491 try:
492 parTable = dataSet.getTableWithRole("parameters")
493 except base.DataError:
494 return
495
496 warnings.warn("Parameters table used. You shouldn't do that any more.")
497 values = {}
498 if parTable:
499 values = parTable.rows[0]
500
501 for item in parTable.tableDef:
502 colDesc = valuemappers.AnnotatedColumn(item)
503 el = V.PARAM()
504 el(value=ctx.mfRegistry.getMapper(colDesc)(values.get(item.name)))
505 defineField(ctx, el, colDesc)
506 ctx.addID(el, item)
507 yield el
508
509
510
511
512
513
514 STC_FRAMES_TO_COOSYS = {
515 'ICRS': 'ICRS',
516 'FK5': 'eq_FK5',
517 'FK4': 'eq_FK4',
518 'ECLIPTIC': 'ecl_FK5',
519 'GALACTIC_II': 'galactic',
520 'SUPERGALACTIC': 'supergalactic'}
521
522
523 COLUMN_REF_UTYPES = [
524 "stc:astrocoords.position2d.value2.c1",
525 "stc:astrocoords.position2d.value2.c2",
526 "stc:astrocoords.position2d.error2.c1",
527 "stc:astrocoords.position2d.error2.c2",
528 "stc:astrocoords.velocity2d.value2.c1",
529 "stc:astrocoords.velocity2d.value2.c2",
530 "stc:astrocoords.velocity2d.error2.c1",
531 "stc:astrocoords.velocity2d.error2.c2",
532 "stc:astrocoords.redshift.value",
533 "stc:astrocoords.position3d.value3.c1",
534 "stc:astrocoords.position3d.value3.c2",
535 "stc:astrocoords.position3d.value3.c3",
536 "stc:astrocoords.time.timeinstant",
537 "stc:astrocoords.velocity3d.value3.c1",
538 "stc:astrocoords.velocity3d.value3.c2",
539 "stc:astrocoords.velocity3d.value3.c3",
540 "stc:astrocoords.position2d.epoch",
541 ]
545 """returns a VOTable 1.1 COOSYS element inferred from a map of stc utypes
546 to STC1 values.
547
548 We let through coordinate frames not defined in VOTable 1.1 at the
549 expense of making the VOTables XSD-invalid with such frames; this
550 seems preferable to not declaring anything.
551
552 As a side effect, this will change column/@ref attributes (possibly also
553 param/@ref). If a column is part of two STC structures, the first
554 one will win. Yeah, that spec sucks.
555 """
556 coosys = V.COOSYS()
557 sysId = serManager.makeIdFor(coosys, "system")
558 coosys(ID=sysId)
559
560 if "stc:astrocoords.position2d.epoch" in utypeMap:
561 epVal = utypeMap["stc:astrocoords.position2d.epoch"]
562 if not isinstance(epVal, stc.ColRef):
563
564
565 coosys(epoch="%s%s"%(
566 utypeMap.get("stc:astrocoords.position2d.epoch.yeardef", "J"),
567 epVal))
568
569 stcFrame = utypeMap.get(
570 "stc:astrocoordsystem.spaceframe.coordrefframe", None)
571 coosys(system=STC_FRAMES_TO_COOSYS.get(stcFrame, stcFrame))
572
573 for utype in COLUMN_REF_UTYPES:
574 if utype in utypeMap:
575 val = utypeMap[utype]
576 if isinstance(val, stc.ColRef):
577 col = serManager.getColumnByName(str(val))
578 if not col.get("ref"):
579 col["ref"] = sysId
580
581 return coosys
582
583
584 -def _iterSTC(ctx, tableDef, serManager):
585 """adds STC groups for the systems to votTable fetching data from
586 tableDef.
587 """
588 def getColumnFor(colRef):
589 try:
590 return serManager.getColumnByName(colRef.dest)
591 except KeyError:
592
593
594
595
596 return serManager.getColumnByName(colRef.dest.lower())
597
598 def getIdFor(colRef):
599 return getColumnFor(colRef)["id"]
600
601 for ast in tableDef.getSTCDefs():
602 container, utypeMap = modelgroups.marshal_STC(ast, getIdFor)
603 if ctx.version>(1,1):
604
605 yield container
606
607 ctx.getEnclosingResource()[
608 _makeCOOSYSFromSTC1(utypeMap, serManager)]
609
626
629 """adds COOSYS and TIMESYS elements from stc2 annotation to the enclosing
630 RESOURCE.
631 """
632 for ann in tableDef.iterAnnotationsOfType("stc2:Coords"):
633 if "time" in ann:
634 timeFrame = ann["time"].get("frame", {})
635 timesysEl = V.TIMESYS(
636 timescale=timeFrame.get("timescale"),
637 refposition=timeFrame.get("refPosition"),
638 timeorigin=timeFrame.get("time0"))
639 timesysId = serManager.makeIdFor(timesysEl, "ts")
640 timesysEl(ID=timesysId)
641 ctx.getEnclosingResource()[
642 timesysEl]
643 _addStupidRefByAnnotation(
644 ctx,
645 ann["time"]["location"],
646 serManager,
647 timesysId)
648
649 if "space" in ann:
650 spaceFrame = ann["space"]["frame"]
651 coosys = V.COOSYS()
652 sysId = serManager.makeIdFor(coosys, "system")
653
654
655 coosys(ID=sysId,
656 system=spaceFrame.get("orientation"))
657 epVal = spaceFrame.get("epoch")
658 if isinstance(epVal, basestring):
659 coosys(epoch=epVal)
660
661
662
663 for child in ann["space"].iterChildRoles():
664 _addStupidRefByAnnotation(ctx, child, serManager, sysId)
665
666 ctx.getEnclosingResource()[coosys]
667
670 """yields GROUPs for table notes.
671
672 The idea is that the note is in the group's description, and the FIELDrefs
673 give the columns that the note applies to.
674 """
675
676 for key, note in serManager.notes.iteritems():
677 noteId = serManager.getOrMakeIdFor(note)
678 noteGroup = V.GROUP(name="note-%s"%key, ID=noteId)[
679 V.DESCRIPTION[note.getContent(targetFormat="text")]]
680 for col in serManager:
681 if col["note"] is note:
682 noteGroup[V.FIELDref(ref=col["id"])]
683 yield noteGroup
684
685
686 -def _makeRef(baseType, ref, container, serManager):
687 """returns a new node of baseType reflecting the group.TypedRef
688 instance ref.
689
690 container is the destination of the reference. For columns, that's
691 the table definition, but for parameters, this must be the table
692 itself rather than its definition because it's the table's
693 params that are embedded in the VOTable.
694 """
695 return baseType(
696 ref=serManager.getOrMakeIdFor(ref.resolve(container)),
697 utype=ref.utype,
698 ucd=ref.ucd)
699
702 """yields GROUPs for the RD groups within container, taking params and
703 fields from serManager's table.
704
705 container can be a tableDef or a group.
706 """
707 for group in container.groups:
708 votGroup = V.GROUP(ucd=group.ucd, utype=group.utype, name=group.name)
709 votGroup[V.DESCRIPTION[group.description]]
710
711 for ref in group.columnRefs:
712 votGroup[_makeRef(V.FIELDref, ref,
713 serManager.table.tableDef, serManager)]
714
715 for ref in group.paramRefs:
716 votGroup[_makeRef(V.PARAMref, ref,
717 serManager.table, serManager)]
718
719 for param in group.params:
720 votGroup[_makeVOTParam(ctx, param)]
721
722 for subgroup in _iterGroups(ctx, group, serManager):
723 votGroup[subgroup]
724
725 yield votGroup
726
729 """returns a Table node for the table.Table instance table.
730 """
731 sm = valuemappers.SerManager(table, mfRegistry=ctx.mfRegistry,
732 idManager=ctx, acquireSamples=ctx.acquireSamples)
733
734
735
736
737 result = V.TABLE()
738 with ctx.activeContainer(result):
739
740
741 if ctx.produceVODML:
742 for ann in table.tableDef.annotations:
743 try:
744 ctx.addVODMLMaterial(ann.getVOT(ctx, table))
745 except Exception as msg:
746
747 base.ui.notifyError("%s-typed DM annotation failed: %s"%(
748 ann.type, msg))
749
750
751
752 result[_iterSTC(ctx, table.tableDef, sm)]
753
754
755 _addLegacySYSFromSTC2(ctx, table.tableDef, sm)
756
757 result(
758 name=table.tableDef.id,
759 utype=base.getMetaText(table, "utype", macroPackage=table.tableDef,
760 propagate=False))[
761
762
763
764 V.DESCRIPTION[base.getMetaText(table, "description",
765 macroPackage=table.tableDef, propagate=False)],
766 _iterGroups(ctx, table.tableDef, sm),
767 _iterFields(ctx, sm),
768 _iterTableParams(ctx, sm),
769 _iterNotes(sm),
770 _linkBuilder.build(table.tableDef),
771 ]
772
773
774 return votable.DelayedTable(result,
775 sm.getMappedTuples(),
776 tableEncoders[ctx.tablecoding],
777 overflowElement=ctx.overflowElement)
778
781 """returns a Resource node for the rsc.Data instance data.
782 """
783 res = V.RESOURCE()
784 with ctx.activeContainer(res):
785 res(type=base.getMetaText(data, "_type"),
786 utype=base.getMetaText(data, "utype"))[
787 _iterResourceMeta(ctx, data),
788 _iterParams(ctx, data), [
789 _makeVOTParam(ctx, param) for param in data.iterParams()],
790 _linkBuilder.build(data.dd),
791 ]
792 for table in data:
793 with ctx.buildingFromTable(table):
794 res[makeTable(ctx, table)]
795 res[ctx.overflowElement]
796 return res
797
798
799
800 makeResource = _makeResource
804 """returns a votable.V.VOTABLE object representing data.
805
806 data can be an rsc.Data or an rsc.Table. data can be a data or a table
807 instance, tablecoding any key in votable.tableEncoders.
808
809 You may pass a VOTableContext object; if you don't a context
810 with all defaults will be used.
811
812 A deprecated alternative is to directly pass VOTableContext constructor
813 arguments as additional keyword arguments. Don't do this, though,
814 we'll probably remove the option to do so at some point.
815
816 You will usually pass the result to votable.write. The object returned
817 contains DelayedTables, i.e., most of the content will only be realized at
818 render time.
819 """
820 ctx = ctx or VOTableContext(**kwargs)
821
822 data = rsc.wrapTable(data)
823 if ctx.version==(1,1):
824 vot = V.VOTABLE11()
825 elif ctx.version==(1,2):
826 vot = V.VOTABLE12()
827 elif ctx.version==(1,3):
828 vot = V.VOTABLE()
829 elif ctx.version==(1,4):
830 vot = V.VOTABLE()
831 else:
832 raise votable.VOTableError("No toplevel element for VOTable version %s"%
833 ctx.version)
834
835 dlResources = list(_iterDatalinkResources(ctx, data))
836 vot[_iterToplevelMeta(ctx, data)]
837 vot[_makeResource(ctx, data)]
838 vot[dlResources]
839
840 if ctx.produceVODML:
841 if ctx.modelsUsed:
842
843 ctx.addVODMLPrefix("vo-dml")
844
845 ctx.rootVODML[[
846 model.getVOT(ctx, None)
847 for model in ctx.modelsUsed.values()]]
848
849 vot[ctx.rootVODML]
850
851 if ctx.suppressNamespace:
852
853 vot._fixedTagMaterial = ""
854
855
856
857 rootAttrs = data.getMeta("_votableRootAttributes")
858 if rootAttrs:
859 rootHacks = [vot._fixedTagMaterial]+[
860 item.getContent() for item in rootAttrs]
861 vot._fixedTagMaterial = " ".join(s for s in rootHacks if s)
862
863
864 return vot
865
868 """writes ``data`` to the ``outputFile``.
869
870 data can be a table or ``Data`` item.
871
872 ``ctx`` can be a ``VOTableContext`` instance; alternatively,
873 ``VOTableContext`` constructor arguments can be passed in as
874 ``kwargs``.
875 """
876 ctx = ctx or VOTableContext(**kwargs)
877 vot = makeVOTable(data, ctx)
878 votable.write(vot, outputFile)
879
882 """returns a string containing a VOTable representation of data.
883
884 ``kwargs`` can be constructor arguments for VOTableContext.
885 """
886 ctx = ctx or VOTableContext(**kwargs)
887 dest = StringIO()
888 writeAsVOTable(data, dest, ctx)
889 return dest.getvalue()
890
895
896 common.registerDataWriter("votable", format,
897 base.votableType, "Default VOTable", ".vot")
898 common.registerDataWriter("votableb2", functools.partial(
899 format, tablecoding="binary2"),
900 "application/x-votable+xml;serialization=BINARY2",
901 "Binary2 VOTable",
902 ".votb2")
903 common.registerDataWriter("votabletd", functools.partial(
904 format, tablecoding="td"),
905 "application/x-votable+xml;serialization=TABLEDATA", "Tabledata VOTable",
906 ".vottd",
907 "text/xml")
908 common.registerDataWriter("votabletd1.1", functools.partial(
909 format, tablecoding="td", version=(1,1)),
910 "application/x-votable+xml;serialization=TABLEDATA;version=1.1",
911 "Tabledata VOTable version 1.1",
912 ".vot1",
913 "text/xml")
914 common.registerDataWriter("votable1.1", functools.partial(
915 format, tablecoding="binary", version=(1,1)),
916 "application/x-votable+xml;version=1.1",
917 "Tabledata VOTable version 1.1",
918 ".vot1",
919 "text/xml")
920 common.registerDataWriter("votabletd1.2", functools.partial(
921 format, tablecoding="td", version=(1,2)),
922 "application/x-votable+xml;serialization=TABLEDATA;version=1.2",
923 "Tabledata VOTable version 1.2",
924 ".vot2",
925 "text/xml")
926 common.registerDataWriter("vodml", functools.partial(
927 format, tablecoding="td", version=(1,4)),
928 "application/x-votable+xml;serialization=TABLEDATA;version=1.4",
929 "VOTable version 1.4",
930 ".vot4")
931