Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

pyvo

Package Overview
Dependencies
Maintainers
5
Versions
42
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

pyvo - npm Package Compare versions

Comparing version
1.7.1
to
1.8
+22
pyvo/io/uws/tests/data/job-with-duplicate-elements.xml
<?xml version="1.0"?>
<uws:job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<uws:jobId>myjobid</uws:jobId>
<uws:ownerId/>
<uws:phase>COMPLETED</uws:phase>
<uws:creationTime>2025-06-04T00:00:00Z</uws:creationTime>
<uws:startTime>2025-06-04T00:05:00Z</uws:startTime>
<uws:endTime>2025-06-04T02:00:00Z</uws:endTime>
<uws:executionDuration>7200</uws:executionDuration>
<uws:destruction>2025-06-04T00:00:00Z</uws:destruction>
<uws:parameters>
<uws:parameter id="query">SELECT * FROM table</uws:parameter>
</uws:parameters>
<uws:results>
<uws:result id="result" xlink:href="http://example.com/result"/>
</uws:results>
<uws:jobInfo>
<status>initial</status>
<status>processing</status>
<status>completed</status>
</uws:jobInfo>
</uws:job>
<?xml version="1.0"?>
<uws:job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<uws:jobId>myjobid</uws:jobId>
<uws:ownerId/>
<uws:phase>COMPLETED</uws:phase>
<uws:creationTime>2025-06-04T00:00:00Z</uws:creationTime>
<uws:startTime>2025-06-04T00:05:00Z</uws:startTime>
<uws:endTime>2025-06-04T02:00:00Z</uws:endTime>
<uws:executionDuration>7200</uws:executionDuration>
<uws:destruction>2025-06-04T00:00:00Z</uws:destruction>
<uws:parameters>
<uws:parameter id="query">SELECT * FROM table</uws:parameter>
</uws:parameters>
<uws:results>
<uws:result id="result" xlink:href="http://example.com/result"/>
</uws:results>
<uws:jobInfo>
<!-- Empty jobInfo element -->
</uws:jobInfo>
</uws:job>
<?xml version="1.0"?>
<uws:job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<uws:jobId>myjobid</uws:jobId>
<uws:ownerId/>
<uws:phase>COMPLETED</uws:phase>
<uws:creationTime>2025-06-04T00:00:00Z</uws:creationTime>
<uws:startTime>2025-06-04T00:05:00Z</uws:startTime>
<uws:endTime>2025-06-04T02:00:00Z</uws:endTime>
<uws:executionDuration>7200</uws:executionDuration>
<uws:destruction>2025-06-04T00:00:00Z</uws:destruction>
<uws:parameters>
<uws:parameter id="query">SELECT * FROM table</uws:parameter>
</uws:parameters>
<uws:results>
<uws:result id="result" xlink:href="http://example.com/result"/>
</uws:results>
<uws:jobInfo>
<tap:progress xmlns:tap="http://example-tap.org">50</tap:progress>
<custom:progress xmlns:custom="http://example-custom.org">75</custom:progress>
<uniqueElement>no collision</uniqueElement>
</uws:jobInfo>
</uws:job>
<?xml version="1.0"?>
<uws:job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<uws:jobId>jobid123</uws:jobId>
<uws:ownerId/>
<uws:phase>COMPLETED</uws:phase>
<uws:creationTime>2025-06-04T00:00:00Z</uws:creationTime>
<uws:startTime>2025-06-04T00:05:00Z</uws:startTime>
<uws:endTime>2025-06-04T02:00:00Z</uws:endTime>
<uws:executionDuration>7200</uws:executionDuration>
<uws:destruction>2025-06-04T00:00:00Z</uws:destruction>
<uws:parameters>
<uws:parameter id="query">SELECT * FROM table</uws:parameter>
</uws:parameters>
<uws:results>
<uws:result id="result" xlink:href="http://example.com/result"/>
</uws:results>
<uws:jobInfo>
<queryInfo>
<metrics>
<execution_time>1500</execution_time>
<rows_returned>100</rows_returned>
</metrics>
</queryInfo>
</uws:jobInfo>
</uws:job>
<?xml version="1.0"?>
<uws:job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<uws:jobId>test123</uws:jobId>
<uws:ownerId/>
<uws:phase>COMPLETED</uws:phase>
<uws:creationTime>2025-06-04T00:00:00Z</uws:creationTime>
<uws:startTime>2025-06-04T00:05:00Z</uws:startTime>
<uws:endTime>2025-06-04T02:00:00Z</uws:endTime>
<uws:executionDuration>7200</uws:executionDuration>
<uws:destruction>2025-06-04T00:00:00Z</uws:destruction>
<uws:parameters>
<uws:parameter id="query">SELECT * FROM table</uws:parameter>
</uws:parameters>
<uws:results>
<uws:result id="result" xlink:href="http://example.com/result"/>
</uws:results>
<uws:jobInfo>
<tapQueryInfo>
<pct_complete>100</pct_complete>
<chunks_processed>1</chunks_processed>
<total_chunks>1</total_chunks>
</tapQueryInfo>
</uws:jobInfo>
</uws:job>
<?xml version="1.0"?>
<uws:job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
<uws:jobId>myid123</uws:jobId>
<uws:ownerId/>
<uws:phase>COMPLETED</uws:phase>
<uws:creationTime>2025-06-04T00:00:00Z</uws:creationTime>
<uws:startTime>2025-06-04T00:05:00Z</uws:startTime>
<uws:endTime>2025-06-04T02:00:00Z</uws:endTime>
<uws:executionDuration>7200</uws:executionDuration>
<uws:destruction>2025-06-04T00:00:00Z</uws:destruction>
<uws:parameters>
<uws:parameter id="query">SELECT * FROM table</uws:parameter>
</uws:parameters>
<uws:results>
<uws:result id="result" xlink:href="http://example.com/result"/>
</uws:results>
<uws:jobInfo>
<integer_value>100</integer_value>
<float_value>3.14</float_value>
<string_value>pyvo</string_value>
<empty_value></empty_value>
</uws:jobInfo>
</uws:job>
<?xml version="1.0" encoding="utf-8"?>
<VOTABLE version="1.4"
xmlns="http://www.ivoa.net/xml/VOTable/v1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.ivoa.net/xml/VOTable/v1.3 http://www.ivoa.net/xml/VOTable/votable-1.4.xsd">
<RESOURCE name="simbad_cone-search_result" type="results">
<INFO name="standardID" value="ivo://ivoa.net/std/conesearch">IVOID of the service specification</INFO>
<INFO name="service_protocol" value="Simple Cone Search 1.03-extended">Used access protocol and version</INFO>
<INFO name="publisher" value="CDS/SIMBAD">Data publisher</INFO>
<INFO name="request" value="https://simbad.cds.unistra.fr/cone/?RA=269.4521&amp;DEC=4.693365&amp;SR=0.01&amp;VERB=2&amp;MAXREC=1&amp;ORDER_BY=distance&amp;ORDER_DIR=ASC&amp;RESPONSEFORMAT=application%2Fx-votable%2Bxml%3Bcontent%3Dmivot%3Bserialization%3DTABLEDATA%3Bcharset%3DUTF-8">HTTP request URL</INFO>
<INFO name="request_date" value="2025-09-23T15:01:26.078545Z">Query execution date</INFO>
<INFO name="contact" value="cds-question@unistra.fr">Publisher email address</INFO>
<INFO name="QUERY_STATUS" value="OK">Successful query</INFO>
<COOSYS ID="SIMBAD-COOSYS" system="ICRS" epoch="J2000" equinox="2000"/>
<RESOURCE type="meta">
<VODML xmlns="http://www.ivoa.net/xml/mivot">
<REPORT status="OK"/>
<MODEL name="ivoa" url="https://www.ivoa.net/xml/VODML/IVOA-v1.vo-dml.xml"/>
<MODEL name="coords" url="https://ivoa.net/xml/VODML/Coords-v1.0.vo-dml.xml"/>
<MODEL name="mango" url="https://raw.githubusercontent.com/ivoa-std/MANGO/refs/heads/wd-v1.0/vo-dml/mango.vo-dml.xml"/>
<GLOBALS>
<INSTANCE dmtype="coords:SpaceSys" dmid="_spaceframe_ICRS_2000_BARYCENTER">
<INSTANCE dmtype="coords:SpaceFrame" dmrole="coords:PhysicalCoordSys.frame">
<ATTRIBUTE dmtype="ivoa:string" dmrole="coords:SpaceFrame.spaceRefFrame" value="ICRS"/>
</INSTANCE>
</INSTANCE>
</GLOBALS>
<TEMPLATES>
<INSTANCE dmtype="mango:MangoObject" dmid="main_id">
<ATTRIBUTE dmtype="ivoa:string" dmrole="mango:MangoObject.identifier" ref="main_id"/>
<COLLECTION dmrole="mango:MangoObject.propertyDock">
<INSTANCE dmtype="mango:EpochPosition">
<ATTRIBUTE dmtype="ivoa:string" dmrole="mango:Property.description" value="6 parameters position"/>
<INSTANCE dmtype="mango:VocabularyTerm" dmrole="mango:Property.semantics">
<ATTRIBUTE dmtype="ivoa:string" dmrole="mango:VocabularyTerm.uri" value="https://www.ivoa.net/rdf/uat/2024-06-25/uat.html#astronomical-location"/>
<ATTRIBUTE dmtype="ivoa:string" dmrole="mango:VocabularyTerm.label" value="Astronomical location"/>
</INSTANCE>
<ATTRIBUTE dmtype="ivoa:RealQuantity" dmrole="mango:EpochPosition.longitude" unit="deg" ref="ra"/>
<ATTRIBUTE dmtype="ivoa:RealQuantity" dmrole="mango:EpochPosition.latitude" unit="deg" ref="dec"/>
<ATTRIBUTE dmtype="ivoa:RealQuantity" dmrole="mango:EpochPosition.pmLongitude" unit="mas / yr" ref="pmra"/>
<ATTRIBUTE dmtype="ivoa:RealQuantity" dmrole="mango:EpochPosition.pmLatitude" unit="mas / yr" ref="pmdec"/>
<ATTRIBUTE dmtype="ivoa:RealQuantity" dmrole="mango:EpochPosition.parallax" unit="mas" ref="plx_value"/>
<ATTRIBUTE dmtype="ivoa:RealQuantity" dmrole="mango:EpochPosition.radialVelocity" unit="km / s" ref="rvz_radvel"/>
<INSTANCE dmrole="mango:EpochPosition.obsDate" dmtype="mango:DateTime">
<ATTRIBUTE dmrole="mango:DateTime.representation" dmtype="ivoa:string" value="year"/>
<ATTRIBUTE dmrole="mango:DateTime.dateTime" dmtype="ivoa:datetime" value="2000" unit="y"/>
</INSTANCE>
<INSTANCE dmtype="mango:EpochPositionErrors" dmrole="mango:EpochPosition.errors">
<INSTANCE dmtype="mango:error.PErrorEllipse" dmrole="mango:EpochPositionErrors.position">
<ATTRIBUTE dmtype="ivoa:RealQuantity" dmrole="mango:error.PErrorEllipse.semiMajorAxis" unit="mas" ref="coo_err_maja"/>
<ATTRIBUTE dmtype="ivoa:RealQuantity" dmrole="mango:error.PErrorEllipse.semiMinorAxis" unit="mas" ref="coo_err_mina"/>
<ATTRIBUTE dmtype="ivoa:RealQuantity" dmrole="mango:error.PErrorEllipse.angle" unit="deg" ref="coo_err_angle"/>
</INSTANCE>
<INSTANCE dmtype="mango:error.PErrorEllipse" dmrole="mango:EpochPositionErrors.properMotion">
<ATTRIBUTE dmtype="ivoa:RealQuantity" dmrole="mango:error.PErrorEllipse.semiMajorAxis" unit="mas / yr" ref="pm_err_maja"/>
<ATTRIBUTE dmtype="ivoa:RealQuantity" dmrole="mango:error.PErrorEllipse.semiMinorAxis" unit="mas / yr" ref="pm_err_mina"/>
<ATTRIBUTE dmtype="ivoa:RealQuantity" dmrole="mango:error.PErrorEllipse.angle" unit="deg" ref="pm_err_angle"/>
</INSTANCE>
</INSTANCE>
<REFERENCE dmrole="mango:EpochPosition.spaceSys" dmref="_spaceframe_ICRS_2000_BARYCENTER"/>
</INSTANCE>
</COLLECTION>
</INSTANCE>
</TEMPLATES>
</VODML>
</RESOURCE>
<TABLE>
<FIELD datatype="float" name="distance" ucd="pos.angDistance" unit="deg">
<DESCRIPTION>Distance (in degrees) between this object and the cone-search's target.</DESCRIPTION>
</FIELD>
<FIELD ID="main_id" arraysize="*" datatype="char" name="main_id" ucd="meta.id;meta.main">
<DESCRIPTION>Main identifier for an object</DESCRIPTION>
<LINK href="https://simbad.cds.unistra.fr/simbad/sim-id?Ident=${main_id}&amp;NbIdent=1"/>
</FIELD>
<FIELD datatype="double" name="ra" ref="SIMBAD-COOSYS" ucd="pos.eq.ra;meta.main" unit="deg">
<DESCRIPTION>Right ascension</DESCRIPTION>
</FIELD>
<FIELD datatype="double" name="dec" ref="SIMBAD-COOSYS" ucd="pos.eq.dec;meta.main" unit="deg">
<DESCRIPTION>Declination</DESCRIPTION>
</FIELD>
<FIELD arraysize="*" datatype="char" name="otype" ucd="src.class">
<DESCRIPTION>Object type</DESCRIPTION>
<LINK href="https://simbad.cds.unistra.fr/guide/otypes.htx#${otype}"/>
</FIELD>
<FIELD arraysize="*" datatype="char" name="coo"/>
<FIELD datatype="double" name="coo_err_maja" ucd="phys.angSize.smajAxis;pos.errorEllipse;pos.eq" unit="mas">
<DESCRIPTION>Coordinate error major axis</DESCRIPTION>
</FIELD>
<FIELD datatype="double" name="coo_err_mina" ucd="phys.angSize.sminAxis;pos.errorEllipse;pos.eq" unit="mas">
<DESCRIPTION>Coordinate error minor axis</DESCRIPTION>
</FIELD>
<FIELD datatype="short" name="coo_err_angle" ucd="pos.posAng;pos.errorEllipse;pos.eq" unit="deg">
<DESCRIPTION>Coordinate error angle</DESCRIPTION>
</FIELD>
<FIELD datatype="double" name="pmra" ucd="pos.pm;pos.eq.ra" unit="mas.yr-1">
<DESCRIPTION>Proper motion in RA</DESCRIPTION>
</FIELD>
<FIELD datatype="double" name="pmdec" ucd="pos.pm;pos.eq.dec" unit="mas.yr-1">
<DESCRIPTION>Proper motion in DEC</DESCRIPTION>
</FIELD>
<FIELD datatype="double" name="pm_err_maja" ucd="phys.angSize.smajAxis;pos.errorEllipse;pos.pm" unit="mas.yr-1">
<DESCRIPTION>Proper motion error major axis</DESCRIPTION>
</FIELD>
<FIELD datatype="double" name="pm_err_mina" ucd="phys.angSize.sminAxis;pos.errorEllipse;pos.pm" unit="mas.yr-1">
<DESCRIPTION>Proper motion error minor axis</DESCRIPTION>
</FIELD>
<FIELD datatype="short" name="pm_err_angle" ucd="pos.posAng;pos.errorEllipse;pos.pm" unit="deg">
<DESCRIPTION>Proper motion error angle</DESCRIPTION>
</FIELD>
<FIELD datatype="double" name="plx_value" ucd="pos.parallax.trig" unit="mas">
<DESCRIPTION>Parallax</DESCRIPTION>
</FIELD>
<FIELD datatype="double" name="rvz_radvel" ucd="spect.dopplerVeloc.opt" unit="km.s-1">
<DESCRIPTION>Radial Velocity</DESCRIPTION>
</FIELD>
<FIELD datatype="double" name="galdim_majaxis" ucd="phys.angSize.smajAxis" unit="arcmin">
<DESCRIPTION>Angular size major axis</DESCRIPTION>
</FIELD>
<FIELD datatype="double" name="galdim_minaxis" ucd="phys.angSize.sminAxis" unit="arcmin">
<DESCRIPTION>Angular size minor axis</DESCRIPTION>
</FIELD>
<FIELD datatype="short" name="galdim_angle" ucd="pos.posAng" unit="deg">
<DESCRIPTION>Galaxy ellipse angle</DESCRIPTION>
</FIELD>
<FIELD arraysize="*" datatype="char" name="sp_type" ucd="src.spType">
<DESCRIPTION>MK spectral type</DESCRIPTION>
</FIELD>
<FIELD arraysize="*" datatype="char" name="morph_type" ucd="src.morph.type">
<DESCRIPTION>Morphological type</DESCRIPTION>
</FIELD>
<FIELD datatype="int" name="biblist"/>
<DATA>
<TABLEDATA>
<TR>
<TD>1.0409523503457668E-5</TD>
<TD>NAME Barnard's Star c</TD>
<TD>269.45207695861900</TD>
<TD>4.69336496657667</TD>
<TD>Planet</TD>
<TD> 17 57 48.4984700683 +04 41 36.113879675</TD>
<TD>0.0262</TD>
<TD>0.0290</TD>
<TD>90</TD>
<TD>-801.551</TD>
<TD>10362.394</TD>
<TD>0.032</TD>
<TD>0.036</TD>
<TD>90</TD>
<TD>546.9759</TD>
<TD></TD>
<TD></TD>
<TD></TD>
<TD></TD>
<TD></TD>
<TD></TD>
<TD>1</TD>
</TR>
</TABLEDATA>
</DATA>
</TABLE>
<INFO name="QUERY_STATUS" value="OVERFLOW">Truncated result</INFO>
</RESOURCE>
</VOTABLE>
<?xml version="1.0" encoding="utf-8"?>
<VOTABLE version="1.3" xmlns="http://www.ivoa.net/xml/VOTable/v1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.ivoa.net/xml/VOTable/v1.3 http://www.ivoa.net/xml/VOTable/v1.3">
<RESOURCE type="results">
<INFO name="QUERY_STATUS" value="OK" />
<INFO name="QUERY"
value='SELECT TOP 1 "public".mergedentry.SC_EP_1_FLUX,"public".mergedentry.SC_EP_2_FLUX,"public".mergedentry.SC_EP_3_FLUX
FROM "public".mergedentry
' />
<!-- Here starts the mapping block This bloc maps all data contained in the VOTable on the MANGO MODEL_INSTANCE. The MODEL_INSTANCEInstanceInVot
syntax is detailed here https://github.com/ivoa-std/MODEL_INSTANCEinstanceinvot with a lot of snippet here https://github.com/ivoa/MODEL_INSTANCEinstanceinvot-code -->
<RESOURCE type="meta">
<VODML xmlns="http://www.ivoa.net/xml/mivot">
<REPORT status="OK">Automatically annotated by XTAPDB</REPORT>
<MODEL name="ivoa" url="https://www.ivoa.net/xml/VODML/IVOA-v1.vo-dml.xml" />
<MODEL name="coords" url="https://www.ivoa.net/xml/STC/20200908/Coords-v1.0.vo-dml.xml" />
<MODEL name="meas" url="https://www.ivoa.net/xml/Meas/20200908/Meas-v1.0.vo-dml.xml" />
<MODEL name="phot" url="https://ivoa.net/xml/VODML/Phot-v1.vodml.xml" />
<MODEL name="mango" url="https://github.com/ivoa-std/MANGO/raw/refs/heads/wd-v1.0/vo-dml/mango.vo-dml.xml" />
<!-- The GLOBALS block contains all objects with a scope covering all data. This is typically the case for the coordinate
Systems -->
<GLOBALS>
<INSTANCE dmid="CoordSystem_XMM_EB1_id" dmtype="Phot:PhotCal">
<ATTRIBUTE dmrole="Phot:PhotCal.identifier" dmtype="ivoa:string" value="XMM/EPIC/EB1" />
<!-- Magnitude System -->
<INSTANCE dmrole="Phot:PhotCal.magnitudeSystem" dmtype="Phot:MagnitudeSystem">
<ATTRIBUTE dmrole="Phot:MagnitudeSystem.type" dmtype="Phot:TypeOfMagSystem" value="XMM" />
<ATTRIBUTE dmrole="Phot:MagnitudeSystem.referenceSpectrum" dmtype="ivoa:anyURI"
value="https://xmm-tools.cosmos.esa.int/external/xmm_user_support/documentation/sas_usg/USG/SASUSG.html" />
</INSTANCE>
<!-- Filter -->
<REFERENCE dmref="CoordSystem_XMM_FILTER_EB1_id" dmrole="Phot:PhotCal.photometryFilter" />
</INSTANCE>
<INSTANCE dmid="CoordSystem_XMM_EB2_id" dmtype="Phot:PhotCal">
<ATTRIBUTE dmrole="Phot:PhotCal.identifier" dmtype="ivoa:string" value="XMM/EPIC/EB2" />
<!-- Magnitude System -->
<INSTANCE dmrole="Phot:PhotCal.magnitudeSystem" dmtype="Phot:MagnitudeSystem">
<ATTRIBUTE dmrole="Phot:MagnitudeSystem.type" dmtype="Phot:TypeOfMagSystem" value="XMM" />
<ATTRIBUTE dmrole="Phot:MagnitudeSystem.referenceSpectrum" dmtype="ivoa:anyURI"
value="https://xmm-tools.cosmos.esa.int/external/xmm_user_support/documentation/sas_usg/USG/SASUSG.html" />
</INSTANCE>
<!-- Filter -->
<REFERENCE dmref="CoordSystem_XMM_FILTER_EB2_id" dmrole="Phot:PhotCal.photometryFilter" />
</INSTANCE>
<INSTANCE dmid="CoordSystem_XMM_EB3_id" dmtype="Phot:PhotCal">
<ATTRIBUTE dmrole="Phot:PhotCal.identifier" dmtype="ivoa:string" value="XMM/EPIC/EB3" />
<!-- Magnitude System -->
<INSTANCE dmrole="Phot:PhotCal.magnitudeSystem" dmtype="Phot:MagnitudeSystem">
<ATTRIBUTE dmrole="Phot:MagnitudeSystem.type" dmtype="Phot:TypeOfMagSystem" value="XMM" />
<ATTRIBUTE dmrole="Phot:MagnitudeSystem.referenceSpectrum" dmtype="ivoa:anyURI"
value="https://xmm-tools.cosmos.esa.int/external/xmm_user_support/documentation/sas_usg/USG/SASUSG.html" />
</INSTANCE>
<!-- Filter -->
<REFERENCE dmref="CoordSystem_XMM_FILTER_EB3_id" dmrole="Phot:PhotCal.photometryFilter" />
</INSTANCE>
<INSTANCE dmid="CoordSystem_XMM_FILTER_EB1_id" dmtype="Phot:PhotometryFilter">
<ATTRIBUTE dmrole="Phot:PhotometryFilter.identifier" dmtype="ivoa:string" value="XMM/EPIC/EB1" />
<ATTRIBUTE dmrole="Phot:PhotometryFilter.name" dmtype="ivoa:string" value="XMM EPIC EB1" />
<ATTRIBUTE dmrole="Phot:PhotometryFilter.description" dmtype="ivoa:string" value="Soft" />
<ATTRIBUTE dmrole="Phot:PhotometryFilter.bandName" dmtype="ivoa:string" value="EB1" />
<!-- Spectral Location -->
<INSTANCE dmrole="Phot:PhotometryFilter.spectralLocation" dmtype="Phot:SpectralLocation">
<ATTRIBUTE dmrole="Phot:SpectralLocation.ucd" dmtype="Phot:UCD" value="em.wl.effective" />
<ATTRIBUTE dmrole="Phot:SpectralLocation.unitexpression" dmtype="ivoa:Unit" value="keV" />
<ATTRIBUTE dmrole="Phot:SpectralLocation.value" dmtype="ivoa:real" value="0.35" />
</INSTANCE>
<!-- Band width -->
<INSTANCE dmrole="Phot:PhotometryFilter.bandwidth" dmtype="Phot:Bandwidth">
<ATTRIBUTE dmrole="Phot:Bandwidth.ucd" dmtype="Phot:UCD" value="instr.bandwidth;stat.fwhm" />
<ATTRIBUTE dmrole="Phot:Bandwidth.unitexpression" dmtype="ivoa:Unit" value="keV" />
<ATTRIBUTE dmrole="Phot:Bandwidth.extent" dmtype="ivoa:real" value="0.3" />
<ATTRIBUTE dmrole="Phot:Bandwidth.start" dmtype="ivoa:real" value="0.2" />
<ATTRIBUTE dmrole="Phot:Bandwidth.stop" dmtype="ivoa:real" value="0.5" />
</INSTANCE>
<!-- Transmission Curve -->
<INSTANCE dmrole="Phot:PhotometryFilter.transmissionCurve" dmtype="Phot:TransmissionCurve">
<INSTANCE dmrole="Phot:TransmissionCurve.access" dmtype="Phot:Access">
<ATTRIBUTE dmrole="Phot:Access.reference" dmtype="ivoa:anyURI"
value="https://xmm-tools.cosmos.esa.int/external/xmm_user_support/documentation/sas_usg/USG/SASUSG.html" />
<ATTRIBUTE dmrole="Phot:Access.format" dmtype="ivoa:string" value="text/html" />
</INSTANCE>
</INSTANCE>
</INSTANCE>
<INSTANCE dmid="CoordSystem_XMM_FILTER_EB2_id" dmtype="Phot:PhotometryFilter">
<ATTRIBUTE dmrole="Phot:PhotometryFilter.identifier" dmtype="ivoa:string" value="XMM/EPIC/EB2" />
<ATTRIBUTE dmrole="Phot:PhotometryFilter.name" dmtype="ivoa:string" value="XMM EPIC EB2" />
<ATTRIBUTE dmrole="Phot:PhotometryFilter.description" dmtype="ivoa:string" value="Soft" />
<ATTRIBUTE dmrole="Phot:PhotometryFilter.bandName" dmtype="ivoa:string" value="EB2" />
<!-- Spectral Location -->
<INSTANCE dmrole="Phot:PhotometryFilter.spectralLocation" dmtype="Phot:SpectralLocation">
<ATTRIBUTE dmrole="Phot:SpectralLocation.ucd" dmtype="Phot:UCD" value="em.wl.effective" />
<ATTRIBUTE dmrole="Phot:SpectralLocation.unitexpression" dmtype="ivoa:Unit" value="keV" />
<ATTRIBUTE dmrole="Phot:SpectralLocation.value" dmtype="ivoa:real" value="0.75" />
</INSTANCE>
<!-- Band width -->
<INSTANCE dmrole="Phot:PhotometryFilter.bandwidth" dmtype="Phot:Bandwidth">
<ATTRIBUTE dmrole="Phot:Bandwidth.ucd" dmtype="Phot:UCD" value="instr.bandwidth;stat.fwhm" />
<ATTRIBUTE dmrole="Phot:Bandwidth.unitexpression" dmtype="ivoa:Unit" value="keV" />
<ATTRIBUTE dmrole="Phot:Bandwidth.extent" dmtype="ivoa:real" value="0.5" />
<ATTRIBUTE dmrole="Phot:Bandwidth.start" dmtype="ivoa:real" value="0.5" />
<ATTRIBUTE dmrole="Phot:Bandwidth.stop" dmtype="ivoa:real" value="1.0" />
</INSTANCE>
<!-- Transmission Curve -->
<INSTANCE dmrole="Phot:PhotometryFilter.transmissionCurve" dmtype="Phot:TransmissionCurve">
<INSTANCE dmrole="Phot:TransmissionCurve.access" dmtype="Phot:Access">
<ATTRIBUTE dmrole="Phot:Access.reference" dmtype="ivoa:anyURI"
value="https://xmm-tools.cosmos.esa.int/external/xmm_user_support/documentation/sas_usg/USG/SASUSG.html" />
<ATTRIBUTE dmrole="Phot:Access.format" dmtype="ivoa:string" value="text/html" />
</INSTANCE>
</INSTANCE>
</INSTANCE>
<INSTANCE dmid="CoordSystem_XMM_FILTER_EB3_id" dmtype="Phot:PhotometryFilter">
<ATTRIBUTE dmrole="Phot:PhotometryFilter.identifier" dmtype="ivoa:string" value="XMM/EPIC/EB3" />
<ATTRIBUTE dmrole="Phot:PhotometryFilter.name" dmtype="ivoa:string" value="XMM EPIC EB3" />
<ATTRIBUTE dmrole="Phot:PhotometryFilter.description" dmtype="ivoa:string" value="Medium" />
<ATTRIBUTE dmrole="Phot:PhotometryFilter.bandName" dmtype="ivoa:string" value="EB3" />
<!-- Spectral Location -->
<INSTANCE dmrole="Phot:PhotometryFilter.spectralLocation" dmtype="Phot:SpectralLocation">
<ATTRIBUTE dmrole="Phot:SpectralLocation.ucd" dmtype="Phot:UCD" value="em.wl.effective" />
<ATTRIBUTE dmrole="Phot:SpectralLocation.unitexpression" dmtype="ivoa:Unit" value="keV" />
<ATTRIBUTE dmrole="Phot:SpectralLocation.value" dmtype="ivoa:real" value="1.5" />
</INSTANCE>
<!-- Band width -->
<INSTANCE dmrole="Phot:PhotometryFilter.bandwidth" dmtype="Phot:Bandwidth">
<ATTRIBUTE dmrole="Phot:Bandwidth.ucd" dmtype="Phot:UCD" value="instr.bandwidth;stat.fwhm" />
<ATTRIBUTE dmrole="Phot:Bandwidth.unitexpression" dmtype="ivoa:Unit" value="keV" />
<ATTRIBUTE dmrole="Phot:Bandwidth.extent" dmtype="ivoa:real" value="1.0" />
<ATTRIBUTE dmrole="Phot:Bandwidth.start" dmtype="ivoa:real" value="1.0" />
<ATTRIBUTE dmrole="Phot:Bandwidth.stop" dmtype="ivoa:real" value="2.0" />
</INSTANCE>
<!-- Transmission Curve -->
<INSTANCE dmrole="Phot:PhotometryFilter.transmissionCurve" dmtype="Phot:TransmissionCurve">
<INSTANCE dmrole="Phot:TransmissionCurve.access" dmtype="Phot:Access">
<ATTRIBUTE dmrole="Phot:Access.reference" dmtype="ivoa:anyURI"
value="https://xmm-tools.cosmos.esa.int/external/xmm_user_support/documentation/sas_usg/USG/SASUSG.html" />
<ATTRIBUTE dmrole="Phot:Access.format" dmtype="ivoa:string" value="text/html" />
</INSTANCE>
</INSTANCE>
</INSTANCE>
</GLOBALS>
<!-- This block maps the data of the table identified as "Results" -->
<TEMPLATES tableref="Results">
<INSTANCE dmrole="" dmtype="mango:Brightness">
<ATTRIBUTE dmrole="mango:Brightness.value" dmtype="ivoa:RealQuantity" ref="SC_EP_1_FLUX" />
<REFERENCE dmref="CoordSystem_XMM_EB1_id" dmrole="mango:Brightness.photCal" />
</INSTANCE>
<INSTANCE dmrole="" dmtype="mango:Brightness">
<ATTRIBUTE dmrole="mango:Brightness.value" dmtype="ivoa:RealQuantity" ref="SC_EP_2_FLUX" />
<REFERENCE dmref="CoordSystem_XMM_EB2_id" dmrole="mango:Brightness.photCal" />
</INSTANCE>
<INSTANCE dmrole="" dmtype="mango:Brightness">
<ATTRIBUTE dmrole="mango:Brightness.value" dmtype="ivoa:RealQuantity" ref="SC_EP_3_FLUX" />
<REFERENCE dmref="CoordSystem_XMM_EB3_id" dmrole="mango:Brightness.photCal" />
</INSTANCE>
</TEMPLATES>
</VODML>
</RESOURCE>
<TABLE name="Results">
<FIELD datatype="float" name="SC_EP_1_FLUX" ucd="phot.flux" unit="erg/cm**2/s">
<DESCRIPTION>the flux of the energy band number 1 of the ep camera in sc</DESCRIPTION>
</FIELD>
<FIELD datatype="float" name="SC_EP_2_FLUX" ucd="phot.flux" unit="erg/cm**2/s">
<DESCRIPTION>the flux of the energy band number 2 of the ep camera in sc</DESCRIPTION>
</FIELD>
<FIELD datatype="float" name="SC_EP_3_FLUX" ucd="phot.flux" unit="erg/cm**2/s">
<DESCRIPTION>the flux of the energy band number 3 of the ep camera in sc</DESCRIPTION>
</FIELD>
<DATA>
<TABLEDATA>
<TR>
<TD>0.0</TD>
<TD>0.1</TD>
<TD>0.2</TD>
</TR>
<TR>
<TD>1.0</TD>
<TD>2.1</TD>
<TD>3.2</TD>
</TR>
</TABLEDATA>
</DATA>
</TABLE>
</RESOURCE>
</VOTABLE>
<?xml version="1.0" encoding="UTF-8"?>
<!--W3C Schema for VOTable = Virtual Observatory Tabular Format
.Version 1.0 : 15-Apr-2002
.Version 1.09: 23-Jan-2004 Version 1.09
.Version 1.09: 30-Jan-2004 Version 1.091
.Version 1.09: 22-Mar-2004 Version 1.092
.Version 1.094: 02-Jun-2004 GROUP does not contain FIELD
.Version 1.1 : 10-Jun-2004 remove the complexContent
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified"
targetNamespace="http://www.ivoa.net/xml/VOTable/v1.1"
xmlns="http://www.ivoa.net/xml/VOTable/v1.1"
>
<!-- Here we define some interesting new datatypes:
- anyTEXT may have embedded XHTML (conforming HTML)
- astroYear is an epoch in Besselian or Julian year, e.g. J2000
- arrayDEF specifies an array size e.g. 12x23x*
- dataType defines the acceptable datatypes
- ucdType defines the acceptable UCDs (UCD1+)
- precType defines the acceptable precisions
- yesno defines just the 2 alternatives
-->
<xs:complexType name="anyTEXT" mixed="true">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" processContents="skip"/>
</xs:sequence>
</xs:complexType>
<xs:simpleType name="astroYear">
<xs:restriction base="xs:token">
<xs:pattern value="[JB]?[0-9]+([.][0-9]*)?"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ucdType">
<xs:restriction base="xs:token">
<xs:pattern value="[A-Za-z0-9_.;\-]*"/><!-- UCD1 use also / + % -->
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="arrayDEF">
<xs:restriction base="xs:token">
<xs:pattern value="([0-9]+x)*[0-9]*[*]?(s\W)?"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="encodingType">
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="gzip"/>
<xs:enumeration value="base64"/>
<xs:enumeration value="dynamic"/>
<xs:enumeration value="none"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="dataType">
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="boolean"/>
<xs:enumeration value="bit"/>
<xs:enumeration value="unsignedByte"/>
<xs:enumeration value="short"/>
<xs:enumeration value="int"/>
<xs:enumeration value="long"/>
<xs:enumeration value="char"/>
<xs:enumeration value="unicodeChar"/>
<xs:enumeration value="float"/>
<xs:enumeration value="double"/>
<xs:enumeration value="floatComplex"/>
<xs:enumeration value="doubleComplex"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="precType">
<xs:restriction base="xs:token">
<xs:pattern value="[EF]?[1-9][0-9]*"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="yesno">
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="yes"/>
<xs:enumeration value="no"/>
</xs:restriction>
</xs:simpleType>
<!-- VOTable is the root element -->
<xs:element name="VOTABLE">
<xs:complexType>
<xs:sequence>
<xs:element ref="DESCRIPTION" minOccurs="0"/>
<xs:element ref="DEFINITIONS" minOccurs="0"/><!-- Deprecated -->
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="COOSYS" />
<xs:element ref="PARAM" />
<xs:element ref="INFO" />
</xs:choice>
<xs:element ref="RESOURCE" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="version">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="1.1"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:element>
<!-- RESOURCES can contain DESCRIPTION, (INFO|PARAM|COSYS), LINK, TABLEs -->
<xs:element name="RESOURCE">
<xs:complexType>
<xs:sequence>
<xs:element ref="DESCRIPTION" minOccurs="0"/>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="INFO" />
<xs:element ref="COOSYS" />
<xs:element ref="PARAM" />
</xs:choice>
<xs:element ref="LINK" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="TABLE" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="RESOURCE" minOccurs="0" maxOccurs="unbounded"/>
<!-- Suggested Doug Tody, to include new RESOURCE types -->
<xs:any namespace="##other" processContents="lax"
minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="name" type="xs:token"/>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="utype" type="xs:string"/>
<xs:attribute name="type" default="results">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="results"/>
<xs:enumeration value="meta"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<!-- Suggested Doug Tody, to include new RESOURCE attributes -->
<xs:anyAttribute namespace="##other" processContents="lax"/>
</xs:complexType>
</xs:element>
<xs:element name="DESCRIPTION" type="anyTEXT" />
<xs:element name="DEFINITIONS">
<xs:annotation>
<xs:documentation>Deprecated in Version 1.1</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="COOSYS" />
<xs:element ref="PARAM" />
</xs:choice>
</xs:complexType>
</xs:element>
<!-- INFO is a name-value pair -->
<xs:element name="INFO">
<xs:complexType><xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="value" type="xs:string" use="required"/>
</xs:extension>
</xs:simpleContent></xs:complexType>
</xs:element>
<!-- A PARAM is similar to a FIELD, but it also has a "value" attribute -->
<xs:element name="PARAM">
<xs:complexType>
<xs:sequence>
<xs:element ref="DESCRIPTION" minOccurs="0"/>
<xs:element ref="VALUES" minOccurs="0"/>
<xs:element ref="LINK" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="unit" type="xs:token"/>
<xs:attribute name="datatype" type="dataType" use="required"/>
<xs:attribute name="precision" type="precType"/>
<xs:attribute name="width" type="xs:positiveInteger"/>
<xs:attribute name="ref" type="xs:IDREF"/>
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
<xs:attribute name="value" type="xs:string" use="required"/>
<xs:attribute name="arraysize" type="arrayDEF"/>
</xs:complexType>
</xs:element>
<!-- A TABLE is a sequence of FIELD/PARAMs and LINKS and DESCRIPTION,
possibly followed by a DATA section
-->
<xs:element name="TABLE">
<xs:complexType>
<xs:sequence>
<xs:element ref="DESCRIPTION" minOccurs="0"/>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="FIELD" />
<xs:element ref="PARAM" />
<xs:element ref="GROUP" />
</xs:choice>
<xs:element ref="LINK" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="DATA" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="name" type="xs:token"/>
<xs:attribute name="ref" type="xs:IDREF"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
<xs:attribute name="nrows" type="xs:nonNegativeInteger"/>
</xs:complexType>
</xs:element>
<!-- FIELD is the definition of what is in a column of the table -->
<xs:element name="FIELD">
<xs:complexType>
<xs:sequence> <!-- minOccurs="0" maxOccurs="unbounded" -->
<xs:element ref="DESCRIPTION" minOccurs="0"/>
<xs:element ref="VALUES" minOccurs="0"/> <!-- maxOccurs="2" -->
<xs:element ref="LINK" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="unit" type="xs:token"/>
<xs:attribute name="datatype" type="dataType" use="required"/>
<xs:attribute name="precision" type="precType"/>
<xs:attribute name="width" type="xs:positiveInteger"/>
<xs:attribute name="ref" type="xs:IDREF"/>
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
<xs:attribute name="arraysize" type="xs:string"/>
<xs:attribute name="type">
<!-- type is not in the Version 1.1, but is kept for
backward compatibility purposes
-->
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="hidden"/>
<xs:enumeration value="no_query"/>
<xs:enumeration value="trigger"/>
<xs:enumeration value="location"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:element>
<!-- GROUP groups columns; may include descriptions, fields/params/groups -->
<xs:element name="GROUP">
<xs:complexType>
<xs:sequence>
<xs:element ref="DESCRIPTION" minOccurs="0"/>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="FIELDref"/>
<xs:element ref="PARAMref"/>
<xs:element ref="PARAM"/>
<xs:element ref="GROUP"/>
</xs:choice>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="name" type="xs:token"/>
<xs:attribute name="ref" type="xs:IDREF"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
</xs:complexType>
</xs:element>
<!-- FIELDref and PARAMref are references to FIELD or PARAM defined
in the parent TABLE or RESOURCE -->
<xs:element name="FIELDref">
<xs:complexType>
<xs:attribute name="ref" type="xs:IDREF" use="required"/>
<!-- utype and maybe ucd could well be added there,
will be if necessary -->
</xs:complexType>
</xs:element>
<xs:element name="PARAMref">
<xs:complexType>
<xs:attribute name="ref" type="xs:IDREF" use="required"/>
<!-- utype and maybe ucd could well be added there,
will be if necessary -->
</xs:complexType>
</xs:element>
<!-- VALUES expresses the values that can be taken by the data
in a column or by a parameter
-->
<xs:element name="VALUES">
<xs:complexType>
<xs:sequence>
<xs:element ref="MIN" minOccurs="0"/>
<xs:element ref="MAX" minOccurs="0"/>
<xs:element ref="OPTION" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="type" default="legal">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="legal"/>
<xs:enumeration value="actual"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="null" type="xs:token"/>
<xs:attribute name="ref" type="xs:IDREF"/>
<!-- xs:attribute name="invalid" type="yesno" default="no"/ -->
</xs:complexType>
</xs:element>
<xs:element name="MIN">
<xs:complexType>
<xs:attribute name="value" type="xs:string" use="required"/>
<xs:attribute name="inclusive" type="yesno" default="yes"/>
</xs:complexType>
</xs:element>
<xs:element name="MAX">
<xs:complexType>
<xs:attribute name="value" type="xs:string" use="required"/>
<xs:attribute name="inclusive" type="yesno" default="yes"/>
</xs:complexType>
</xs:element>
<xs:element name="OPTION">
<xs:complexType>
<xs:sequence>
<xs:element ref="OPTION" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="name" type="xs:token"/>
<xs:attribute name="value" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
<!-- The LINK is a URL (href) or some other kind of reference (gref) -->
<xs:element name="LINK">
<xs:complexType mixed="true">
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="content-role">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="query"/>
<xs:enumeration value="hints"/>
<xs:enumeration value="doc"/>
<xs:enumeration value="location"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="content-type" type="xs:token"/>
<xs:attribute name="title" type="xs:string"/>
<xs:attribute name="value" type="xs:string"/>
<xs:attribute name="href" type="xs:anyURI"/>
<xs:attribute name="gref" type="xs:token"/><!-- Deprecated in V1.1 -->
<xs:attribute name="action" type="xs:anyURI"/>
</xs:complexType>
</xs:element>
<!-- DATA is the actual table data, in one of three formats -->
<xs:element name="DATA">
<xs:complexType>
<xs:choice>
<xs:element ref="TABLEDATA"/>
<xs:element ref="BINARY"/>
<xs:element ref="FITS"/>
</xs:choice>
</xs:complexType>
</xs:element>
<!-- Pure XML data -->
<xs:element name="TABLEDATA">
<xs:complexType>
<xs:sequence>
<xs:element ref="TR" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="TD">
<xs:complexType><xs:simpleContent>
<xs:extension base="xs:string">
<!-- xs:attribute name="ref" type="xs:IDREF"/ -->
<xs:attribute name="encoding" type="encodingType"/>
</xs:extension>
</xs:simpleContent></xs:complexType>
</xs:element>
<xs:element name="TR">
<xs:complexType>
<xs:sequence>
<xs:element ref="TD" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<!-- FITS file, perhaps with specification of which extension to seek to -->
<xs:element name="FITS">
<xs:complexType>
<xs:sequence>
<xs:element ref="STREAM"/>
</xs:sequence>
<xs:attribute name="extnum" type="xs:positiveInteger"/>
</xs:complexType>
</xs:element>
<!-- BINARY data format -->
<xs:element name="BINARY">
<xs:complexType>
<xs:sequence>
<xs:element ref="STREAM"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<!-- STREAM can be local or remote, encoded or not -->
<xs:element name="STREAM">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="type" default="locator">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="locator"/>
<xs:enumeration value="other"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="href" type="xs:anyURI"/>
<xs:attribute name="actuate" default="onRequest">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="onLoad"/>
<xs:enumeration value="onRequest"/>
<xs:enumeration value="other"/>
<xs:enumeration value="none"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="encoding" type="encodingType" default="none"/>
<xs:attribute name="expires" type="xs:dateTime"/>
<xs:attribute name="rights" type="xs:token"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<!-- Expresses the coordinate system we are using -->
<xs:element name="COOSYS">
<xs:complexType><xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="ID" type="xs:ID" use="required"/>
<xs:attribute name="equinox" type="astroYear"/>
<xs:attribute name="epoch" type="astroYear"/>
<xs:attribute name="system" default="eq_FK5">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="eq_FK4"/>
<xs:enumeration value="eq_FK5"/>
<xs:enumeration value="ICRS"/>
<xs:enumeration value="ecl_FK4"/>
<xs:enumeration value="ecl_FK5"/>
<xs:enumeration value="galactic"/>
<xs:enumeration value="supergalactic"/>
<xs:enumeration value="xy"/>
<xs:enumeration value="barycentric"/>
<xs:enumeration value="geo_app"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:extension></xs:simpleContent></xs:complexType>
</xs:element>
</xs:schema>
<?xml version="1.0" encoding="UTF-8"?>
<!--W3C Schema for VOTable = Virtual Observatory Tabular Format
.Version 1.0 : 15-Apr-2002
.Version 1.09: 23-Jan-2004 Version 1.09
.Version 1.09: 30-Jan-2004 Version 1.091
.Version 1.09: 22-Mar-2004 Version 1.092
.Version 1.094: 02-Jun-2004 GROUP does not contain FIELD
.Version 1.1 : 10-Jun-2004 remove the complexContent
.Version 1.11: GL: 23-May-2006 remove most root elements, use name= type= iso ref= structure
.Version 1.11: GL: 29-Aug-2006 review and added comments (prefixed by GL)
before sending to Francois Ochsenbein
.Version 1.12: FO: Preliminary Version 1.2
.Version 1.18: FO: Tested (jax) version 1.2
.Version 1.19: FO: Completed INFO attributes
.Version 1.20: FO: Added xtype; content-role is less restrictive (May2009)
.Version 1.20a: FO: PR-20090710 Cosmetics.
.Version 1.20b: FO: INFO does not accept sub-elements (2009-09-29)
.Version 1.20c: FO: elementFormDefault="qualified" to stay compatible with 1.1
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified"
xmlns="http://www.ivoa.net/xml/VOTable/v1.2"
targetNamespace="http://www.ivoa.net/xml/VOTable/v1.2"
>
<xs:annotation><xs:documentation>
VOTable1.2 is meant to serialize tabular documents in the
context of Virtual Observatory applications. This schema
corresponds to the VOTable document available from
http://www.ivoa.net/Documents/latest/VOT.html
</xs:documentation></xs:annotation>
<!-- Here we define some interesting new datatypes:
- anyTEXT may have embedded XHTML (conforming HTML)
- astroYear is an epoch in Besselian or Julian year, e.g. J2000
- arrayDEF specifies an array size e.g. 12x23x*
- dataType defines the acceptable datatypes
- ucdType defines the acceptable UCDs (UCD1+)
- precType defines the acceptable precisions
- yesno defines just the 2 alternatives
-->
<xs:complexType name="anyTEXT" mixed="true">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" processContents="skip"/>
</xs:sequence>
</xs:complexType>
<xs:simpleType name="astroYear">
<xs:restriction base="xs:token">
<xs:pattern value="[JB]?[0-9]+([.][0-9]*)?"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ucdType">
<xs:restriction base="xs:token">
<xs:annotation><xs:documentation>
Accept UCD1+
Accept also old UCD1 (but not / + %) including SIAP convention (with :)
</xs:documentation></xs:annotation>
<xs:pattern value="[A-Za-z0-9_.:;\-]*"/><!-- UCD1 use also / + % -->
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="arrayDEF">
<xs:restriction base="xs:token">
<xs:pattern value="([0-9]+x)*[0-9]*[*]?(s\W)?"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="encodingType">
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="gzip"/>
<xs:enumeration value="base64"/>
<xs:enumeration value="dynamic"/>
<xs:enumeration value="none"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="dataType">
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="boolean"/>
<xs:enumeration value="bit"/>
<xs:enumeration value="unsignedByte"/>
<xs:enumeration value="short"/>
<xs:enumeration value="int"/>
<xs:enumeration value="long"/>
<xs:enumeration value="char"/>
<xs:enumeration value="unicodeChar"/>
<xs:enumeration value="float"/>
<xs:enumeration value="double"/>
<xs:enumeration value="floatComplex"/>
<xs:enumeration value="doubleComplex"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="precType">
<xs:restriction base="xs:token">
<xs:pattern value="[EF]?[1-9][0-9]*"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="yesno">
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="yes"/>
<xs:enumeration value="no"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="Min">
<xs:attribute name="value" type="xs:string" use="required"/>
<xs:attribute name="inclusive" type="yesno" default="yes"/>
</xs:complexType>
<xs:complexType name="Max">
<xs:attribute name="value" type="xs:string" use="required"/>
<xs:attribute name="inclusive" type="yesno" default="yes"/>
</xs:complexType>
<xs:complexType name="Option">
<xs:sequence>
<xs:element name="OPTION" type="Option" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="name" type="xs:token"/>
<xs:attribute name="value" type="xs:string" use="required"/>
</xs:complexType>
<!-- VALUES expresses the values that can be taken by the data
in a column or by a parameter
-->
<xs:complexType name="Values">
<xs:sequence>
<xs:element name="MIN" type="Min" minOccurs="0"/>
<xs:element name="MAX" type="Max" minOccurs="0"/>
<xs:element name="OPTION" type="Option" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="type" default="legal">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="legal"/>
<xs:enumeration value="actual"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="null" type="xs:token"/>
<xs:attribute name="ref" type="xs:IDREF"/>
<!-- xs:attribute name="invalid" type="yesno" default="no"/ -->
</xs:complexType>
<!-- The LINK is a URL (href) or some other kind of reference (gref) -->
<xs:complexType name="Link">
<xs:annotation><xs:documentation>
content-role was previsouly restricted as: <![CDATA[
<xs:attribute name="content-role">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="query"/>
<xs:enumeration value="hints"/>
<xs:enumeration value="doc"/>
<xs:enumeration value="location"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>]]>; is now a name token.
</xs:documentation></xs:annotation>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="content-role" type="xs:NMTOKEN"/>
<xs:attribute name="content-type" type="xs:NMTOKEN"/>
<xs:attribute name="title" type="xs:string"/>
<xs:attribute name="value" type="xs:string"/>
<xs:attribute name="href" type="xs:anyURI"/>
<xs:attribute name="gref" type="xs:token"/><!-- Deprecated in V1.1 -->
<xs:attribute name="action" type="xs:anyURI"/>
</xs:complexType>
<!-- INFO is defined in Version 1.2 as a PARAM of String type
<xs:complexType name="Info">
<xs:complexContent>
<xs:restriction base="Param">
<xs:attribute name="unit" fixed=""/>
<xs:attribute name="datatype" fixed="char"/>
<xs:attribute name="arraysize" fixed="*"/>
</xs:restriction>
</xs:complexContent>
</xs:complexType>
-or- as a full definition:
<xs:complexType name="Info">
<xs:sequence>
<xs:element name="DESCRIPTION" type="anyTEXT" minOccurs="0"/>
<xs:element name="VALUES" type="Values" minOccurs="0"/>
<xs:element name="LINK" type="Link" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="value" type="xs:string" use="required"/>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="unit" type="xs:token"/>
<xs:attribute name="xtype" type="xs:token"/>
<xs:attribute name="ref" type="xs:IDREF"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
</xs:complexType>
-->
<!-- No sub-element is accepted in INFO for backward compatibility -->
<xs:complexType name="Info">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="value" type="xs:string" use="required"/>
<xs:attribute name="unit" type="xs:token"/>
<xs:attribute name="xtype" type="xs:token"/>
<xs:attribute name="ref" type="xs:IDREF"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<!-- Expresses the coordinate system we are using --><!-- Deprecated V1.2 -->
<xs:complexType name="CoordinateSystem">
<xs:annotation><xs:documentation>
Deprecated in Version 1.2
</xs:documentation></xs:annotation>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="ID" type="xs:ID" use="required"/>
<xs:attribute name="equinox" type="astroYear"/>
<xs:attribute name="epoch" type="astroYear"/>
<xs:attribute name="system" default="eq_FK5">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="eq_FK4"/>
<xs:enumeration value="eq_FK5"/>
<xs:enumeration value="ICRS"/>
<xs:enumeration value="ecl_FK4"/>
<xs:enumeration value="ecl_FK5"/>
<xs:enumeration value="galactic"/>
<xs:enumeration value="supergalactic"/>
<xs:enumeration value="xy"/>
<xs:enumeration value="barycentric"/>
<xs:enumeration value="geo_app"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="Definitions">
<xs:annotation><xs:documentation>
Deprecated in Version 1.1
</xs:documentation></xs:annotation>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="COOSYS" type="CoordinateSystem"/><!-- Deprecated in V1.2 -->
<xs:element name="PARAM" type="Param"/>
</xs:choice>
</xs:complexType>
<!-- FIELD is the definition of what is in a column of the table -->
<xs:complexType name="Field">
<xs:sequence> <!-- minOccurs="0" maxOccurs="unbounded" -->
<xs:element name="DESCRIPTION" type="anyTEXT" minOccurs="0"/>
<xs:element name="VALUES" type="Values" minOccurs="0"/> <!-- maxOccurs="2" -->
<xs:element name="LINK" type="Link" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="unit" type="xs:token"/>
<xs:attribute name="datatype" type="dataType" use="required"/>
<xs:attribute name="precision" type="precType"/>
<xs:attribute name="width" type="xs:positiveInteger"/>
<xs:attribute name="xtype" type="xs:token"/>
<xs:attribute name="ref" type="xs:IDREF"/>
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
<xs:attribute name="arraysize" type="xs:string"/>
<!-- GL: is the next deprecated element remaining
(is not in PARAM, but will in new model be inherited)
-->
<xs:attribute name="type">
<!-- type is not in the Version 1.1, but is kept for
backward compatibility purposes
-->
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="hidden"/>
<xs:enumeration value="no_query"/>
<xs:enumeration value="trigger"/>
<xs:enumeration value="location"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
<!-- A PARAM is similar to a FIELD, but it also has a "value" attribute -->
<!-- GL: implemented here as a subtype as suggested we do in Kyoto. -->
<xs:complexType name="Param">
<xs:complexContent>
<xs:extension base="Field">
<xs:attribute name="value" type="xs:string" use="required"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<!-- GROUP groups columns; may include descriptions, fields/params/groups -->
<xs:complexType name="Group">
<xs:sequence>
<xs:element name="DESCRIPTION" type="anyTEXT" minOccurs="0"/>
<!-- GL I guess I can understand the next choice element as one may (?)
really want to group fields and params and groups in a particular order.
-->
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="FIELDref" type="FieldRef"/>
<xs:element name="PARAMref" type="ParamRef"/>
<xs:element name="PARAM" type="Param"/>
<xs:element name="GROUP" type="Group"/>
<!-- GL a GroupRef could remove recursion -->
</xs:choice>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="name" type="xs:token"/>
<xs:attribute name="ref" type="xs:IDREF"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
</xs:complexType>
<!-- FIELDref and PARAMref are references to FIELD or PARAM defined
in the parent TABLE or RESOURCE -->
<!-- GL This can not be enforced in XML Schema, so why not IDREF in <Group> ?
In particular if the UCD and utype attributes will NOT be added -->
<xs:complexType name="FieldRef">
<xs:attribute name="ref" type="xs:IDREF" use="required"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
</xs:complexType>
<xs:complexType name="ParamRef">
<xs:attribute name="ref" type="xs:IDREF" use="required"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
</xs:complexType>
<!-- DATA is the actual table data, in one of three formats -->
<!--
GL in Kyoto we discussed the option of having the specific Data items
be subtypes of Data:
-->
<!--
<xs:complexType name="Data" abstract="true"/>
<xs:complexType name="TableData">
<xs:complexContent>
<xs:extension base="Data">
... etc
</xs:extension>
</xs:complexContent>
</xs:complexType>
-->
<xs:complexType name="Data">
<xs:annotation><xs:documentation>
Added in Version 1.2: INFO for diagnostics
</xs:documentation></xs:annotation>
<xs:sequence>
<xs:choice>
<xs:element name="TABLEDATA" type="TableData"/>
<xs:element name="BINARY" type="Binary"/>
<xs:element name="FITS" type="FITS"/>
</xs:choice>
<xs:element name="INFO" type="Info" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<!-- Pure XML data -->
<xs:complexType name="TableData">
<xs:sequence>
<xs:element name="TR" type="Tr" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="Td">
<xs:simpleContent>
<xs:extension base="xs:string">
<!-- xs:attribute name="ref" type="xs:IDREF"/ -->
<xs:annotation><xs:documentation>
The 'encoding' attribute is added here to avoid
problems of code generators which do not properly
interpret the TR/TD structures.
'encoding' was chosen because it appears in
appendix A.5
</xs:documentation></xs:annotation>
<xs:attribute name="encoding" type="encodingType"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="Tr">
<xs:annotation><xs:documentation>
The ID attribute is added here to the TR tag to avoid
problems of code generators which do not properly
interpret the TR/TD structures
</xs:documentation></xs:annotation>
<xs:sequence>
<xs:element name="TD" type="Td" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
</xs:complexType>
<!-- FITS file, perhaps with specification of which extension to seek to -->
<xs:complexType name="FITS">
<xs:sequence>
<xs:element name="STREAM" type="Stream"/>
</xs:sequence>
<xs:attribute name="extnum" type="xs:positiveInteger"/>
</xs:complexType>
<!-- BINARY data format -->
<xs:complexType name="Binary">
<xs:sequence>
<xs:element name="STREAM" type="Stream"/>
</xs:sequence>
</xs:complexType>
<!-- STREAM can be local or remote, encoded or not -->
<xs:complexType name="Stream">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="type" default="locator">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="locator"/>
<xs:enumeration value="other"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="href" type="xs:anyURI"/>
<xs:attribute name="actuate" default="onRequest">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="onLoad"/>
<xs:enumeration value="onRequest"/>
<xs:enumeration value="other"/>
<xs:enumeration value="none"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="encoding" type="encodingType" default="none"/>
<xs:attribute name="expires" type="xs:dateTime"/>
<xs:attribute name="rights" type="xs:token"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<!-- A TABLE is a sequence of FIELD/PARAMs and LINKS and DESCRIPTION,
possibly followed by a DATA section
-->
<xs:complexType name="Table">
<xs:annotation><xs:documentation>
Added in Version 1.2: INFO for diagnostics
</xs:documentation></xs:annotation>
<xs:sequence>
<xs:element name="DESCRIPTION" type="anyTEXT" minOccurs="0"/>
<!-- GL: why a choice iso for example -->
<!--
<xs:element name="PARAM" type="Param" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="FIELD" type="Field" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="GROUP" type="Group" minOccurs="0" maxOccurs="unbounded"/>
-->
<!--
This could also enforce groups to be defined after the fields and params
to which they must have a reference, which is somewhat more logical
-->
<!-- Added Version 1.2: -->
<xs:element name="INFO" type="Info" minOccurs="0" maxOccurs="unbounded"/>
<!-- An empty table without any FIELD/PARAM should not be acceptable -->
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="FIELD" type="Field"/>
<xs:element name="PARAM" type="Param"/>
<xs:element name="GROUP" type="Group"/>
</xs:choice>
<xs:element name="LINK" type="Link" minOccurs="0" maxOccurs="unbounded"/>
<!-- This would allow several DATA parts in a table (future extension?)
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="DATA" type="Data"/>
<xs:element name="INFO" type="Info" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
-->
<xs:element name="DATA" type="Data" minOccurs="0"/>
<xs:element name="INFO" type="Info" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="name" type="xs:token"/>
<xs:attribute name="ref" type="xs:IDREF"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
<xs:attribute name="nrows" type="xs:nonNegativeInteger"/>
</xs:complexType>
<!-- RESOURCES can contain DESCRIPTION, (INFO|PARAM|COSYS), LINK, TABLEs -->
<xs:complexType name="Resource">
<xs:annotation><xs:documentation>
Added in Version 1.2: INFO for diagnostics in several places
</xs:documentation></xs:annotation>
<xs:sequence>
<xs:element name="DESCRIPTION" type="anyTEXT" minOccurs="0"/>
<xs:element name="INFO" type="Info" minOccurs="0" maxOccurs="unbounded"/>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="COOSYS" type="CoordinateSystem"/><!-- Deprecated in V1.2 -->
<xs:element name="GROUP" type="Group" />
<xs:element name="PARAM" type="Param" />
</xs:choice>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="LINK" type="Link" minOccurs="0" maxOccurs="unbounded"/>
<xs:choice>
<xs:element name="TABLE" type="Table" />
<xs:element name="RESOURCE" type="Resource" />
</xs:choice>
<xs:element name="INFO" type="Info" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<!-- Suggested Doug Tody, to include new RESOURCE types -->
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="name" type="xs:token"/>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="utype" type="xs:string"/>
<xs:attribute name="type" default="results">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="results"/>
<xs:enumeration value="meta"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<!-- Suggested Doug Tody, to include new RESOURCE attributes -->
<xs:anyAttribute namespace="##other" processContents="lax"/>
</xs:complexType>
<!-- VOTable is the root element -->
<xs:element name="VOTABLE">
<xs:complexType>
<xs:sequence>
<xs:element name="DESCRIPTION" type="anyTEXT" minOccurs="0"/>
<xs:element name="DEFINITIONS" type="Definitions" minOccurs="0"/><!-- Deprecated -->
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="COOSYS" type="CoordinateSystem"/><!-- Deprecated in V1.2 -->
<xs:element name="GROUP" type="Group" />
<xs:element name="PARAM" type="Param" />
<xs:element name="INFO" type="Info" minOccurs="0" maxOccurs="unbounded"/>
</xs:choice>
<xs:element name="RESOURCE" type="Resource" minOccurs="1" maxOccurs="unbounded"/>
<xs:element name="INFO" type="Info" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="version">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="1.2"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>
<?xml version="1.0" encoding="UTF-8"?>
<!--W3C Schema for VOTable = Virtual Observatory Tabular Format
.Version 1.0 : 15-Apr-2002
.Version 1.09: 23-Jan-2004 Version 1.09
.Version 1.09: 30-Jan-2004 Version 1.091
.Version 1.09: 22-Mar-2004 Version 1.092
.Version 1.094: 02-Jun-2004 GROUP does not contain FIELD
.Version 1.1 : 10-Jun-2004 remove the complexContent
.Version 1.11: GL: 23-May-2006 remove most root elements, use name= type= iso ref= structure
.Version 1.11: GL: 29-Aug-2006 review and added comments (prefixed by GL)
before sending to Francois Ochsenbein
.Version 1.12: FO: Preliminary Version 1.2
.Version 1.18: FO: Tested (jax) version 1.2
.Version 1.19: FO: Completed INFO attributes
.Version 1.20: FO: Added xtype; content-role is less restrictive (May2009)
.Version 1.20a: FO: PR-20090710 Cosmetics.
.Version 1.20b: FO: INFO does not accept sub-elements (2009-09-29)
.Version 1.20c: FO: elementFormDefault="qualified" to stay compatible with 1.1
.Version 1.3: MT: Added BINARY2 element
.Version 1.3: MT: Further relaxed LINK content-role type to token
.Version 1.3-Erratum-2 MT: Made slight change to precType pattern
.Version 1.4pre1: MD: merged 1.3-Erratrum 2, added TIMESYS.
.Version 1.4wd-a: TD: updates for initial draft of v1.4.
.Version 1.4: TD: Change version to 1.4
.Version 1.5pre1: MD: adding refposition attribute and FIELDref/PARAMref
in COOSYS, which no longer is derived from xs:string, either.
.Version 1.5pre2: MD: vocabularising system attribute in COOSYS
.Version 1.5pre3: TD: Undo the FIELDref/PARAMref addition to COOSYS
.Version 1.5: TD: Update version to 1.5 for REC
-->
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified"
xmlns="http://www.ivoa.net/xml/VOTable/v1.3"
targetNamespace="http://www.ivoa.net/xml/VOTable/v1.3"
version="1.5"
>
<xs:annotation><xs:documentation>
VOTable is meant to serialize tabular documents in the
context of Virtual Observatory applications. This schema
corresponds to the VOTable document available from
http://www.ivoa.net/Documents/latest/VOT.html
</xs:documentation></xs:annotation>
<!-- Here we define some interesting new datatypes:
- anyTEXT may have embedded XHTML (conforming HTML)
- astroYear is an epoch in Besselian or Julian year, e.g. J2000
- arrayDEF specifies an array size e.g. 12x23x*
- dataType defines the acceptable datatypes
- ucdType defines the acceptable UCDs (UCD1+)
- precType defines the acceptable precisions
- yesno defines just the 2 alternatives
-->
<xs:complexType name="anyTEXT" mixed="true">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" processContents="skip"/>
</xs:sequence>
</xs:complexType>
<xs:simpleType name="astroYear">
<xs:restriction base="xs:token">
<xs:pattern value="[JB]?[0-9]+([.][0-9]*)?"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ucdType">
<xs:restriction base="xs:token">
<xs:annotation><xs:documentation>
Accept UCD1+
Accept also old UCD1 (but not / + %) including SIAP convention (with :)
</xs:documentation></xs:annotation>
<xs:pattern value="[A-Za-z0-9_.:;\-]*"/><!-- UCD1 use also / + % -->
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="arrayDEF">
<xs:restriction base="xs:token">
<xs:pattern value="([0-9]+x)*[0-9]*[*]?(s\W)?"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="encodingType">
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="gzip"/>
<xs:enumeration value="base64"/>
<xs:enumeration value="dynamic"/>
<xs:enumeration value="none"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="dataType">
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="boolean"/>
<xs:enumeration value="bit"/>
<xs:enumeration value="unsignedByte"/>
<xs:enumeration value="short"/>
<xs:enumeration value="int"/>
<xs:enumeration value="long"/>
<xs:enumeration value="char"/>
<xs:enumeration value="unicodeChar"/>
<xs:enumeration value="float"/>
<xs:enumeration value="double"/>
<xs:enumeration value="floatComplex"/>
<xs:enumeration value="doubleComplex"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="precType">
<xs:restriction base="xs:token">
<xs:pattern value="[EF]?[0-9][0-9]*"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="yesno">
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="yes"/>
<xs:enumeration value="no"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="Min">
<xs:attribute name="value" type="xs:string" use="required"/>
<xs:attribute name="inclusive" type="yesno" default="yes"/>
</xs:complexType>
<xs:complexType name="Max">
<xs:attribute name="value" type="xs:string" use="required"/>
<xs:attribute name="inclusive" type="yesno" default="yes"/>
</xs:complexType>
<xs:complexType name="Option">
<xs:sequence>
<xs:element name="OPTION" type="Option" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="name" type="xs:token"/>
<xs:attribute name="value" type="xs:string" use="required"/>
</xs:complexType>
<!-- VALUES expresses the values that can be taken by the data
in a column or by a parameter
-->
<xs:complexType name="Values">
<xs:sequence>
<xs:element name="MIN" type="Min" minOccurs="0"/>
<xs:element name="MAX" type="Max" minOccurs="0"/>
<xs:element name="OPTION" type="Option" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="type" default="legal">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="legal"/>
<xs:enumeration value="actual"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="null" type="xs:token"/>
<xs:attribute name="ref" type="xs:IDREF"/>
<!-- xs:attribute name="invalid" type="yesno" default="no"/ -->
</xs:complexType>
<!-- The LINK is a URL (href) or some other kind of reference (gref) -->
<xs:complexType name="Link">
<xs:annotation><xs:documentation>
content-role was previsouly restricted as: <![CDATA[
<xs:attribute name="content-role">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="query"/>
<xs:enumeration value="hints"/>
<xs:enumeration value="doc"/>
<xs:enumeration value="location"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>]]>; is now a token.
</xs:documentation></xs:annotation>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="content-role" type="xs:token"/>
<xs:attribute name="content-type" type="xs:token"/>
<xs:attribute name="title" type="xs:string"/>
<xs:attribute name="value" type="xs:string"/>
<xs:attribute name="href" type="xs:anyURI"/>
<xs:attribute name="gref" type="xs:token"/><!-- Deprecated in V1.1 -->
<xs:attribute name="action" type="xs:anyURI"/>
</xs:complexType>
<!-- INFO is defined in Version 1.2 as a PARAM of String type
<xs:complexType name="Info">
<xs:complexContent>
<xs:restriction base="Param">
<xs:attribute name="unit" fixed=""/>
<xs:attribute name="datatype" fixed="char"/>
<xs:attribute name="arraysize" fixed="*"/>
</xs:restriction>
</xs:complexContent>
</xs:complexType>
-or- as a full definition:
<xs:complexType name="Info">
<xs:sequence>
<xs:element name="DESCRIPTION" type="anyTEXT" minOccurs="0"/>
<xs:element name="VALUES" type="Values" minOccurs="0"/>
<xs:element name="LINK" type="Link" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="value" type="xs:string" use="required"/>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="unit" type="xs:token"/>
<xs:attribute name="xtype" type="xs:token"/>
<xs:attribute name="ref" type="xs:IDREF"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
</xs:complexType>
-->
<!-- No sub-element is accepted in INFO for backward compatibility -->
<xs:complexType name="Info">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="value" type="xs:string" use="required"/>
<xs:attribute name="unit" type="xs:token"/>
<xs:attribute name="xtype" type="xs:token"/>
<xs:attribute name="ref" type="xs:IDREF"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<!-- Expresses the coordinate system we are using -->
<xs:complexType name="CoordinateSystem">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="ID" type="xs:ID" use="required"/>
<xs:attribute name="equinox" type="astroYear"/>
<xs:attribute name="epoch" type="astroYear"/>
<xs:attribute name="system" default="FK5">
<xs:annotation>
<xs:documentation>
Values for this attribute must be taken from the IVOA
refframe vocabulary, http://www.ivoa.net/rdf/refframe
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="refposition" type="xs:token">
<xs:annotation>
<xs:documentation>
The reference position SHOULD be taken from the IVOA
refposition vocabulary (http://www.ivoa.net/rdf/refposition).
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:simpleType name="Timeorigin">
<xs:annotation>
<xs:documentation>
This is a time origin of a time coordinate, given as a
Julian Date for the the time scale and reference point
defined. It is usually given as a floating point
literal; for convenience, the magic strings “MJD-origin”
(standing for 2400000.5) and “JD-origin” (standing for 0)
are also allowed.
</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:token">
<xs:pattern value="[+-]?([0-9]+\.?[0-9]*|\.[0-9]+)([eE][+-]?[0-9]+)?|(JD|MJD)-origin">
</xs:pattern>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="TimeSystem">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="ID" type="xs:ID" use="required"/>
<xs:attribute name="timeorigin" type="Timeorigin">
<xs:annotation>
<xs:documentation>
The time origin is the offset or the time coordinate to Julian
Date. The timeorigin attribute MUST be given unless the time's
representation contains a year of a calendar era, in which case it
MUST NOT be present.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="timescale" use="required" type="xs:token">
<xs:annotation>
<xs:documentation>
This is the time scale used. Values SHOULD be
taken from the IVOA timescale vocabulary (http://www.ivoa.net/rdf/timescale).
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="refposition" use="required" type="xs:token">
<xs:annotation>
<xs:documentation>
The reference position SHOULD be taken from the IVOA
refposition vocabulary (http://www.ivoa.net/rdf/refposition).
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="Definitions">
<xs:annotation><xs:documentation>
Deprecated in Version 1.1
</xs:documentation></xs:annotation>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="COOSYS" type="CoordinateSystem"/>
<xs:element name="TIMESYS" type="TimeSystem"/>
<xs:element name="PARAM" type="Param"/>
</xs:choice>
</xs:complexType>
<!-- FIELD is the definition of what is in a column of the table -->
<xs:complexType name="Field">
<xs:sequence> <!-- minOccurs="0" maxOccurs="unbounded" -->
<xs:element name="DESCRIPTION" type="anyTEXT" minOccurs="0"/>
<xs:element name="VALUES" type="Values" minOccurs="0"/> <!-- maxOccurs="2" -->
<xs:element name="LINK" type="Link" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="unit" type="xs:token"/>
<xs:attribute name="datatype" type="dataType" use="required"/>
<xs:attribute name="precision" type="precType"/>
<xs:attribute name="width" type="xs:positiveInteger"/>
<xs:attribute name="xtype" type="xs:token"/>
<xs:attribute name="ref" type="xs:IDREF"/>
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
<xs:attribute name="arraysize" type="xs:string"/>
<!-- GL: is the next deprecated element remaining
(is not in PARAM, but will in new model be inherited)
-->
<xs:attribute name="type">
<!-- type is not in the Version 1.1, but is kept for
backward compatibility purposes
-->
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="hidden"/>
<xs:enumeration value="no_query"/>
<xs:enumeration value="trigger"/>
<xs:enumeration value="location"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
<!-- A PARAM is similar to a FIELD, but it also has a "value" attribute -->
<!-- GL: implemented here as a subtype as suggested we do in Kyoto. -->
<xs:complexType name="Param">
<xs:complexContent>
<xs:extension base="Field">
<xs:attribute name="value" type="xs:string" use="required"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<!-- GROUP groups columns; may include descriptions, fields/params/groups -->
<xs:complexType name="Group">
<xs:sequence>
<xs:element name="DESCRIPTION" type="anyTEXT" minOccurs="0"/>
<!-- GL I guess I can understand the next choice element as one may (?)
really want to group fields and params and groups in a particular order.
-->
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="FIELDref" type="FieldRef"/>
<xs:element name="PARAMref" type="ParamRef"/>
<xs:element name="PARAM" type="Param"/>
<xs:element name="GROUP" type="Group"/>
<!-- GL a GroupRef could remove recursion -->
</xs:choice>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="name" type="xs:token"/>
<xs:attribute name="ref" type="xs:IDREF"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
</xs:complexType>
<!-- FIELDref and PARAMref are references to FIELD or PARAM defined
in the parent TABLE or RESOURCE -->
<!-- GL This can not be enforced in XML Schema, so why not IDREF in <Group> ?
In particular if the UCD and utype attributes will NOT be added -->
<xs:complexType name="FieldRef">
<xs:attribute name="ref" type="xs:IDREF" use="required"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
</xs:complexType>
<xs:complexType name="ParamRef">
<xs:attribute name="ref" type="xs:IDREF" use="required"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
</xs:complexType>
<!-- DATA is the actual table data, in one of three formats -->
<!--
GL in Kyoto we discussed the option of having the specific Data items
be subtypes of Data:
-->
<!--
<xs:complexType name="Data" abstract="true"/>
<xs:complexType name="TableData">
<xs:complexContent>
<xs:extension base="Data">
... etc
</xs:extension>
</xs:complexContent>
</xs:complexType>
-->
<xs:complexType name="Data">
<xs:annotation><xs:documentation>
Added in Version 1.2: INFO for diagnostics
</xs:documentation></xs:annotation>
<xs:sequence>
<xs:choice>
<xs:element name="TABLEDATA" type="TableData"/>
<xs:element name="BINARY" type="Binary"/>
<xs:element name="BINARY2" type="Binary2"/>
<xs:element name="FITS" type="FITS"/>
</xs:choice>
<xs:element name="INFO" type="Info" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<!-- Pure XML data -->
<xs:complexType name="TableData">
<xs:sequence>
<xs:element name="TR" type="Tr" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="Td">
<xs:simpleContent>
<xs:extension base="xs:string">
<!-- xs:attribute name="ref" type="xs:IDREF"/ -->
<xs:annotation><xs:documentation>
The 'encoding' attribute is added here to avoid
problems of code generators which do not properly
interpret the TR/TD structures.
'encoding' was chosen because it appears in
appendix A.5
</xs:documentation></xs:annotation>
<xs:attribute name="encoding" type="encodingType"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:complexType name="Tr">
<xs:annotation><xs:documentation>
The ID attribute is added here to the TR tag to avoid
problems of code generators which do not properly
interpret the TR/TD structures
</xs:documentation></xs:annotation>
<xs:sequence>
<xs:element name="TD" type="Td" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
</xs:complexType>
<!-- FITS file, perhaps with specification of which extension to seek to -->
<xs:complexType name="FITS">
<xs:sequence>
<xs:element name="STREAM" type="Stream"/>
</xs:sequence>
<xs:attribute name="extnum" type="xs:positiveInteger"/>
</xs:complexType>
<!-- BINARY data format -->
<xs:complexType name="Binary">
<xs:sequence>
<xs:element name="STREAM" type="Stream"/>
</xs:sequence>
</xs:complexType>
<!-- BINARY2 data format -->
<xs:complexType name="Binary2">
<xs:sequence>
<xs:element name="STREAM" type="Stream"/>
</xs:sequence>
</xs:complexType>
<!-- STREAM can be local or remote, encoded or not -->
<xs:complexType name="Stream">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="type" default="locator">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="locator"/>
<xs:enumeration value="other"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="href" type="xs:anyURI"/>
<xs:attribute name="actuate" default="onRequest">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="onLoad"/>
<xs:enumeration value="onRequest"/>
<xs:enumeration value="other"/>
<xs:enumeration value="none"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="encoding" type="encodingType" default="none"/>
<xs:attribute name="expires" type="xs:dateTime"/>
<xs:attribute name="rights" type="xs:token"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<!-- A TABLE is a sequence of FIELD/PARAMs and LINKS and DESCRIPTION,
possibly followed by a DATA section
-->
<xs:complexType name="Table">
<xs:annotation><xs:documentation>
Added in Version 1.2: INFO for diagnostics
</xs:documentation></xs:annotation>
<xs:sequence>
<xs:element name="DESCRIPTION" type="anyTEXT" minOccurs="0"/>
<!-- GL: why a choice iso for example -->
<!--
<xs:element name="PARAM" type="Param" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="FIELD" type="Field" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="GROUP" type="Group" minOccurs="0" maxOccurs="unbounded"/>
-->
<!--
This could also enforce groups to be defined after the fields and params
to which they must have a reference, which is somewhat more logical
-->
<!-- Added Version 1.2: -->
<xs:element name="INFO" type="Info" minOccurs="0" maxOccurs="unbounded"/>
<!-- An empty table without any FIELD/PARAM should not be acceptable -->
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="FIELD" type="Field"/>
<xs:element name="PARAM" type="Param"/>
<xs:element name="GROUP" type="Group"/>
</xs:choice>
<xs:element name="LINK" type="Link" minOccurs="0" maxOccurs="unbounded"/>
<!-- This would allow several DATA parts in a table (future extension?)
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="DATA" type="Data"/>
<xs:element name="INFO" type="Info" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
-->
<xs:element name="DATA" type="Data" minOccurs="0"/>
<xs:element name="INFO" type="Info" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="name" type="xs:token"/>
<xs:attribute name="ref" type="xs:IDREF"/>
<xs:attribute name="ucd" type="ucdType"/>
<xs:attribute name="utype" type="xs:string"/>
<xs:attribute name="nrows" type="xs:nonNegativeInteger"/>
</xs:complexType>
<!-- RESOURCES can contain DESCRIPTION, (INFO|PARAM|COSYS), LINK, TABLEs -->
<xs:complexType name="Resource">
<xs:annotation><xs:documentation>
Added in Version 1.2: INFO for diagnostics in several places
</xs:documentation></xs:annotation>
<xs:sequence>
<xs:element name="DESCRIPTION" type="anyTEXT" minOccurs="0"/>
<xs:element name="INFO" type="Info" minOccurs="0" maxOccurs="unbounded"/>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="COOSYS" type="CoordinateSystem"/>
<xs:element name="TIMESYS" type="TimeSystem"/>
<xs:element name="GROUP" type="Group" />
<xs:element name="PARAM" type="Param" />
</xs:choice>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="LINK" type="Link" minOccurs="0" maxOccurs="unbounded"/>
<xs:choice>
<xs:element name="TABLE" type="Table" />
<xs:element name="RESOURCE" type="Resource" />
</xs:choice>
<xs:element name="INFO" type="Info" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<!-- Suggested Doug Tody, to include new RESOURCE types -->
<xs:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="name" type="xs:token"/>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="utype" type="xs:string"/>
<xs:attribute name="type" default="results">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="results"/>
<xs:enumeration value="meta"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<!-- Suggested Doug Tody, to include new RESOURCE attributes -->
<xs:anyAttribute namespace="##other" processContents="lax"/>
</xs:complexType>
<!-- VOTable is the root element -->
<xs:element name="VOTABLE">
<xs:complexType>
<xs:sequence>
<xs:element name="DESCRIPTION" type="anyTEXT" minOccurs="0"/>
<xs:element name="DEFINITIONS" type="Definitions" minOccurs="0"/><!-- Deprecated -->
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="COOSYS" type="CoordinateSystem"/>
<xs:element name="TIMESYS" type="TimeSystem"/>
<xs:element name="GROUP" type="Group" />
<xs:element name="PARAM" type="Param" />
<xs:element name="INFO" type="Info" minOccurs="0" maxOccurs="unbounded"/>
</xs:choice>
<xs:element name="RESOURCE" type="Resource" minOccurs="1" maxOccurs="unbounded"/>
<xs:element name="INFO" type="Info" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="ID" type="xs:ID"/>
<xs:attribute name="version">
<xs:simpleType>
<xs:restriction base="xs:NMTOKEN">
<xs:enumeration value="1.3"/>
<xs:enumeration value="1.4"/>
<xs:enumeration value="1.5"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>
+30
-0

@@ -0,1 +1,31 @@

1.8 (2025-11-13)
================
Enhancements and Fixes
----------------------
- No longer show a warning when an overflow status is encountered with a maxrec
set by the user [#690]
- Add support for the jobInfo element for UWS jobs [#679]
- Fix DALOverflowWarning error message to only indicate the cause as limits from
user or server. [#689]
- Add retry option to AsyncTAPJob.fetch_result for transient failures [#696]
- Improve the ``MivotViewer`` API: adding support of multiple mapped objects
per row and a partial redesign of the public API. Also, improved the gateway
between annotations and ``SkyCoord`` objects. [#698]
- Adding "hats" and "hips" servicetype shorthands for registry searches for
HATS and HIPS resources. [#706]
Deprecations and Removals
-------------------------
- Upgrade of the ``MivotViewer`` API including the removal of the
``xml_viewer`` module. [#698]
1.7.1 (2025-10-16)

@@ -2,0 +32,0 @@ ==================

+14
-4

@@ -351,10 +351,20 @@ .. _pyvo-data-access:

To retrieve more rows than that (often conservative) default limit, you
must override maxrec in the call to ``search``. A warning can be expected if
you reach the ``maxrec`` limit:
must override maxrec in the call to ``search``. PyVO will only warn about
truncation when it's unexpected. If you request 5 records and get 5 records,
no warning is issued:
.. doctest-remote-data::
>>> tap_results = tap_service.search("SELECT * FROM arihip.main", maxrec=5) # doctest: +SHOW_WARNINGS
DALOverflowWarning: Partial result set. Potential causes MAXREC, async storage space, etc.
>>> tap_results = tap_service.search("SELECT * FROM arihip.main", maxrec=5)
>>> len(tap_results)
5
However, if results are truncated by server limits without you specifying
maxrec, you'll receive a helpful warning:
.. doctest-remote-data::
>>> tap_results = tap_service.search("SELECT * FROM arihip.main") # doctest: +SHOW_WARNINGS
DALOverflowWarning: Results truncated due to server limits. Consider setting a maxrec value.
Services will not let you raise maxrec beyond the hard match limit:

@@ -361,0 +371,0 @@

@@ -106,2 +106,15 @@ ******************************************

What is JobInfo?
================
The :class:`~pyvo.io.uws.tree.JobInfo` element is an extensible container
that allows UWS implementations to include arbitrary, service-specific information
about a job. This for example can be used to provide:
* **Implementation-specific metadata** about job execution like progress info
* **Service-specific configuration** or diagnostic information
* **Extended error details** or debugging information
JobInfo exposes this information in a possibly hierarchical dictionary.
Working with UWS Jobs

@@ -140,2 +153,11 @@ =====================

... </results>
... <jobInfo>
... <rowsReturned>1234</rowsReturned>
... <executionTime>133.333</executionTime>
... <cpuTime>98.765</cpuTime>
... <queuePosition>0</queuePosition>
... <estimatedDuration>120</estimatedDuration>
... <nodeId>compute-node-03</nodeId>
... <serviceVersion>1.1</serviceVersion>
... </jobInfo>
... </job>'''

@@ -211,2 +233,106 @@ >>>

Working with JobInfo
====================
JobInfo provides access to service-specific metadata about job execution.
Basic JobInfo Access
--------------------
>>> if job.jobinfo:
... # RECOMMENDED: Dict-like access for expected elements
... try:
... rows_returned = job.jobinfo['rowsReturned']
... execution_time = job.jobinfo['executionTime']
... print(f"Query returned {rows_returned.value:,} rows in {execution_time.value:.1f} seconds")
... except KeyError as e:
... print(f"Missing expected element: {e}")
...
... # Use .get() with defaults for optional elements
... queue_pos = job.jobinfo.get('queuePosition', None)
... node_id = job.jobinfo.get('nodeId', None)
...
... if queue_pos:
... print(f"Final queue position: {queue_pos.value}")
...
... if node_id:
... print(f"Executed on: {node_id.text}")
Query returned 1,234 rows in 133.3 seconds
Final queue position: 0
Executed on: compute-node-03
.. note::
**API Usage Recommendation**: When accessing jobInfo elements, prefer dict-like
access (``job.jobinfo['element']``) for expected elements, as this provides
clearer error handling with KeyError exceptions. Use ``.get()`` with default
values for optional elements. See the jobInfo examples for detailed patterns.
Understanding .value vs .text
-----------------------------
JobInfo elements provide two ways to access their content:
>>> if job.jobinfo:
... duration_elem = job.jobinfo['estimatedDuration']
...
... # .text returns the raw string content
... print(f"Raw text: '{duration_elem.text}'")
...
... # .value attempts automatic type conversion (int, float, or string)
... print(f"Converted value: {duration_elem.value}")
... print(f"Type: {type(duration_elem.value)}")
...
... # For elements with string content that shouldn't be converted
... node_elem = job.jobinfo['nodeId']
... print(f"Node ID text: {node_elem.text}")
... print(f"Node ID value: {node_elem.value}")
Raw text: '120'
Converted value: 120
Type: <class 'int'>
Node ID text: compute-node-03
Node ID value: compute-node-03
Best Practices for JobInfo Access
---------------------------------
>>> def safe_jobinfo_access(job):
... """Demonstrates safe jobInfo access patterns"""
... if not job.jobinfo:
... print("No jobInfo available")
... return
...
... # Pattern 1: Expected elements with error handling
... try:
... rows = job.jobinfo['rowsReturned']
... exec_time = job.jobinfo['executionTime']
... print(f"Processed {rows.value:,} rows in {exec_time.value:.1f} seconds")
... except KeyError as e:
... print(f"Required statistics missing: {e}")
...
... # Pattern 2: Optional elements with defaults
... queue_pos = job.jobinfo.get('queuePosition', 'unknown')
... priority = job.jobinfo.get('priority', 'normal')
...
... # Pattern 3: Check existence before accessing
... if 'nodeId' in job.jobinfo:
... node = job.jobinfo['nodeId']
... print(f"Executed on node: {node.text}")
...
... # Pattern 4: Iterate through all available elements
... print("All jobInfo elements:")
... for key in job.jobinfo.keys():
... element = job.jobinfo[key]
... print(f" {key}: {element.text}")
>>> safe_jobinfo_access(job)
Processed 1,234 rows in 133.3 seconds
Executed on node: compute-node-03
All jobInfo elements:
rowsReturned: 1234
executionTime: 133.333
cpuTime: 98.765
queuePosition: 0
estimatedDuration: 120
nodeId: compute-node-03
serviceVersion: 1.1
Job Status Monitoring

@@ -226,2 +352,9 @@ =====================

...
... # Access jobInfo safely
... if job.jobinfo:
... try:
... rows = job.jobinfo['rowsReturned']
... print(f"Total rows returned: {rows.value:,}")
... except KeyError:
... print("Row count not available")
... elif job.phase == 'ERROR' and job.errorsummary and job.errorsummary.message:

@@ -233,2 +366,3 @@ ... print(f"Job failed: {job.errorsummary.message.content}")

Total job runtime: 2.3 minutes
Total rows returned: 1,234

@@ -336,2 +470,11 @@ Working with Job Lists

<jobInfo>
<rowsReturned>1234</rowsReturned>
<executionTime>133.333</executionTime>
<cpuTime>98.765</cpuTime>
<queuePosition>0</queuePosition>
<estimatedDuration>120</estimatedDuration>
<nodeId>compute-node-03</nodeId>
<serviceVersion>1.1</serviceVersion>
</jobInfo>
</job>

@@ -0,1 +1,3 @@

.. _mivot-examples:
************************************************************

@@ -5,3 +7,3 @@ MIVOT (``pyvo.mivot``): How to use annotated data - Examples

Photometric properties readout
Photometric Properties Readout
==============================

@@ -26,30 +28,28 @@

.. code-block:: python
.. doctest-skip::
import pytest
from pyvo.utils import activate_features
from pyvo.dal import TAPService
from pyvo.mivot.utils.xml_utils import XmlUtils
from pyvo.mivot.utils.dict_utils import DictUtils
from pyvo.mivot.viewer.mivot_viewer import MivotViewer
>>> from pyvo.utils import activate_features
>>> from pyvo.dal import TAPService
>>> from pyvo.mivot.utils.xml_utils import XmlUtils
>>> from pyvo.mivot.viewer.mivot_viewer import MivotViewer
>>>
>>> # Enable MIVOT-specific features in the pyvo library
>>> activate_features("MIVOT")
>>>
>>> service = TAPService('https://xcatdb.unistra.fr/xtapdb')
>>> result = service.run_sync(
... """
... SELECT TOP 5 * FROM "public".mergedentry
... """,
... format="application/x-votable+xml;content=mivot"
... )
>>>
>>> # The MIVOT viewer generates the model view of the data
>>> m_viewer = MivotViewer(result, resolve_ref=True)
>>>
>>> # Print out the Mivot annotations read out of the VOtable
>>> # This statement is just for a pedagogic purpose (access to a private attribute)
>>> XmlUtils.pretty_print(m_viewer._mapping_block)
# Enable MIVOT-specific features in the pyvo library
activate_features("MIVOT")
service = TAPService('https://xcatdb.unistra.fr/xtapdb')
result = service.run_sync(
"""
SELECT TOP 5 * FROM "public".mergedentry
""",
format="application/x-votable+xml;content=mivot"
)
# The MIVOT viewer generates the model view of the data
m_viewer = MivotViewer(result, resolve_ref=True)
# Print out the Mivot annotations read out of the VOtable
# This statement is just for a pedagogic purpose (access to a private attribute)
XmlUtils.pretty_print(m_viewer._mapping_block)
In this first step we just queried the service and we built the object that will process the Mivot annotations.

@@ -68,44 +68,41 @@ The Mivot block printing output is too long to be listed here. However, the screenshot below shows its shallow structure.

At instantiation time, the viewer reads the first data row, which must exist,
in order to construct a Python object that reflects the mapped model.
in order to construct the Python objects that reflect the mapped models and
to make the data available through them.
.. code-block:: python
.. doctest-skip::
# Build a Python object matching the TEMPLATES content and
# which leaves are set with the values of the first row
mango_object = m_viewer.dm_instance
>>> # Discover the Python objects matching the TEMPLATES content
>>> for dm_instance in m_viewer.dm_instances;
>>> print(dm_instance)
<MivotInstance: dmtype="mango:MangoObject">
# Print out the content of the Python object
# This statement is just for a pedagogic purpose
DictUtils.print_pretty_json(mango_object.to_dict())
The annotations are consumed by this dynamic Python object which leaves are set with the data of the current row.
You can explore the structure of this object by using the printed dictionary or standard object paths as shown below.
You can explore the structure of this object by using standard object paths as shown below.
Now, we can iterate through the table data and retrieve an updated Mivot instance for each row.
.. code-block:: python
.. doctest-skip::
while m_viewer.next_row_view():
if mango_object.dmtype == "mango:MangoObject":
print(f"Read source {mango_object.identifier.value} {mango_object.dmtype}")
for mango_property in mango_object.propertyDock:
if mango_property.dmtype == "mango:Brightness":
if mango_property.value.value:
mag_value = mango_property.value.value
mag_error = mango_property.error.sigma.value
phot_cal = mango_property.photCal
spectral_location = phot_cal.photometryFilter.spectralLocation
mag_filter = phot_cal.identifier.value
spectral_location = phot_cal.photometryFilter.spectralLocation
mag_wl = spectral_location.value.value
sunit = spectral_location.unitexpression.value
print(f" flux at {mag_wl} {sunit} (filter {mag_filter}) is {mag_value:.2e} +/- {mag_error:.2e}")
Read source 4XMM J054329.3-682106 mango:MangoObject
>>> mango_object = m_viewer.dm_instances[0]
>>> while m_viewer.next_row_view():
>>> if mango_object.dmtype == "mango:MangoObject":
>>> print(f"Read source {mango_object.identifier.value} {mango_object.dmtype}")
>>> for mango_property in mango_object.propertyDock:
>>> if mango_property.dmtype == "mango:Brightness":
>>> if mango_property.value.value:
>>> mag_value = mango_property.value.value
>>> mag_error = mango_property.error.sigma.value
>>> phot_cal = mango_property.photCal
>>> spectral_location = phot_cal.photometryFilter.spectralLocation
>>> mag_filter = phot_cal.identifier.value
>>> spectral_location = phot_cal.photometryFilter.spectralLocation
>>> mag_wl = spectral_location.value.value
>>> sunit = spectral_location.unitexpression.value
>>> print(f" flux at {mag_wl} {sunit} (filter {mag_filter}) is {mag_value:.2e} +/- {mag_error:.2e}")
Read source 4XMM J054329.3-682106 mango:MangoObject
flux at 0.35 keV (filter XMM/EPIC/EB1) is 8.35e-14 +/- 3.15e-14
flux at 0.75 keV (filter XMM/EPIC/EB2) is 3.26e-15 +/- 5.45e-15
flux at 6.1 keV (filter XMM/EPIC/EB8) is 8.68e-14 +/- 6.64e-14
...
...
...
...

@@ -119,7 +116,7 @@ The same code can easily be connected with matplotlib to plot SEDs as shown below (code not provided).

It is to noted that the current table row keeps available through the Mivot viewer.
It is to be noted that the current table row keeps available through the Mivot viewer.
.. code-block:: python
.. code-block:: python
row = m_viewer.table_row
row = m_viewer.table_row

@@ -136,3 +133,3 @@

EpochPosition property readout
EpochPosition Property Readout
==============================

@@ -146,3 +143,3 @@

.. warning::
At the time of writing, Vizier only mapped positions and proper motions (when available),
At the time of writing (Q1 2025), Vizier only mapped positions and proper motions (when available),
and the definitive epoch class had not been adopted.

@@ -153,48 +150,54 @@ Therefore, this implementation may differ a little bit from the standard model.

but rather lists them in the Mivot *TEMPLATES*.
The annotation reader must support both designs.
The annotation reader supports both designs.
In the first step below, we run a standard cone search query by using the standard PyVO API.
Once the query is finished, we can get a reference to the object that will process the Mivot annotations.
.. code-block:: python
.. doctest-skip::
import pytest
import astropy.units as u
from astropy.coordinates import SkyCoord
from pyvo.dal.scs import SCSService
>>> import astropy.units as u
>>> from astropy.coordinates import SkyCoord
>>> from pyvo.dal.scs import SCSService
>>> from pyvo.utils import activate_features
>>> from pyvo.mivot.viewer.mivot_viewer import MivotViewer
>>> from pyvo.mivot.features.sky_coord_builder import SkyCoordBuilder
>>>
>>> # Enable MIVOT-specific features in the pyvo library
>>> activate_features("MIVOT")
>>>
>>> scs_srv = SCSService("https://vizier.cds.unistra.fr/viz-bin/conesearch/V1.5/I/239/hip_main")
>>>
>>> query_result = scs_srv.search(
... pos=SkyCoord(ra=52.26708 * u.degree, dec=59.94027 * u.degree, frame='icrs'),
... radius=0.5)
>>>
>>> # The MIVOT viewer generates the model view of the data
>>> m_viewer = MivotViewer(query_result, resolve_ref=True)
from pyvo.utils import activate_features
from pyvo.mivot.viewer.mivot_viewer import MivotViewer
from pyvo.mivot.features.sky_coord_builder import SkyCoordBuilder
from pyvo.mivot.utils.dict_utils import DictUtils
We can now discover which data model classes the data is mapped to.
# Enable MIVOT-specific features in the pyvo library
activate_features("MIVOT")
.. doctest-skip::
scs_srv = SCSService("https://vizier.cds.unistra.fr/viz-bin/conesearch/V1.5/I/239/hip_main")
>>> # Get a set of Python objects matching the TEMPLATES content and
>>> # which leaves are set with the values of the first row
>>> for dm_instance in m_viewer.dm_instances;
>>> print(dm_instance)
<MivotInstance: dmtype="mango:EpochPosition">
query_result = scs_srv.search(
pos=SkyCoord(ra=52.26708 * u.degree, dec=59.94027 * u.degree, frame='icrs'),
radius=0.5)
The first instance can be accessed by the ``m_viewer.dm_instance`` getter.
This is a simple shorcut aiming at simplifying the code.
# The MIVOt viewer generates the model view of the data
m_viewer = MivotViewer(query_result, resolve_ref=True)
.. doctest-skip::
Once the query is finished, we can get a reference to the object that will process the Mivot annotations.
>>> dm_instance = m_viewer.dm_instance
>>> print(dm_instance.dmtype)
mango:EpochPosition
.. code-block:: python
We can also provide a complete instance representation that includes all fields in the entire hierarchy.
# Build a Python object matching the TEMPLATES content and
# which leaves are set with the values of the first row
mango_property = m_viewer.dm_instance
.. doctest-skip::
# Print out the content of the Python object
# This statement is just for a pedagogic purpose
DictUtils.print_pretty_json(mango_property.to_dict())
The annotations are consumed by this dynamic Python object which leaves are set with the data of the current row.
You can explore the structure of this object by using standard object paths or by browsing the dictionary shown below.
.. code-block:: json
{
>>> # Print out the json serialization of the Python object
>>> print(repr(dm_instance))
{
"dmtype": "mango:EpochPosition",

@@ -241,19 +244,19 @@ "longitude": {

"value": "ICRS"
}
}
}
}
}
}
}
}
The reader can transform ``EpochPosition`` instances into ``SkyCoord`` instances.
These can then be used for further scientific processing.
The reader can transform ``EpochPosition`` instances into ``SkyCoord`` instances.
These can then be used for further scientific processing.
.. doctest-skip::
.. code-block:: python
>>> while m_viewer.next_row_view():
>>> mango_property = m_viewer.dm_instance
>>> if mango_property.dmtype == "mango:EpochPosition":
>>> scb = SkyCoordBuilder(mango_property)
>>> # do whatever process with the SkyCoord object
>>> print(scb.build_sky_coord())
while m_viewer.next_row_view():
if mango_property.dmtype == "mango:EpochPosition":
scb = SkyCoordBuilder(mango_property.to_dict())
# do whatever process with the SkyCoord object
print(scb.build_sky_coord())
.. important::

@@ -266,3 +269,29 @@ Similar to the previous example, this code can be used with any VOTable with data mapped to MANGO.

Homework
========
The next section provides some tips to use the API documented in the :ref:`annoter page <mivot-annoter>`.
Simbad has released (Q3 2025) an annotated version of its Cone Search.
It's a good case to exercise this API.
.. code-block:: python
SERVER = "https://simbad.cds.unistra.fr/cone?"
VERB = 2
RA = 269.452076* u.degree
DEC = 4.6933649* u.degree
SR = 0.1* u.degree
MAXREC = 100
RESPONSEFORMAT = "mivot"
scs_srv = SCSService(SERVER)
query_result = scs_srv.search(
pos=SkyCoord(ra=RA, dec=DEC, frame='icrs'),
radius=SR,
verbosity=VERB,
RESPONSEFORMAT=RESPONSEFORMAT,
MAXREC=MAXREC)
*The next section provides some tips to use the API documented in the* :ref:`annoter page <mivot-annoter>`.

@@ -26,2 +26,6 @@ ******************************************************

.. attention::
The module based on XPath queries and allowing to browse the XML
annotations (``viewer.XmlViewer``) has been removed from version 1.8
Integrated Readout

@@ -32,7 +36,19 @@ ------------------

The example below shows how a VOTable result of a cone-search query can be parsed and data
mapped to the ``EpochPosition`` class.
``MivotInstance`` is a generic class that does not refer to any specific model.
The mapped class of a particular instance is stored in its ``dmtype`` attribute.
These objects can be used as standard Python objects, with their fields representing
elements of the model instances.
.. doctest-remote-data::
The first step is to instanciate a viewer that will provide the API for browsing annotations.
The viewer can be built from a VOTable file path, a parsed VOtable (``VOTableFile`` object),
or a ``DALResults`` instance.
.. attention::
The code below only works with ``astropy 6+``
.. doctest-skip::
>>> import astropy.units as u

@@ -42,111 +58,148 @@ >>> from astropy.coordinates import SkyCoord

>>> from pyvo.utils.prototype import activate_features
>>> from pyvo.mivot.version_checker import check_astropy_version
>>> from pyvo.mivot.viewer.mivot_viewer import MivotViewer
>>> activate_features("MIVOT")
>>> if check_astropy_version() is False:
... pytest.skip("MIVOT test skipped because of the astropy version.")
>>>
>>> scs_srv = SCSService("https://vizier.cds.unistra.fr/viz-bin/conesearch/V1.5/I/239/hip_main")
>>> m_viewer = MivotViewer(
... scs_srv.search(
... pos=SkyCoord(ra=52.26708 * u.degree, dec=59.94027 * u.degree, frame='icrs'),
... radius=0.05
... ),
... pos=SkyCoord(ra=52.26708 * u.degree, dec=59.94027 * u.degree,
... frame='icrs'),
... radius=0.1
... ),
... resolve_ref=True
... )
>>> mivot_instance = m_viewer.dm_instance
>>> print(mivot_instance.dmtype)
mango:EpochPosition
>>> print(mivot_instance.spaceSys.frame.spaceRefFrame.value)
ICRS
>>> while m_viewer.next_row_view():
... print(f"position: {mivot_instance.latitude.value} {mivot_instance.longitude.value}")
position: 59.94033461 52.26722684
In this example, the query result is mapped to the ``mango:EpochPosition`` class,
but users do not need to know this in advance, since the API provides tools
to discover the mapped models.
In this example, the data readout is totally managed by the ``MivotViewer`` instance.
The ``astropy.io.votable`` API is encapsulated in this module.
.. doctest-skip::
Model leaves (class attributes) are complex types that provide additional information:
>>> if m_viewer.get_models().get("mango"):
>>> print("data is mapped to the MANGO data model")
data is mapped to the MANGO data model
- ``value``: attribute value
- ``dmtype``: attribute type such as defined in the Mivot annotations
- ``unit``: attribute unit such as defined in the Mivot annotations
- ``ref``: identifier of the table column mapped on the attribute
We can also check which datamodel classes the data is mapped to.
The model view on a data row can also be passed as a Python dictionary
using the ``dict`` property of ``MivotInstance``.
.. doctest-skip::
.. code-block:: python
:caption: Working with a model view as a dictionary
(the JSON layout has been squashed for display purpose)
>>> mivot_instances = m_viewer.dm_instances
>>> print(f"data is mapped to {len(mivot_instances)} model class(es)")
data is mapped to 1 model class(es)
from pyvo.mivot.utils.dict_utils import DictUtils
.. doctest-skip::
mivot_instance = m_viewer.dm_instance
mivot_object_dict = mivot_object.dict
>>> mivot_instance = m_viewer.dm_instances[0]
>>> print(f"data is mapped to the {mivot_instance.dmtype} class")
data is mapped to the mango:EpochPosition class
DictUtils.print_pretty_json(mivot_object_dict)
{
"dmtype": "EpochPosition",
"longitude": {"value": 359.94372764, "unit": "deg"},
"latitude": {"value": -0.28005255, "unit": "deg"},
"pmLongitude": {"value": -5.14, "unit": "mas/yr"},
"pmLatitude": {"value": -25.43, "unit": "mas/yr"},
"epoch": {"value": 1991.25, "unit": "year"},
"Coordinate_coordSys": {
"dmtype": "SpaceSys",
"dmid": "SpaceFrame_ICRS",
"dmrole": "coordSys",
"spaceRefFrame": {"value": "ICRS"},
},
At this point, we know that the data has been mapped to the ``MANGO`` model,
and that the data rows can be interpreted as instances of the ``mango:EpochPosition``.
.. doctest-skip::
>>> print(mivot_instance.spaceSys.frame.spaceRefFrame.value)
ICRS
.. doctest-skip::
>>> while m_viewer.next_row_view():
>>> print(f"position: {mivot_instance.latitude.value} {mivot_instance.longitude.value}")
position: 59.94033461 52.26722684
....
.. important::
Coordinate systems are usually mapped in the GLOBALS MIVOT block.
This allows them to be referenced from any other MIVOT element.
The viewer resolves such references when the constructor flag ``resolve_ref`` is set to ``True``.
In this case the coordinate system instances are copied into their host elements.
The code below shows how to access GLOBALS instances (one in this example) independently of the mapped data.
.. doctest-skip::
>>> for globals_instance in m_viewer.dm_globals_instances:
>>> print(globals_instance)
<MivotInstance: dmtype="coords:SpaceSys">
We can also provide a complete instance representation that includes all fields in the entire hierarchy.
.. doctest-skip::
>>> print(repr(globals_instance))
{
"dmtype": "coords:SpaceSys",
"dmid": "SpaceFrame_ICRS",
"frame": {
"dmrole": "coords:PhysicalCoordSys.frame",
"dmtype": "coords:SpaceFrame",
"spaceRefFrame": {
"dmtype": "ivoa:string",
"value": "ICRS"
}
}
}
- It is recommended to use a copy of the
dictionary as it will be rebuilt each time the ``dict`` property is invoked.
- The default representation of ``MivotInstance`` instances is made with a pretty
string serialization of this dictionary.
As you can see from the previous examples, model leaves (class attributes) are complex types.
This is because they contain additional metadata as well as values:
- ``value`` : attribute value
- ``dmtype`` : attribute type such as defined in the Mivot annotations
- ``unit`` (if any) : attribute unit such as defined in the Mivot annotations
Per-Row Readout
---------------
The annotation schema can also be applied to table rows read outside of the ``MivotViewer``
with the `astropy.io.votable` API:
This annotation schema can also be applied to table rows read using the `astropy.io.votable` API
outside of the ``MivotViewer`` context.
.. code-block:: python
:caption: Accessing the model view of Astropy table rows
.. doctest-skip::
votable = parse(path_to_votable)
table = votable.resources[0].tables[0]
# init the viewer
mivot_viewer = MivotViewer(votable, resource_number=0)
mivot_object = mivot_viewer.dm_instance
# and feed it with the table row
read = []
for rec in table.array:
mivot_object.update(rec)
read.append(mivot_object.longitude.value)
# show that the model retrieve the correct data values
assert rec["RAICRS"] == mivot_object.longitude.value
assert rec["DEICRS"] == mivot_object.latitude.value
>>> votable = parse(path_to_votable)
>>> table = votable.resources[0].tables[0]
>>> # init the viewer on the first resource of the votable (default)
>>> mivot_viewer = MivotViewer(votable)
>>> mivot_object = mivot_viewer.dm_instance
>>> # and feed it with the numpy table row
>>> for rec in table.array:
>>> # apply the mapping to current row
>>> mivot_object.update(rec)
>>> # show that the model retrieve the correct values
>>> # ... or do whatever you want
>>> assert rec["RAICRS"] == mivot_object.longitude.value
>>> assert rec["DEICRS"] == mivot_object.latitude.value
In this case, it is up to the user to ensure that the read data rows are those mapped by the Mivot annotations.
Mivot/Mango as a Direct Gateway from Data to Astropy SkyCoord
-------------------------------------------------------------
For XML Hackers
---------------
A simple way to get the most out of annotations is to use them
to directly create Astropy objects, without having to parse the metadata,
whether it comes from the annotation or the VOTable.
The model instances can also be serialized as XML elements that can be parsed with XPath queries.
.. doctest-skip::
.. code-block:: python
:caption: Accessing the XML view of the mapped model instances
>>> from pyvo.mivot.features.sky_coord_builder import SkyCoordBuilder
>>>
>>> m_viewer.rewind()
>>> while m_viewer.next_row_view():
>>> sky_coord_builder = SkyCoordBuilder(mivot_instance)
>>> sky_coord = sky_coord_builder.build_sky_coord()
>>> print(sky_coord)
<SkyCoord (ICRS): (ra, dec, distance) in (deg, deg, pc)
(52.26722684, 59.94033461, 1315.7894902)
(pm_ra_cosdec, pm_dec) in mas / yr
(-0.82, -1.85)>
with MivotViewer(path_to_votable) as mivot_viewer:
while mivot_viewer.next_row_view():
xml_view = mivot_viewer.xml_view
# do whatever you want with this XML element
In the above example, we assume that the mapped model can be used as a ``SkyCoord`` precursor.
If this is not the case, an error is raised.
It is to be noted that ``mivot_viewer.xml_view`` is a shortcut
for ``mivot_viewer.xml_view.view`` where ``mivot_viewer.xml_view``
is is an instance of ``pyvo.mivot.viewer.XmlViewer``.
This object provides many functions facilitating the XML parsing.
.. important::
In the current implementation, the only functioning gateway connects
``Mango:EpochPosition`` objects with the ``SkyCoord`` class.
The ultimate objective is to generalize this mechanism to any property modeled by Mango,
and eventually to other IVOA models, thereby realizing the full potential
of a comprehensive and interoperable mapping framework.
Class Generation in a Nutshell

@@ -177,24 +230,25 @@ ------------------------------

- Original ``@dmtype`` are kept as attributes of generated Python objects.
- The structure of the ``MivotInstance`` objects can be inferred from the mapped model in 2 different ways:
- If the end-user is unaware of the class mapped by the actual ``MivotInstance``,
if can can explore it by using its class dictionary ``MivotInstance.__dict__``
(see the Python `data model <https://docs.python.org/3/reference/datamodel.html>`_).
- 1. From the MIVOT instance property ``MivotInstance.dict`` a shown above.
This is a pure Python dictionary but its access can be slow because it is generated
on the fly each time the property is invoked.
- 2. From the internal class dictionary ``MivotInstance.__dict__``
(see the Python `data model <https://docs.python.org/3/reference/datamodel.html>`_).
.. doctest-skip::
.. code-block:: python
:caption: Exploring the MivotInstance structure with the internal dictionaries
>>> mivot_instance = mivot_viewer.dm_instance
>>> print(mivot_instance.__dict__.keys())
dict_keys(['dmtype', 'longitude', 'latitude', 'pmLongitude', 'pmLatitude', 'epoch', 'Coordinate_coordSys'])
mivot_instance = mivot_viewer.dm_instance
.. doctest-skip::
print(mivot_instance.__dict__.keys())
dict_keys(['dmtype', 'longitude', 'latitude', 'pmLongitude', 'pmLatitude', 'epoch', 'Coordinate_coordSys'])
>>> print(mivot_instance.Coordinate_coordSys.__dict__.keys())
dict_keys(['dmtype', 'dmid', 'dmrole', 'spaceRefFrame'])
print(mivot_instance.Coordinate_coordSys.__dict__.keys())
dict_keys(['dmtype', 'dmid', 'dmrole', 'spaceRefFrame'])
.. doctest-skip::
print(mivot_instance.Coordinate_coordSys.spaceRefFrame.__dict__.keys())
dict_keys(['dmtype', 'value', 'unit', 'ref'])
>>> print(mivot_instance.Coordinate_coordSys.spaceRefFrame.__dict__.keys())
dict_keys(['dmtype', 'value', 'unit', 'ref'])
*More examples can be found* :ref:`here <mivot-examples>`.
Reference/API

@@ -201,0 +255,0 @@ =============

Metadata-Version: 2.4
Name: pyvo
Version: 1.7.1
Version: 1.8
Summary: Astropy affiliated package for accessing Virtual Observatory data and services

@@ -5,0 +5,0 @@ Author: the PyVO Developers

Metadata-Version: 2.4
Name: pyvo
Version: 1.7.1
Version: 1.8
Summary: Astropy affiliated package for accessing Virtual Observatory data and services

@@ -5,0 +5,0 @@ Author: the PyVO Developers

@@ -154,2 +154,8 @@ .gitignore

pyvo/io/uws/tests/data/job-implicit-v1.0.xml
pyvo/io/uws/tests/data/job-with-duplicate-elements.xml
pyvo/io/uws/tests/data/job-with-empty-jobinfo.xml
pyvo/io/uws/tests/data/job-with-namespace-elements.xml
pyvo/io/uws/tests/data/job-with-nested-jobinfo.xml
pyvo/io/uws/tests/data/job-with-simple-jobinfo.xml
pyvo/io/uws/tests/data/job-with-typed-jobinfo.xml
pyvo/io/uws/tests/data/job.xml

@@ -221,3 +227,2 @@ pyvo/io/vosi/__init__.py

pyvo/mivot/tests/test_mivot_instance.py
pyvo/mivot/tests/test_mivot_instance_generation.py
pyvo/mivot/tests/test_mivot_viewer.py

@@ -230,5 +235,5 @@ pyvo/mivot/tests/test_mivot_writer.py

pyvo/mivot/tests/test_vizier_cs.py
pyvo/mivot/tests/test_xml_viewer.py
pyvo/mivot/tests/data/filter_gaia_grp.xml
pyvo/mivot/tests/data/filter_gaia_grvs.xml
pyvo/mivot/tests/data/simbad-cone-mivot.xml
pyvo/mivot/tests/data/static_reference.xml

@@ -238,2 +243,3 @@ pyvo/mivot/tests/data/test.header_extraction.1.xml

pyvo/mivot/tests/data/test.header_extraction.xml
pyvo/mivot/tests/data/test.instance_multiple.xml
pyvo/mivot/tests/data/test.mango_annoter.xml

@@ -253,3 +259,2 @@ pyvo/mivot/tests/data/test.mivot_viewer.first_instance.xml

pyvo/mivot/tests/data/reference/static_reference_resolved.xml
pyvo/mivot/tests/data/reference/templates_models.json
pyvo/mivot/tests/data/reference/test_header_extraction.xml

@@ -271,3 +276,2 @@ pyvo/mivot/tests/data/reference/test_mivot_frames.xml

pyvo/mivot/viewer/mivot_viewer.py
pyvo/mivot/viewer/xml_viewer.py
pyvo/mivot/writer/__init__.py

@@ -280,2 +284,5 @@ pyvo/mivot/writer/annotations.py

pyvo/mivot/writer/mivot-v1.xsd
pyvo/mivot/writer/v1.1.xsd
pyvo/mivot/writer/v1.2.xsd
pyvo/mivot/writer/v1.3.xsd
pyvo/registry/__init__.py

@@ -282,0 +289,0 @@ pyvo/registry/regtap.py

@@ -306,3 +306,3 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst

def __init__(self, votable, *, url=None, session=None):
def __init__(self, votable, *, url=None, session=None, client_set_maxrec=None):
"""

@@ -322,2 +322,4 @@ initialize the cursor. This constructor is not typically called

optional session to use for network requests
client_set_maxrec: int
the maximum number of records that were requested by the client.

@@ -337,2 +339,3 @@ Raises

self._session = use_session(session)
self._client_set_maxrec = client_set_maxrec

@@ -344,4 +347,3 @@ self._status = self._findstatus(votable)

if self._status[0].lower() == "overflow":
warn("Partial result set. Potential causes MAXREC, async storage space, etc.",
category=DALOverflowWarning)
self._handle_overflow_warning(client_set_maxrec)

@@ -362,2 +364,50 @@ self._resultstable = self._findresultstable(votable)

def _handle_overflow_warning(self, client_set_maxrec=None):
"""
Handle overflow warning - can be overridden by subclasses.
Default implementation issues a generic overflow warning.
Subclasses can override this to customize or suppress warnings.
Parameters
----------
client_set_maxrec : int, optional
The maximum records requested by the client
"""
warn("Result set limited by user- or server-supplied MAXREC "
"parameter.", category=DALOverflowWarning)
def check_overflow_warning(self, client_set_maxrec=None):
"""
Check for overflow warnings and issue them if appropriate.
It will check if the results were truncated due to server limits
and issue a warning if the number of records returned is less than
the maximum records requested by the user.
If the results were truncated it distinguishes between expected
truncation (where the user requested a maxrec)
and unexpected truncation (where the user did not specify a maxrec).
If the results were truncated it issues a warning.
Parameters
----------
client_set_maxrec : int, optional
The maximum records explicitly requested by the client
"""
if self._status[0].lower() == "overflow":
maxrec_to_check = client_set_maxrec if client_set_maxrec is not None else self._client_set_maxrec
if (maxrec_to_check is not None
and len(self.resultstable.array) == maxrec_to_check):
pass
else:
if maxrec_to_check is not None:
warn(f"Results truncated at {len(self.resultstable.array)} records by service limits "
f"(you requested maxrec={maxrec_to_check})",
category=DALOverflowWarning)
else:
warn("Results truncated due to server limits. Consider "
"setting a maxrec value.",
category=DALOverflowWarning)
def _findresultstable(self, votable):

@@ -364,0 +414,0 @@ # this can be overridden to specialize for a particular DAL protocol

@@ -7,3 +7,5 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst

from datetime import datetime
import time
from time import sleep
import random

@@ -43,3 +45,7 @@ import requests

# common transient errors that can be retried
TRANSIENT_ERRORS = (requests.exceptions.ConnectionError,
requests.exceptions.Timeout)
def _from_ivoa_format(datetime_str):

@@ -340,3 +346,3 @@ """

result = job.fetch_result()
result = job.fetch_result(max_retries=keywords.get('max_retries', 0))

@@ -660,2 +666,3 @@ if delete:

job = cls(response.url, session=session)
job._client_set_maxrec = maxrec
return job

@@ -679,2 +686,3 @@

self._delete_on_exit = delete
self._client_set_maxrec = None
self._update()

@@ -1033,5 +1041,11 @@

def fetch_result(self):
def fetch_result(self, max_retries=0):
"""
returns the result votable if query is finished
Parameters
----------
max_retries : int, optional
Maximum number of retry attempts for transient network errors.
Default is 0 (no retries).
"""

@@ -1045,16 +1059,30 @@ result_uri = self.result_uri

try:
response = self._session.get(self.result_uri, stream=True)
response.raise_for_status()
except requests.RequestException as ex:
self._update()
# we propably got a 404 because query error. raise with error msg
self.raise_if_error()
raise DALServiceError.from_except(ex, self.url)
response = None
response.raw.read = partial(
response.raw.read, decode_content=True)
return TAPResults(votableparse(response.raw.read), url=self.result_uri, session=self._session)
for attempt in range(max_retries + 1):
try:
response = self._session.get(self.result_uri, stream=True)
response.raise_for_status()
break
except TRANSIENT_ERRORS as ex:
if attempt < max_retries:
delay = (2 ** attempt) + random.uniform(0.8, 1)
time.sleep(delay)
continue
else:
raise DALServiceError.from_except(ex, self.url)
except requests.RequestException as ex:
# Non-retryable error - update and check for query errors
self._update()
self.raise_if_error()
raise DALServiceError.from_except(ex, self.url)
response.raw.read = partial(response.raw.read, decode_content=True)
result = TAPResults(votableparse(response.raw.read), url=self.result_uri, session=self._session)
result.check_overflow_warning(self._client_set_maxrec)
return result
class TAPQuery(DALQuery):

@@ -1113,2 +1141,4 @@ """

self._client_set_maxrec = maxrec
if maxrec:

@@ -1166,4 +1196,11 @@ self["MAXREC"] = maxrec

"""
return TAPResults(self.execute_votable(), url=self.queryurl, session=self._session)
result = TAPResults(
self.execute_votable(),
url=self.queryurl,
session=self._session
)
result.check_overflow_warning(self._client_set_maxrec)
return result
def submit(self, *, post=False):

@@ -1277,4 +1314,13 @@ """

def _handle_overflow_warning(self, client_set_maxrec=None):
"""
TAP-specific overflow warning handling.
For TAP results we suppress the default overflow warning during
initialization because TAPQuery.execute() will call check_overflow_warning()
"""
pass
class TAPRecord(SodaRecordMixin, DatalinkRecordMixin, Record):
pass

@@ -6,2 +6,3 @@ #!/usr/bin/env python

"""
import warnings
from functools import partial

@@ -423,3 +424,67 @@

def test_check_overflow_warning_no_maxrec(self):
with pytest.warns(DALOverflowWarning):
dalresults = DALResults.from_result_url('http://example.com/query/overflowstatus')
with pytest.warns(DALOverflowWarning, match="Results truncated due to server limits"):
dalresults.check_overflow_warning()
def test_check_overflow_warning_exact_match(self):
with pytest.warns(DALOverflowWarning):
dalresults = DALResults.from_result_url('http://example.com/query/overflowstatus')
with warnings.catch_warnings():
warnings.simplefilter("error")
dalresults.check_overflow_warning(client_set_maxrec=3)
def test_check_overflow_warning_service_truncation(self):
with pytest.warns(DALOverflowWarning):
dalresults = DALResults.from_result_url('http://example.com/query/overflowstatus')
with pytest.warns(DALOverflowWarning, match="Results truncated at 3 records by service limits"):
dalresults.check_overflow_warning(client_set_maxrec=1000)
def test_check_overflow_warning_uses_stored_maxrec(self):
with pytest.warns(DALOverflowWarning):
dalresults = DALResults.from_result_url('http://example.com/query/overflowstatus')
dalresults._client_set_maxrec = 1000
with pytest.warns(DALOverflowWarning, match="Results truncated at 3 records by service limits"):
dalresults.check_overflow_warning()
def test_check_overflow_warning_parameter_overrides_stored(self):
with pytest.warns(DALOverflowWarning):
dalresults = DALResults.from_result_url('http://example.com/query/overflowstatus')
dalresults._client_set_maxrec = 1000
with warnings.catch_warnings():
warnings.simplefilter("error")
dalresults.check_overflow_warning(client_set_maxrec=3)
def test_check_overflow_warning_no_overflow_status(self):
dalresults = DALResults.from_result_url('http://example.com/query/basic')
with warnings.catch_warnings():
warnings.simplefilter("error")
dalresults.check_overflow_warning(client_set_maxrec=1000)
def test_handle_overflow_warning_default_behavior(self):
votable = votableparse(BytesIO(get_pkg_data_contents('data/query/overflowstatus.xml')))
with pytest.warns(DALOverflowWarning,
match="Result set limited by user- or server-supplied MAXREC parameter."):
_ = DALResults(votable, url='http://test.com')
def test_stored_client_set_maxrec_initialization(self):
votable = votableparse(BytesIO(get_pkg_data_contents('data/query/basic.xml')))
result = DALResults(votable, url='http://test.com', client_set_maxrec=100)
assert result._client_set_maxrec == 100
result2 = DALResults(votable, url='http://test.com')
assert result2._client_set_maxrec is None
@pytest.mark.filterwarnings('ignore::astropy.io.votable.exceptions.W03')

@@ -426,0 +491,0 @@ @pytest.mark.filterwarnings('ignore::astropy.io.votable.exceptions.W06')

@@ -5,2 +5,3 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst

"""
import warnings
from functools import partial

@@ -20,3 +21,3 @@ from contextlib import ExitStack

from pyvo.dal.tap import escape, search, AsyncTAPJob, TAPService
from pyvo.dal import DALQueryError, DALServiceError
from pyvo.dal import DALQueryError, DALServiceError, DALOverflowWarning

@@ -136,2 +137,28 @@ from pyvo.io.uws import JobFile

@pytest.fixture()
def overflow_fixture(mocker):
"""Mock TAP service that returns overflow status with exactly 10 records"""
def callback(request, context):
votable_content = '''<?xml version="1.0" encoding="UTF-8"?>
<VOTABLE version="1.3" xmlns="http://www.ivoa.net/xml/VOTable/v1.3">
<RESOURCE type="results">
<INFO name="QUERY_STATUS" value="OVERFLOW">Result truncated due to MAXREC</INFO>
<TABLE>
<FIELD name="id" datatype="int"/>
<FIELD name="value" datatype="char" arraysize="*"/>
<DATA>
<TABLEDATA>''' + ''.join(f'<TR><TD>{i}</TD><TD>test{i}</TD></TR>' for i in range(10)) + '''
</TABLEDATA>
</DATA>
</TABLE>
</RESOURCE>
</VOTABLE>'''
return votable_content.encode('utf-8')
with mocker.register_uri(
'POST', 'http://example.com/tap/sync', content=callback
) as matcher:
yield matcher
class MockAsyncTAPServer:

@@ -1051,3 +1078,183 @@ def __init__(self):

@pytest.mark.usefixtures('async_fixture')
@pytest.mark.filterwarnings("ignore::astropy.io.votable.exceptions.W27")
@pytest.mark.filterwarnings("ignore::astropy.io.votable.exceptions.W48")
@pytest.mark.filterwarnings("ignore::astropy.io.votable.exceptions.W06")
def test_fetch_result_retry_connection_error(self):
service = TAPService('http://example.com/tap')
job = service.submit_job("SELECT * FROM ivoa.obscore")
job.run()
job.wait()
status_response = '''<?xml version="1.0" encoding="UTF-8"?>
<uws:job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<uws:jobId>1</uws:jobId>
<uws:phase>COMPLETED</uws:phase>
<uws:results>
<uws:result id="result" xsi:type="vot:VOTable"
href="http://example.com/tap/async/1/results/result"/>
</uws:results>
</uws:job>'''
with requests_mock.Mocker() as rm:
rm.get(f'http://example.com/tap/async/{job.job_id}', text=status_response)
call_count = 0
def response_callback(request, context):
nonlocal call_count
call_count += 1
if call_count <= 2:
raise requests.exceptions.ConnectionError()
else:
return get_pkg_data_contents('data/tap/obscore-image.xml')
rm.get(
f'http://example.com/tap/async/{job.job_id}/results/result',
content=response_callback
)
result = job.fetch_result(max_retries=2)
assert len(result) == 10
assert call_count == 3
job.delete()
@pytest.mark.usefixtures('async_fixture')
@pytest.mark.filterwarnings("ignore::astropy.io.votable.exceptions.W27")
@pytest.mark.filterwarnings("ignore::astropy.io.votable.exceptions.W48")
@pytest.mark.filterwarnings("ignore::astropy.io.votable.exceptions.W06")
def test_fetch_result_retry_timeout_error(self):
service = TAPService('http://example.com/tap')
job = service.submit_job("SELECT * FROM ivoa.obscore")
job.run()
job.wait()
status_response = '''<?xml version="1.0" encoding="UTF-8"?>
<uws:job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<uws:jobId>1</uws:jobId>
<uws:phase>COMPLETED</uws:phase>
<uws:results>
<uws:result id="result" xsi:type="vot:VOTable"
href="http://example.com/tap/async/1/results/result"/>
</uws:results>
</uws:job>'''
with requests_mock.Mocker() as rm:
rm.get(f'http://example.com/tap/async/{job.job_id}', text=status_response)
call_count = 0
def response_callback(request, context):
nonlocal call_count
call_count += 1
if call_count == 1:
raise requests.exceptions.Timeout()
else:
return get_pkg_data_contents('data/tap/obscore-image.xml')
rm.get(
f'http://example.com/tap/async/{job.job_id}/results/result',
content=response_callback
)
result = job.fetch_result(max_retries=1)
assert len(result) == 10
assert call_count == 2
job.delete()
@pytest.mark.usefixtures('async_fixture')
def test_fetch_result_no_retry_on_http_error(self):
service = TAPService('http://example.com/tap')
job = service.submit_job("SELECT * FROM ivoa.obscore")
job.run()
job.wait()
status_response = '''<?xml version="1.0" encoding="UTF-8"?>
<uws:job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<uws:jobId>1</uws:jobId>
<uws:phase>COMPLETED</uws:phase>
<uws:results>
<uws:result id="result" xsi:type="vot:VOTable"
href="http://example.com/tap/async/1/results/result"/>
</uws:results>
</uws:job>'''
with requests_mock.Mocker() as rm:
rm.get(f'http://example.com/tap/async/{job.job_id}', text=status_response)
rm.get(
f'http://example.com/tap/async/{job.job_id}/results/result',
status_code=404
)
with pytest.raises(DALServiceError):
job.fetch_result(max_retries=2)
job.delete()
@pytest.mark.usefixtures('async_fixture')
def test_fetch_result_retry_exhausted(self):
service = TAPService('http://example.com/tap')
job = service.submit_job("SELECT * FROM ivoa.obscore")
job.run()
job.wait()
status_response = '''<?xml version="1.0" encoding="UTF-8"?>
<uws:job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<uws:jobId>1</uws:jobId>
<uws:phase>COMPLETED</uws:phase>
<uws:results>
<uws:result id="result" xsi:type="vot:VOTable"
href="http://example.com/tap/async/1/results/result"/>
</uws:results>
</uws:job>'''
with requests_mock.Mocker() as rm:
rm.get(f'http://example.com/tap/async/{job.job_id}', text=status_response)
rm.get(
f'http://example.com/tap/async/{job.job_id}/results/result',
exc=requests.exceptions.ConnectionError()
)
with pytest.raises(DALServiceError):
job.fetch_result(max_retries=2)
job.delete()
@pytest.mark.usefixtures('async_fixture')
def test_fetch_result_default_no_retry(self):
service = TAPService('http://example.com/tap')
job = service.submit_job("SELECT * FROM ivoa.obscore")
job.run()
job.wait()
status_response = '''<?xml version="1.0" encoding="UTF-8"?>
<uws:job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<uws:jobId>1</uws:jobId>
<uws:phase>COMPLETED</uws:phase>
<uws:results>
<uws:result id="result" xsi:type="vot:VOTable"
href="http://example.com/tap/async/1/results/result"/>
</uws:results>
</uws:job>'''
with requests_mock.Mocker() as rm:
rm.get(f'http://example.com/tap/async/{job.job_id}', text=status_response)
rm.get(
f'http://example.com/tap/async/{job.job_id}/results/result',
exc=requests.exceptions.ConnectionError()
)
with pytest.raises(DALServiceError):
job.fetch_result()
job.delete()
@pytest.mark.usefixtures("tapservice")

@@ -1342,1 +1549,127 @@ class TestTAPCapabilities:

assert row['proposal_id'] == '2013.1.01365.S'
@pytest.mark.usefixtures('overflow_fixture')
@pytest.mark.filterwarnings("ignore::astropy.io.votable.exceptions.W27")
@pytest.mark.filterwarnings("ignore::astropy.io.votable.exceptions.W48")
@pytest.mark.filterwarnings("ignore::astropy.io.votable.exceptions.W06")
class TestTAPOverflowWarnings:
"""Test TAP overflow warning behavior"""
def test_sync_no_maxrec_overflow_warning(self):
service = TAPService('http://example.com/tap')
with pytest.warns(DALOverflowWarning, match="Results truncated due to server limits"):
results = service.run_sync("SELECT * FROM ivoa.obscore")
assert len(results) == 10
def test_sync_maxrec_exact_match_no_warning(self):
service = TAPService('http://example.com/tap')
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
_ = service.run_sync("SELECT * FROM ivoa.obscore", maxrec=10)
overflow_warnings = [warning for warning in w
if issubclass(warning.category, DALOverflowWarning)]
assert len(overflow_warnings) == 0, f"Unexpected overflow warnings: {overflow_warnings}"
def test_sync_maxrec_service_truncation_warning(self):
service = TAPService('http://example.com/tap')
with pytest.warns(DALOverflowWarning, match="Results truncated at "
"10 records by service limits.*you requested maxrec=100"):
results = service.run_sync("SELECT * FROM ivoa.obscore", maxrec=100)
assert len(results) == 10
def test_search_function_no_maxrec(self):
with pytest.warns(DALOverflowWarning,
match="Results truncated due to server limits"):
results = search('http://example.com/tap',
"SELECT * FROM ivoa.obscore")
assert len(results) == 10
def test_search_function_with_maxrec(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
_ = search('http://example.com/tap', "SELECT * FROM "
"ivoa.obscore", maxrec=10)
overflow_warnings = [warning for warning in w
if issubclass(warning.category, DALOverflowWarning)]
assert len(overflow_warnings) == 0, f"Unexpected overflow warnings: {overflow_warnings}"
def test_tapresults_suppresses_construction_warning(self):
from pyvo.dal.tap import TAPResults
from astropy.io.votable import parse as votableparse
import requests
response = requests.post('http://example.com/tap/sync',
data={'QUERY': 'SELECT * FROM test'})
votable = votableparse(BytesIO(response.content))
with warnings.catch_warnings():
warnings.simplefilter("error")
result = TAPResults(votable, url='http://test.com', session=None)
assert len(result) == 10
def test_tap_create_query_maxrec_tracking(self):
service = TAPService('http://example.com/tap')
query = service.create_query("SELECT * FROM ivoa.obscore", maxrec=10)
assert query._client_set_maxrec == 10
assert query["MAXREC"] == 10
query2 = service.create_query("SELECT * FROM ivoa.obscore")
assert query2._client_set_maxrec is None
assert "MAXREC" not in query2
def test_edge_case_maxrec_zero(self):
service = TAPService('http://example.com/tap')
query = service.create_query("SELECT * FROM ivoa.obscore", maxrec=0)
assert query._client_set_maxrec == 0
assert "MAXREC" not in query
@pytest.mark.usefixtures('async_fixture')
class TestTAPAsyncOverflowWarnings:
"""Test async TAP overflow warnings"""
def test_async_job_stores_maxrec(self):
service = TAPService('http://example.com/tap')
job = service.submit_job("SELECT * FROM ivoa.obscore", maxrec=5)
assert job._client_set_maxrec == 5
def test_async_job_manual_creation_no_maxrec(self):
job_response = '''<?xml version="1.0" encoding="UTF-8"?>
<uws:job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0">
<uws:jobId>123</uws:jobId>
<uws:phase>PENDING</uws:phase>
<uws:quote>2025-07-29T17:34:19.638</uws:quote>
<uws:creationTime>2025-07-28T17:34:19.638</uws:creationTime>
<uws:executionDuration>14400</uws:executionDuration>
<uws:destruction>2025-07-04T17:34:19.638</uws:destruction>
<uws:parameters/>
<uws:results/>
</uws:job>'''
with requests_mock.Mocker() as rm:
rm.get('http://example.com/tap/async/123', text=job_response)
job = AsyncTAPJob('http://example.com/tap/async/123')
assert job._client_set_maxrec is None
assert job.job_id == '123'
assert job.phase == 'PENDING'
def test_tap_integration_with_existing_tests():
service = TAPService('http://example.com/tap')
query = service.create_query("SELECT * FROM ivoa.obscore")
assert query is not None
assert hasattr(query, '_client_set_maxrec')
#!/usr/bin/env python
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Tests for pyvo.io.vosi
Tests for pyvo.io.uws
"""
import pytest
import pyvo.io.uws as uws
from pyvo.io.uws.tree import ExtensibleUWSElement

@@ -35,1 +37,181 @@ from astropy.utils.data import get_pkg_data_filename

assert job.errorsummary.message.content == 'We have problem'
def test_simple_jobinfo(self):
job = uws.parse_job(get_pkg_data_filename(
"data/job-with-simple-jobinfo.xml"))
assert job.jobinfo is not None
assert 'tapQueryInfo' in job.jobinfo
assert job.jobinfo['tapQueryInfo'] is not None
tap_info = job.jobinfo['tapQueryInfo']
assert 'pct_complete' in tap_info
assert 'chunks_processed' in tap_info
assert 'total_chunks' in tap_info
assert tap_info['pct_complete'].value == 100
assert tap_info['chunks_processed'].value == 1
assert tap_info['total_chunks'].value == 1
assert tap_info['pct_complete'].text == "100"
keys = list(job.jobinfo.keys())
assert 'tapQueryInfo' in keys
def test_jobinfo_multiple_access_patterns(self):
job = uws.parse_job(get_pkg_data_filename(
"data/job-with-simple-jobinfo.xml"))
assert job.jobinfo is not None
tap_info1 = job.jobinfo['tapQueryInfo']
tap_info2 = job.jobinfo.get('tapQueryInfo')
assert tap_info1 is tap_info2
assert tap_info1 is not None
def test_jobinfo_text_content_and_types(self):
job = uws.parse_job(get_pkg_data_filename(
"data/job-with-typed-jobinfo.xml"))
assert job.jobinfo is not None
int_elem = job.jobinfo['integer_value']
assert int_elem.value == 100
assert isinstance(int_elem.value, int)
assert int_elem.text == "100"
float_elem = job.jobinfo['float_value']
assert float_elem.value == 3.14
assert isinstance(float_elem.value, float)
assert float_elem.text == "3.14"
string_elem = job.jobinfo['string_value']
assert string_elem.value == "pyvo"
assert isinstance(string_elem.value, str)
assert string_elem.text == "pyvo"
empty_elem = job.jobinfo['empty_value']
assert empty_elem.value is None
assert empty_elem.text is None
def test_jobinfo_get_methods(self):
job = uws.parse_job(get_pkg_data_filename(
"data/job-with-simple-jobinfo.xml"))
jobinfo = job.jobinfo
assert jobinfo.get('nonexistent') is None
assert jobinfo.get('nonexistent', 'default') == 'default'
tap_info = jobinfo.get('tapQueryInfo')
assert tap_info is not None
assert 'tapQueryInfo' in jobinfo
assert 'nonexistent' not in jobinfo
tap_info = jobinfo['tapQueryInfo']
assert tap_info is not None
with pytest.raises(KeyError):
_ = jobinfo['nonexistent']
def test_jobinfo_edge_cases(self):
job = uws.parse_job(get_pkg_data_filename(
"data/job-with-simple-jobinfo.xml"))
jobinfo = job.jobinfo
str_repr = str(jobinfo)
assert 'jobInfo' in str_repr or len(str_repr) > 0
repr_str = repr(jobinfo)
assert 'ExtensibleUWSElement' in repr_str
assert 'elements=' in repr_str
def test_no_jobinfo(self):
job = uws.parse_job(get_pkg_data_filename(
"data/job.xml"))
assert job.jobinfo is None
def test_nested_jobinfo_access(self):
job = uws.parse_job(get_pkg_data_filename(
"data/job-with-nested-jobinfo.xml"))
assert job.jobinfo is not None
query_info = job.jobinfo['queryInfo']
assert query_info is not None
metrics = query_info['metrics']
assert metrics is not None
assert metrics['execution_time'].value == 1500
assert metrics['rows_returned'].value == 100
def test_jobinfo_overwrite_behavior(self):
job = uws.parse_job(get_pkg_data_filename(
"data/job-with-duplicate-elements.xml"))
assert job.jobinfo is not None
status = job.jobinfo.get('status')
assert status is not None
assert status.value == "completed"
def test_extensible_element_creation(self):
element = ExtensibleUWSElement(config={}, pos=(1, 1), _name='test')
assert element._name == 'test'
assert len(element._elements) == 0
assert 'test' in str(element)
assert 'elements=0' in repr(element)
assert 'nonexistent' not in element
assert element.get('nonexistent') is None
assert list(element.keys()) == []
def test_jobinfo_namespace_elements(self):
job = uws.parse_job(get_pkg_data_filename(
"data/job-with-namespace-elements.xml"))
assert job.jobinfo is not None
progress = job.jobinfo.get('progress')
assert progress is not None
# Assert that the value is the last one of the elements with the same name
assert progress.value == 75
unique_elem = job.jobinfo.get('uniqueElement')
assert unique_elem is not None
keys = job.jobinfo.keys()
assert len(keys) > 0
def test_multiple_elements_same_name(self):
job = uws.parse_job(get_pkg_data_filename(
"data/job-with-duplicate-elements.xml"))
assert job.jobinfo is not None
_ = [key for key in job.jobinfo.keys() if 'status' in key]
status = job.jobinfo['status']
assert status.value == "completed"
def test_empty_jobinfo(self):
job = uws.parse_job(get_pkg_data_filename(
"data/job-with-empty-jobinfo.xml"))
assert job.jobinfo is not None
assert len(job.jobinfo.keys()) == 0
assert len(job.jobinfo._elements) == 0
def test_jobinfo_numeric_content_conversion(self):
element = ExtensibleUWSElement(config={}, pos=(1, 1), _name='test')
element.content = 100
assert not isinstance(element.content, str)
assert element.content == 100
element.parse(iter([]), {})
assert element.text == "100"
assert element.value == 100
assert isinstance(element.text, str)

@@ -39,3 +39,3 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst

'UWSElement', 'Reference', 'JobSummary', 'Parameters', 'Parameter',
'Results', 'Result', 'Jobs']
'Results', 'Result', 'ExtensibleUWSElement', 'Jobs', 'JobInfo']

@@ -101,2 +101,3 @@

self._message = None
self._jobinfo = None

@@ -274,3 +275,14 @@ @uwselement(name='jobId', plain=True)

@uwselement(name='jobInfo', plain=True) # ← Add plain=True
def jobinfo(self):
"""Implementation-specific job information"""
return self._jobinfo
@jobinfo.adder
def jobinfo(self, iterator, tag, data, config, pos):
jobinfo = JobInfo(config, pos, 'jobInfo', **data)
jobinfo.parse(iterator, config)
self._jobinfo = jobinfo
class Jobs(HomogeneousList, UWSElement):

@@ -448,1 +460,109 @@ """A parsed representation of the joblist endpoint.

super().__init__(config, pos, _name, **kwargs)
class ExtensibleUWSElement(ContentMixin, UWSElement):
"""
UWS Element that can handle arbitrary child elements.
"""
def __init__(self, config=None, pos=None, _name='', **kwargs):
super().__init__(config, pos, _name, **kwargs)
self._elements = {}
self._text_content = None
self._name = _name
def _add_unknown_tag(self, iterator, tag, data, config, pos):
"""Handle unknown tags without generating warnings
Parameters
----------
iterator : iterator
The iterator that provides the XML elements.
tag : str
The tag name of the unknown element.
data : dict
Additional data associated.
config : dict
Configuration options.
pos : tuple
The position of the element in the XML document (line, column).
Returns
-------
ExtensibleUWSElement object
"""
element = ExtensibleUWSElement(config, pos, tag, **data)
element.parse(iterator, config)
# Last element with the same tag wins
self._elements[tag] = element
return element
def parse(self, iterator, config):
"""Override parse to capture text content for leaf elements"""
super().parse(iterator, config)
# Capture text content from ContentMixin
if hasattr(self, 'content') and self.content is not None:
if isinstance(self.content, str):
self._text_content = self.content.strip()
else:
self._text_content = str(self.content).strip() if self.content else None
@property
def text(self):
"""Get the text content of this element"""
if self._text_content is not None and self._text_content.strip():
return self._text_content
if hasattr(self, 'content') and self.content is not None:
content_str = str(self.content).strip()
return content_str if content_str else None
return None
@property
def value(self):
"""Get the text content converted to appropriate type"""
text = self.text
if not text:
return None
# Try to convert to int, float, if not leave as string
try:
return int(text)
except ValueError:
try:
return float(text)
except ValueError:
return text
def get(self, name, default=None):
"""Get element by name (supports both local names and full namespaced names)"""
return self._elements.get(name, default)
def keys(self):
"""Return all available keys (both local and namespaced)"""
return list(self._elements.keys())
def __contains__(self, name):
"""Support 'in' operator"""
return name in self._elements
def __getitem__(self, name):
"""Dict-like access"""
if name not in self._elements:
raise KeyError(f"Element '{name}' not found")
return self._elements[name]
def __str__(self):
if self._text_content:
return self._text_content
return f"<{self._name} with {len(set(self._elements.values()))} children>"
def __repr__(self):
unique_elements = len(set(self._elements.values()))
return f"ExtensibleUWSElement(name='{self._name}', elements={unique_elements})"
class JobInfo(ExtensibleUWSElement):
"""JobInfo element that can contain arbitrary elements."""
def __init__(self, config=None, pos=None, _name='jobInfo', **kwargs):
super().__init__(config, pos, _name, **kwargs)

@@ -5,37 +5,11 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst

"""
import numbers
from astropy.coordinates import SkyCoord
from astropy import units as u
from astropy.coordinates import ICRS, Galactic, FK4, FK5
from pyvo.mivot.utils.exceptions import NoMatchingDMTypeError
from astropy.time.core import Time
from pyvo.mivot.glossary import SkyCoordMapping
from pyvo.mivot.utils.exceptions import NoMatchingDMTypeError, MappingError
class MangoRoles:
"""
Place holder for the roles (attribute names) of the mango:EpochPosition class
"""
LONGITUDE = "longitude"
LATITUDE = "latitude"
PM_LONGITUDE = "pmLongitude"
PM_LATITUDE = "pmLatitude"
PARALLAX = "parallax"
RADIAL_VELOCITY = "radialVelocity"
EPOCH = "obsDate"
FRAME = "frame"
EQUINOX = "equinox"
PMCOSDELTAPPLIED = "pmCosDeltApplied"
# Mapping of the MANGO parameters on the SkyCoord parameters
skycoord_param_default = {
MangoRoles.LONGITUDE: 'ra', MangoRoles.LATITUDE: 'dec', MangoRoles.PARALLAX: 'distance',
MangoRoles.PM_LONGITUDE: 'pm_ra_cosdec', MangoRoles.PM_LATITUDE: 'pm_dec',
MangoRoles.RADIAL_VELOCITY: 'radial_velocity', MangoRoles.EPOCH: 'obstime'}
skycoord_param_galactic = {
MangoRoles.LONGITUDE: 'l', MangoRoles.LATITUDE: 'b', MangoRoles.PARALLAX: 'distance',
MangoRoles.PM_LONGITUDE: 'pm_l_cosb', MangoRoles.PM_LATITUDE: 'pm_b',
MangoRoles.RADIAL_VELOCITY: 'radial_velocity', MangoRoles.EPOCH: 'obstime'}
class SkyCoordBuilder:

@@ -51,3 +25,3 @@ '''

def __init__(self, mivot_instance_dict):
def __init__(self, mivot_instance):
'''

@@ -58,6 +32,6 @@ Constructor

-----------
mivot_instance_dict: viewer.MivotInstance.to_dict()
Internal dictionary of the dynamic Python object generated from the MIVOT block
mivot_instance: dict or MivotInstance
Python object generated from the MIVOT block as either a Pyhon object or a dict
'''
self._mivot_instance_dict = mivot_instance_dict
self._mivot_instance_dict = mivot_instance.to_dict()
self._map_coord_names = None

@@ -68,3 +42,5 @@

Build a SkyCoord instance from the MivotInstance dictionary.
The operation requires the dictionary to have ``mango:EpochPosition`` as dmtype
The operation requires the dictionary to have ``mango:EpochPosition`` as dmtype.
This instance can be either the root of the dictionary or it can be one
of the Mango properties if the root object is a mango:MangoObject instance
This is a public method which could be extended to support other dmtypes.

@@ -82,3 +58,15 @@

"""
if self._mivot_instance_dict and self._mivot_instance_dict["dmtype"] == "mango:EpochPosition":
if self._mivot_instance_dict and self._mivot_instance_dict["dmtype"] == "mango:MangoObject":
property_dock = self._mivot_instance_dict["propertyDock"]
for mango_property in property_dock:
if mango_property["dmtype"] == "mango:EpochPosition":
self._mivot_instance_dict = mango_property
return self._build_sky_coord_from_mango()
raise NoMatchingDMTypeError(
"No INSTANCE with dmtype='mango:EpochPosition' has been found:"
" in the property dock of the MangoObject, "
"cannot build a SkyCoord from annotations")
elif self._mivot_instance_dict and self._mivot_instance_dict["dmtype"] == "mango:EpochPosition":
return self._build_sky_coord_from_mango()

@@ -89,5 +77,6 @@ raise NoMatchingDMTypeError(

def _set_year_time_format(self, hk_field, besselian=False):
def _get_time_instance(self, hk_field, besselian=False):
"""
Format a date expressed in year as [scale]year
- Exception possibly risen by Astropy are not caught

@@ -103,18 +92,80 @@ parameters

-------
string or None
attribute value formatted as [scale]year
Time instance or None
raise
-----
MappingError: if the Time instance cannot be built for some reason
"""
scale = "J" if not besselian else "B"
# Process complex type "mango:DateTime
# only "year" representation are supported yet
if hk_field['dmtype'] == "mango:DateTime":
representation = hk_field['representation']['value']
timestamp = hk_field['dateTime']['value']
if representation == "year":
return f"{scale}{timestamp}"
# Process simple attribute
else:
representation = hk_field.get("unit")
timestamp = hk_field.get("value")
if not representation or not timestamp:
raise MappingError(f"Cannot interpret field {hk_field} "
f"as a {('besselian' if besselian else 'julian')} timestamp")
time_instance = self. _build_time_instance(timestamp, representation, besselian)
if not time_instance:
raise MappingError(f"Cannot build a Time instance from {hk_field}")
return time_instance
def _build_time_instance(self, timestamp, representation, besselian=False):
"""
Build a Time instance matching the input parameters.
- Returns None if the parameters do not allow any Time setup
- Exception possibly risen by Astropy are not caught at this level
parameters
----------
timestamp: string or number
The timestamp must comply with the given representation
representation: string
year, iso, ... (See MANGO primitive types derived from ivoa:timeStamp)
besselian: boolean (optional)
Flag telling to use the besselain calendar. We assume it to only be
relevant for FK5 frame
returns
-------
Time instance or None
"""
if representation in ("year", "yr", "y"):
# it the timestamp is numeric, we infer its format from the besselian flag
if isinstance(timestamp, numbers.Number):
return Time(f"{('B' if besselian else 'J')}{timestamp}",
format=("byear_str" if besselian else "jyear_str"))
if besselian:
if timestamp.startswith("B"):
return Time(f"{timestamp}", format="byear_str")
elif timestamp.startswith("J"):
# a besselain year cannot be given as "Jxxxx"
return None
elif timestamp.isnumeric():
# we force the string representation not to break the test assertions
return Time(f"B{timestamp}", format="byear_str")
else:
if timestamp.startswith("J"):
return Time(f"{timestamp}", format="jyear_str")
elif timestamp.startswith("B"):
# a julian year cannot be given as "Bxxxx"
return None
elif timestamp.isnumeric():
# we force the string representation not to break the test assertions
return Time(f"J{timestamp}", format="jyear_str")
# no case matches
return None
return (f"{scale}{hk_field['value']}" if hk_field["unit"] in ("yr", "year")
else hk_field["value"])
# in the following cases, the calendar (B or J) is given by the besselian flag
# We force to use the string representation to avoid breaking unit tests.
elif representation in ("mjd", "jd", "iso"):
time = Time(f"{timestamp}", format=representation)
return (Time(time.byear_str) if besselian else time)
def _get_space_frame(self, obstime=None):
return None
def _get_space_frame(self):
"""

@@ -126,7 +177,2 @@ Build an astropy space frame instance from the MIVOT annotations.

parameters
----------
obstime: str
Observation time is given to the space frame builder (this method) because
it must be set by the coordinate system constructor in case of FK4 frame.
returns

@@ -142,12 +188,13 @@ -------

if frame == 'fk4':
self._map_coord_names = skycoord_param_default
self._map_coord_names = SkyCoordMapping.default_params
if "equinox" in coo_sys:
equinox = self._set_year_time_format(coo_sys["equinox"], True)
return FK4(equinox=equinox, obstime=obstime)
equinox = self._get_time_instance(coo_sys["equinox"], True)
# by FK4 takes obstime=equinox by default
return FK4(equinox=equinox)
return FK4()
if frame == 'fk5':
self._map_coord_names = skycoord_param_default
self._map_coord_names = SkyCoordMapping.default_params
if "equinox" in coo_sys:
equinox = self._set_year_time_format(coo_sys["equinox"])
equinox = self._get_time_instance(coo_sys["equinox"])
return FK5(equinox=equinox)

@@ -157,6 +204,6 @@ return FK5()

if frame == 'galactic':
self._map_coord_names = skycoord_param_galactic
self._map_coord_names = SkyCoordMapping.galactic_params
return Galactic()
self._map_coord_names = skycoord_param_default
self._map_coord_names = SkyCoordMapping.default_params
return ICRS()

@@ -166,5 +213,3 @@

"""
Build silently a SkyCoord instance from the ``mango:EpochPosition instance``.
No error is trapped, unconsistencies in the ``mango:EpochPosition`` instance will
raise Astropy errors.
Build a SkyCoord instance from the ``mango:EpochPosition instance``.

@@ -184,24 +229,27 @@ - The epoch (obstime) is meant to be given in year.

for key, value in self._map_coord_names.items():
# ignore not set parameters
if key not in self._mivot_instance_dict:
for mango_role, skycoord_field in self._map_coord_names.items():
# ignore not mapped parameters
if mango_role not in self._mivot_instance_dict:
continue
hk_field = self._mivot_instance_dict[key]
# format the observation time (J-year by default)
if value == "obstime":
# obstime must be set into the KK4 frame but not as an input parameter
fobstime = self._set_year_time_format(hk_field)
if isinstance(kwargs["frame"], FK4):
kwargs["frame"] = self._get_space_frame(obstime=fobstime)
hk_field = self._mivot_instance_dict[mango_role]
if mango_role == "obsDate":
besselian = isinstance(kwargs["frame"], FK4)
fobstime = self._get_time_instance(hk_field,
besselian=besselian)
# FK4 class has an obstime attribute which must be set at instanciation time
if besselian:
kwargs["frame"] = FK4(equinox=kwargs["frame"].equinox, obstime=fobstime)
# This is not the case for any other space frames
else:
kwargs[value] = fobstime
# Convert the parallax (mango) into a distance
elif value == "distance":
kwargs[value] = (hk_field["value"]
* u.Unit(hk_field["unit"]).to(u.parsec, equivalencies=u.parallax()))
kwargs[value] = kwargs[value] * u.parsec
elif "unit" in hk_field and hk_field["unit"]:
kwargs[value] = hk_field["value"] * u.Unit(hk_field["unit"])
else:
kwargs[value] = hk_field["value"]
kwargs[skycoord_field] = fobstime
# ignore not set parameters
elif (hk_value := hk_field["value"]) is not None:
# Convert the parallax (mango) into a distance
if skycoord_field == "distance":
kwargs[skycoord_field] = (
(hk_value * u.Unit(hk_field["unit"])).to(u.parsec, equivalencies=u.parallax()))
elif "unit" in hk_field and hk_field["unit"]:
kwargs[skycoord_field] = hk_value * u.Unit(hk_field["unit"])
else:
kwargs[skycoord_field] = hk_value
return SkyCoord(**kwargs)

@@ -44,3 +44,4 @@ """

"""
#: Roles of the EpochPosition class that are supported
# Roles of the EpochPosition class that are supported
# Do not change the ordering: it used below to access fields
EpochPosition = [

@@ -195,1 +196,20 @@ "longitude",

radialVelocity = "spect.dopplerVeloc.opt"
class SkyCoordMapping:
"""
Mapping of the MANGO:EpochPosition parameters to the SkyCoord parameters
"""
default_params = {
Roles.EpochPosition[0]: 'ra', Roles.EpochPosition[1]: 'dec',
Roles.EpochPosition[2]: 'distance',
Roles.EpochPosition[3]: 'radial_velocity',
Roles.EpochPosition[4]: 'pm_ra_cosdec', Roles.EpochPosition[5]: 'pm_dec',
Roles.EpochPosition[6]: 'obstime'}
galactic_params = {
Roles.EpochPosition[0]: 'l', Roles.EpochPosition[1]: 'b',
Roles.EpochPosition[2]: 'distance',
Roles.EpochPosition[3]: 'radial_velocity',
Roles.EpochPosition[4]: 'pm_l_cosb', Roles.EpochPosition[5]: 'pm_b',
Roles.EpochPosition[6]: 'obstime'}

@@ -158,5 +158,7 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst

Return a list of TEMPLATES @tableref.
Returns
-------
list: TEMPLATES tablerefs
[string]
tablerefs of all TEMPLATES elements
"""

@@ -174,10 +176,12 @@ templates_found = []

"""
Return the TEMPLATES mapping block of the table matching @tableref.
If tableref is None returns all values of templates_blocks.
Return the TEMPLATES mapping block of the table identified @tableref.
If tableref is None or equals to Constant.FIRST_TABLE, return the first TEMPLATES.
Parameters
----------
tableref (str): @tableref of the searched TEMPLATES
Returns
-------
dict: TEMPLATES tablerefs and their mapping blocks {'tableref': mapping_block, ...}
XML element: matching TEMPLATES block or None
"""

@@ -188,2 +192,7 @@ # one table: name forced to DEFAULT or take the first

return tmpl
if tableref not in self._templates_blocks:
raise MivotError(
"No TEMPLATES with tableref=" + tableref)
return self._templates_blocks[tableref]

@@ -197,2 +206,3 @@

Get @dmtypes of all mapped instances
Returns

@@ -199,0 +209,0 @@ -------

@@ -34,3 +34,4 @@ """

"""
return self._format_xml(want.strip()) == self._format_xml(got.strip())
return (self._format_xml(want.strip())
== self._format_xml(got.strip()))

@@ -125,5 +126,6 @@ def output_difference(self, want, got):

xmltree2 = XMLOutputChecker.xmltree_from_file(xmltree2_file).getroot()
xml_str1 = etree.tostring(xmltree1).decode("utf-8")
xml_str2 = etree.tostring(xmltree2).decode("utf-8")
xml_str1 = etree.tostring(xmltree1).decode("utf-8").strip()
xml_str2 = etree.tostring(xmltree2).decode("utf-8").strip()
checker = XMLOutputChecker()
assert checker.check_output(xml_str1, xml_str2), f"XML trees differ:\n{xml_str1}\n---\n{xml_str2}"
<TEMPLATES tableref="Results">
<INSTANCE dmid="_ts_data" dmrole="" dmtype="cube:NDPoint">
<COLLECTION dmrole="cube:NDPoint.observable" dmtype="root_collection">
<COLLECTION dmrole="cube:NDPoint.observable">
<INSTANCE dmtype="cube:Observable">

@@ -5,0 +5,0 @@ <ATTRIBUTE dmrole="cube:DataAxis.dependent" dmtype="ivoa:boolean" value="False">

@@ -1,11 +0,6 @@

{
"COLLECTION": [
"coords:TimeSys",
"coords:SpaceSys",
"mango:coordinates.PhotometryCoordSys",
"ds:experiment.ObsDataset"
],
"INSTANCE": [
"ds:experiment.Target"
]
}
{"coords": "https://www.ivoa.net/xml/STC/20200908/Coords-v1.0.vo-dml.xml",
"cube": "https://volute.g-vo.org/svn/trunk/projects/dm/Cube/vo-dml/Cube-1.0.vo-dml.xml",
"ds": "https://volute.g-vo.org/svn/trunk/projects/dm/DatasetMetadata/vo-dml/DatasetMetadata-1.0.vo-dml.xml",
"ivoa": "https://www.ivoa.net/xml/VODML/IVOA-v1.vo-dml.xml",
"mango": "file:/Users/sao/Documents/IVOA/GitHub/ivoa-dm-examples/tmp/Mango-v1.0.vo-dml.xml",
"meas": "https://www.ivoa.net/xml/Meas/20200908/Meas-v1.0.vo-dml.xml"}

@@ -15,16 +15,7 @@ <?xml version="1.0" encoding="UTF-8"?>

</TEMPLATES>
<TEMPLATES tableref="coll_and_instances">
<COLLECTION dmrole="coll"/>
<TEMPLATES tableref="some_instances">
<INSTANCE dmtype="first"/>
<INSTANCE dmtype="second"/>
</TEMPLATES>
<TEMPLATES tableref="one_collection">
<COLLECTION dmrole="OneCollection"/>
</TEMPLATES>
<TEMPLATES tableref="only_collection">
<COLLECTION dmrole="First"/>
<COLLECTION dmrole="Second"/>
</TEMPLATES>
<TEMPLATES tableref="empty">
</TEMPLATES>
<TEMPLATES tableref="empty"/>
</VODML>

@@ -31,0 +22,0 @@ </RESOURCE>

@@ -15,3 +15,3 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst

from pyvo.mivot.viewer import MivotViewer
from . import XMLOutputChecker
from pyvo.mivot.tests import XMLOutputChecker

@@ -28,2 +28,10 @@

@pytest.fixture
def a_multiple_seeker():
m_viewer = MivotViewer(
get_pkg_data_filename("data/test.instance_multiple.xml"),
tableref="Results")
return AnnotationSeeker(m_viewer._mapping_block)
@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+")

@@ -92,1 +100,7 @@ def test_multiple_templates():

assert a_seeker.get_globals_instance_from_collection("wrong_dmid", "ICRS") is None
@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+")
def test_multiple_seeker(a_multiple_seeker):
assert (a_multiple_seeker.get_instance_dmtypes()["TEMPLATES"]
== {"Results": ["mango:Brightness", "mango:Brightness", "mango:Brightness"]})

@@ -155,4 +155,3 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst

add_epoch_positon(builder)
builder.pack_into_votable()
XmlUtils.pretty_print(builder._annotation.mivot_block)
builder.pack_into_votable(schema_check=False)
assert XmlUtils.strip_xml(builder._annotation.mivot_block) == (

@@ -175,5 +174,5 @@ XmlUtils.strip_xml(get_pkg_data_contents("data/reference/mango_object.xml"))

builder.add_mango_epoch_position(**epoch_position_mapping)
builder.pack_into_votable()
builder.pack_into_votable(schema_check=True)
assert XmlUtils.strip_xml(builder._annotation.mivot_block) == (
XmlUtils.strip_xml(get_pkg_data_contents("data/reference/test_header_extraction.xml"))
)

@@ -7,10 +7,5 @@ '''

'''
import os
import pytest
from astropy.table import Table
from astropy.utils.data import get_pkg_data_filename
from pyvo.mivot.version_checker import check_astropy_version
from pyvo.mivot.viewer.mivot_instance import MivotInstance
from pyvo.mivot.utils.mivot_utils import MivotUtils
from pyvo.mivot.viewer import MivotViewer

@@ -107,18 +102,2 @@ fake_hk_dict = {

@pytest.fixture
def m_viewer():
data_path = get_pkg_data_filename(os.path.join("data",
"test.mivot_viewer.xml")
)
return MivotViewer(data_path, tableref="Results")
@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+")
def test_xml_viewer(m_viewer):
xml_instance = m_viewer.xml_viewer.view
dm_instance = MivotInstance(**MivotUtils.xml_to_dict(xml_instance))
assert dm_instance.to_dict() == test_dict
def test_mivot_instance_constructor():

@@ -125,0 +104,0 @@ """Test the class generation from a dict."""

@@ -16,3 +16,150 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst

dm_raw_instances = [
{
"dmrole": "",
"dmtype": "mango:Brightness",
"value": {
"dmtype": "ivoa:RealQuantity",
"value": None,
"unit": None,
"ref": "SC_EP_1_FLUX",
},
},
{
"dmrole": "",
"dmtype": "mango:Brightness",
"value": {
"dmtype": "ivoa:RealQuantity",
"value": None,
"unit": None,
"ref": "SC_EP_2_FLUX",
},
},
{
"dmrole": "",
"dmtype": "mango:Brightness",
"value": {
"dmtype": "ivoa:RealQuantity",
"value": None,
"unit": None,
"ref": "SC_EP_3_FLUX",
},
},
]
globals_photcal = {
"dmid": "CoordSystem_XMM_EB1_id",
"dmtype": "Phot:PhotCal",
"identifier": {
"dmtype": "ivoa:string",
"value": "XMM/EPIC/EB1",
"unit": None,
"ref": None,
},
"magnitudeSystem": {
"dmrole": "Phot:PhotCal.magnitudeSystem",
"dmtype": "Phot:MagnitudeSystem",
"type": {
"dmtype": "Phot:TypeOfMagSystem",
"value": "XMM",
"unit": None,
"ref": None,
},
"referenceSpectrum": {
"dmtype": "ivoa:anyURI",
"value": "https://xmm-tools.cosmos.esa.int/external"
"/xmm_user_support/documentation/sas_usg/USG/SASUSG.html",
"unit": None,
"ref": None,
},
},
"photometryFilter": {
"dmid": "CoordSystem_XMM_FILTER_EB1_id",
"dmtype": "Phot:PhotometryFilter",
"dmrole": "Phot:PhotCal.photometryFilter",
"identifier": {
"dmtype": "ivoa:string",
"value": "XMM/EPIC/EB1",
"unit": None,
"ref": None,
},
"name": {
"dmtype": "ivoa:string",
"value": "XMM EPIC EB1",
"unit": None,
"ref": None,
},
"description": {
"dmtype": "ivoa:string",
"value": "Soft",
"unit": None,
"ref": None,
},
"bandName": {
"dmtype": "ivoa:string",
"value": "EB1",
"unit": None,
"ref": None,
},
"spectralLocation": {
"dmrole": "Phot:PhotometryFilter.spectralLocation",
"dmtype": "Phot:SpectralLocation",
"ucd": {
"dmtype": "Phot:UCD",
"value": "em.wl.effective",
"unit": None,
"ref": None,
},
"unitexpression": {
"dmtype": "ivoa:Unit",
"value": "keV",
"unit": None,
"ref": None,
},
"value": {"dmtype": "ivoa:real", "value": 0.35, "unit": None, "ref": None},
},
"bandwidth": {
"dmrole": "Phot:PhotometryFilter.bandwidth",
"dmtype": "Phot:Bandwidth",
"ucd": {
"dmtype": "Phot:UCD",
"value": "instr.bandwidth;stat.fwhm",
"unit": None,
"ref": None,
},
"unitexpression": {
"dmtype": "ivoa:Unit",
"value": "keV",
"unit": None,
"ref": None,
},
"extent": {"dmtype": "ivoa:real", "value": 0.3, "unit": None, "ref": None},
"start": {"dmtype": "ivoa:real", "value": 0.2, "unit": None, "ref": None},
"stop": {"dmtype": "ivoa:real", "value": 0.5, "unit": None, "ref": None},
},
"transmissionCurve": {
"dmrole": "Phot:PhotometryFilter.transmissionCurve",
"dmtype": "Phot:TransmissionCurve",
"access": {
"dmrole": "Phot:TransmissionCurve.access",
"dmtype": "Phot:Access",
"reference": {
"dmtype": "ivoa:anyURI",
"value": "https://xmm-tools.cosmos.esa.int/external/xmm_user_support"
"/documentation/sas_usg/USG/SASUSG.html",
"unit": None,
"ref": None,
},
"format": {
"dmtype": "ivoa:string",
"value": "text/html",
"unit": None,
"ref": None,
},
},
},
},
}
@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+")

@@ -25,10 +172,12 @@ def test_get_first_instance_dmtype(path_to_first_instance):

m_viewer = MivotViewer(votable_path=path_to_first_instance)
assert m_viewer.get_first_instance_dmtype("one_instance") == "one_instance"
assert m_viewer.get_first_instance_dmtype("coll_and_instances") == "first"
assert m_viewer.get_first_instance_dmtype("one_collection") == Constant.ROOT_COLLECTION
assert m_viewer.get_first_instance_dmtype("only_collection") == Constant.ROOT_COLLECTION
with pytest.raises(Exception, match="Can't find the first INSTANCE/COLLECTION in TEMPLATES"):
m_viewer.get_first_instance_dmtype("empty")
assert m_viewer.get_dm_instance_dmtypes("one_instance")[0] == "one_instance"
assert m_viewer.get_dm_instance_dmtypes("some_instances")[0] == "first"
with pytest.raises(Exception, match="Can't find INSTANCE in TEMPLATES"):
m_viewer.get_dm_instance_dmtypes("empty")
with pytest.raises(Exception, match="No TEMPLATES with tableref=not_existing_tableref"):
m_viewer.get_dm_instance_dmtypes("not_existing_tableref")
@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+")

@@ -62,6 +211,5 @@ def test_table_ref(m_viewer):

assert m_viewer.get_table_ids() == ['_PKTable', 'Results']
assert m_viewer.get_globals_models() == DictUtils.read_dict_from_file(
assert m_viewer.get_models() == DictUtils.read_dict_from_file(
get_pkg_data_filename("data/reference/globals_models.json"))
assert m_viewer.get_templates_models() == DictUtils.read_dict_from_file(
get_pkg_data_filename("data/reference/templates_models.json"))
m_viewer._connect_table('_PKTable')

@@ -83,9 +231,8 @@ row = m_viewer.next_table_row()

"""
Test each getter for GLOBALS of the model_viewer specific .
Test the viewer behavior when there is no mapping
"""
m_viewer = MivotViewer(path_no_mivot)
assert m_viewer.get_table_ids() is None
assert m_viewer.get_globals_models() is None
assert m_viewer.get_models() is None
assert m_viewer.get_templates_models() is None
with pytest.raises(MappingError):

@@ -99,2 +246,53 @@ m_viewer._connect_table('_PKTable')

@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+")
def test_instance_mutiple_in_templates(path_to_multiple_instance):
"""
Test case with a TEMPLATES containing multiple instances
"""
m_viewer = MivotViewer(votable_path=path_to_multiple_instance)
instance_dict = []
# test the DM instances children of TEMPLATES before their values are set
for dmi in m_viewer.dm_instances:
instance_dict.append(dmi.to_hk_dict())
assert instance_dict == dm_raw_instances
# test the DM instances children of TEMPLATES set with the values of the first row
m_viewer.next_row_view()
row_values = []
for dmi in m_viewer.dm_instances:
row_values.append(dmi.value.value)
assert row_values == pytest.approx([0.0, 0.1, 0.2], rel=1e-3)
# test the DM instances children of TEMPLATES set with the values of the second row
m_viewer.next_row_view()
row_values = []
for dmi in m_viewer.dm_instances:
row_values.append(dmi.value.value)
assert row_values == pytest.approx([1.0, 2.1, 3.2], rel=1e-3)
@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+")
def test_globals_instances(path_to_multiple_instance):
"""
Test case for the GLOBALS instance access as MivotInstances
"""
m_viewer = MivotViewer(votable_path=path_to_multiple_instance)
instance_dict = []
photcals = 0
photfilters = 0
# test the DM instances children of TEMPLATES before their values are set
for dmi in m_viewer.dm_globals_instances:
if dmi.dmtype == "Phot:PhotCal":
photcals += 1
elif dmi.dmtype == "Phot:PhotometryFilter":
photfilters += 1
else:
assert False, f"Unexpected dmtype {dmi.dmtype} in GLOBALS "
instance_dict.append(dmi.to_hk_dict())
assert photcals == 3
assert photfilters == 3
# just check the first one
assert instance_dict[0] == globals_photcal
def test_check_version(path_to_viewer):

@@ -134,3 +332,11 @@ if not check_astropy_version():

@pytest.fixture
def path_to_multiple_instance():
votable_name = "test.instance_multiple.xml"
return get_pkg_data_filename(os.path.join("data", votable_name))
@pytest.fixture
def path_to_first_instance():
votable_name = "test.mivot_viewer.first_instance.xml"

@@ -137,0 +343,0 @@ return get_pkg_data_filename(os.path.join("data", votable_name))

@@ -10,2 +10,5 @@ '''

import pytest
from copy import deepcopy
from astropy.utils.data import get_pkg_data_filename
from astropy import units as u
from pyvo.mivot.version_checker import check_astropy_version

@@ -15,3 +18,8 @@ from pyvo.mivot.viewer.mivot_instance import MivotInstance

from pyvo.mivot.utils.exceptions import NoMatchingDMTypeError
from pyvo.mivot.viewer.mivot_viewer import MivotViewer
from pyvo.utils import activate_features
# Enable MIVOT-specific features in the pyvo library
activate_features("MIVOT")
# annotations generated by Vizier as given to the MivotInstance

@@ -187,3 +195,3 @@ vizier_dict = {

mivot_instance = MivotInstance(**vizier_dummy_type)
scb = SkyCoordBuilder(mivot_instance.to_dict())
scb = SkyCoordBuilder(mivot_instance)
scb.build_sky_coord()

@@ -197,3 +205,3 @@

mivot_instance = MivotInstance(**vizier_dict)
scb = SkyCoordBuilder(mivot_instance.to_dict())
scb = SkyCoordBuilder(mivot_instance)
scoo = scb.build_sky_coord()

@@ -225,19 +233,72 @@ assert (str(scoo).replace("\n", "").replace(" ", "")

def test_vizier_output_with_equinox_and_parallax():
"""Test the SkyCoord issued from the modofier Vizier response *
"""Test the SkyCoord issued from the modified Vizier response *
(parallax added and FK5 + Equinox frame)
"""
mivot_instance = MivotInstance(**vizier_equin_dict)
scb = SkyCoordBuilder(mivot_instance.to_dict())
scb = SkyCoordBuilder(mivot_instance)
scoo = scb.build_sky_coord()
assert (str(scoo).replace("\n", "").replace(" ", "")
== "<SkyCoord (FK5: equinox=J2012.000): (ra, dec, distance) in "
"(deg, deg, pc)(52.26722684, 59.94033461, 600.) "
"(deg, deg, pc)(52.26722684, 59.94033461, 1666.66666667) "
"(pm_ra_cosdec, pm_dec) in mas / yr(-0.82, -1.85)>")
vizier_equin_dict["spaceSys"]["frame"]["spaceRefFrame"]["value"] = "FK4"
mivot_instance = MivotInstance(**vizier_equin_dict)
mydict = deepcopy(vizier_equin_dict)
mydict["spaceSys"]["frame"]["spaceRefFrame"]["value"] = "FK4"
mivot_instance = MivotInstance(**mydict)
scoo = mivot_instance.get_SkyCoord()
assert (str(scoo).replace("\n", "").replace(" ", "")
== "<SkyCoord (FK4: equinox=B2012.000, obstime=J1991.250): (ra, dec, distance) in "
"(deg, deg, pc)(52.26722684, 59.94033461, 600.) "
== "<SkyCoord (FK4: equinox=B2012.000, obstime=B1991.250): (ra, dec, distance) in "
"(deg, deg, pc)(52.26722684, 59.94033461, 1666.66666667) "
"(pm_ra_cosdec, pm_dec) in mas / yr(-0.82, -1.85)>")
@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+")
def test_simad_cs_output():
"""Test the SkyCoord issued from a Simbad SCS response
"""
filename = get_pkg_data_filename('data/simbad-cone-mivot.xml')
m_viewer = MivotViewer(filename, resolve_ref=True)
mivot_instance = m_viewer.dm_instance
scb = SkyCoordBuilder(mivot_instance)
scoo = scb.build_sky_coord()
assert scoo.ra.degree == pytest.approx(269.45207696)
assert scoo.dec.degree == pytest.approx(4.69336497)
assert scoo.distance.pc == pytest.approx(1.82823411)
x = scoo.pm_ra_cosdec.value
y = (-801.551 * u.mas/u.yr).value
assert x == pytest.approx(y)
x = scoo.pm_dec.value
y = (10362.394 * u.mas/u.yr).value
assert x == pytest.approx(y)
assert str(scoo.obstime) == "J2000.000"
def test_time_representation():
"""
Test various time representations
Inconsistent values are not tested since there are detected by ``astropy.core.Time``
"""
# work with a copy to not alter other test functions
mydict = deepcopy(vizier_equin_dict)
mydict["obsDate"]["unit"] = "mjd"
mivot_instance = MivotInstance(**mydict)
scb = SkyCoordBuilder(mivot_instance)
scoo = scb.build_sky_coord()
assert scoo.obstime.jyear_str == "J1864.331"
mydict["obsDate"]["unit"] = "jd"
mydict["obsDate"]["value"] = "2460937.36"
mivot_instance = MivotInstance(**mydict)
scb = SkyCoordBuilder(mivot_instance)
scoo = scb.build_sky_coord()
assert scoo.obstime.jyear_str == "J2025.715"
mydict = deepcopy(vizier_equin_dict)
mydict["obsDate"]["unit"] = "iso"
mydict["obsDate"]["dmtype"] = "ivoa:string"
mydict["obsDate"]["value"] = "2025-05-03"
mivot_instance = MivotInstance(**mydict)
scb = SkyCoordBuilder(mivot_instance)
scoo = scb.build_sky_coord()
assert scoo.obstime.jyear_str == "J2025.335"

@@ -18,3 +18,2 @@ """

ref_ra = [

@@ -21,0 +20,0 @@ 0.04827189,

@@ -16,4 +16,2 @@ """

COL_INDEX = "col_index"
ROOT_COLLECTION = "root_collection"
ROOT_OBJECT = "root_object"
NOT_SET = "NotSet"

@@ -20,0 +18,0 @@ ANONYMOUS_TABLE = "AnonymousTable"

@@ -13,3 +13,2 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst

"""
from pyvo.mivot.utils.vocabulary import Constant
from pyvo.utils.prototype import prototype_feature

@@ -46,5 +45,11 @@ from pyvo.mivot.utils.mivot_utils import MivotUtils

def __str__(self):
"""
return a human readable representation of object
"""
return f"<MivotInstance: dmtype=\"{self.dmtype}\">"
def __repr__(self):
"""
return a human readable (json) representation of object
return a human readable (json) unambigous representation of object
"""

@@ -80,5 +85,2 @@ return DictUtils._get_pretty_json(self.to_dict())

for key, value in kwargs.items():
# roles are used as key and the first element in a TEMPLATE has no role
if not key:
key = Constant.ROOT_OBJECT
if isinstance(value, list): # COLLECTION

@@ -137,3 +139,3 @@ setattr(self, self._remove_model_name(key), [])

"""
return SkyCoordBuilder(self.to_dict()).build_sky_coord()
return SkyCoordBuilder(self).build_sky_coord()

@@ -193,2 +195,3 @@ @staticmethod

# This case is likely not to occur because MIVOT does not support dictionaries
if isinstance(obj, dict):

@@ -195,0 +198,0 @@ data = {}

@@ -45,3 +45,2 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst

from pyvo.mivot.utils.mivot_utils import MivotUtils
from pyvo.mivot.viewer.xml_viewer import XMLViewer
# Use defusedxml only if already present in order to avoid a new depency.

@@ -66,8 +65,11 @@ try:

----------
votable_path : str, parsed VOTable or DALResults instance
VOTable that will be parsed with the parser of Astropy,
which extracts the annotation block.
votable_path : str, DALResults, VOTableFile
Reference of the VOTable from which Astropy will extracts the annotation block
tableref : str, optional
Used to identify the table to process. If not specified,
the first table is taken by default.
resolve_ref : boolean
Ask for references between MIVOT instances to be resolved (referenced instances
are copied into the host object). This is usually used to copy the coordinates
systems into the object that uses them.
Parameters

@@ -104,3 +106,4 @@ ----------

self._resource_seeker = None
self._dm_instance = None
self._dm_instances = []
self._dm_globals_instances = []
self._resolve_ref = resolve_ref

@@ -113,3 +116,4 @@ try:

self._connect_table(tableref)
self._init_instance()
self._init_instances()
self._init_globals_instances()
except MappingError as mnf:

@@ -169,31 +173,35 @@ logging.error(str(mnf))

-------
A Python object (MivotInstance) built from the XML view of
the mapped model with attribute values set from the last values
of the last read data rows
MivotInstance: The Python object (MivotInstance) built from the XML view of the
first 'TEMPLATES' child, with the attribute values set according
to the values of the current read data row.
"""
return self._dm_instance
dm_instances = self._dm_instances
return self.dm_instances[0] if dm_instances else None
@property
def xml_view(self):
def dm_instances(self):
"""
returns
Returns
-------
The XML view on the current data row
[MivotInstance]: The list of Python objects (MivotInstance) built from the XML views of
the TEMPLATES children, whose attribute values are set from the values
of the current read data row.
"""
return self.xml_viewer.view
return self._dm_instances
@property
def xml_viewer(self):
def dm_globals_instances(self):
"""
returns
XMLViewer tuned to browse the TEMPLATES content
Returns
-------
[MivotInstance]: The list of Python objects (MivotInstance) built from the XML views of
the GLOBALS children, whose attribute values are set from the values
of the current read data row.
This method allows to retrieve the GLOBALS (coordinates systems usually)
even when the viewer is in ``resolve_ref=False`` mode or if the reference
to the coordinates systems have not been setup in the objects representing
the mapped data.
"""
# build a first XMLViewer for extract the content of the TEMPLATES element
model_view = XMLViewer(self._get_model_view())
first_instance_dmype = self.get_first_instance_dmtype(tableref=self.connected_table_ref)
model_view.get_instance_by_type(first_instance_dmype)
return self._dm_globals_instances
# return an XMLViewer tuned to process the TEMPLATES content
return XMLViewer(model_view._xml_view)
@property

@@ -206,8 +214,9 @@ def table_row(self):

"""
jump to the next table row and update the MivotInstance instance
jump to the next table row and update the MivotInstance instance with the row values
returns
Returns
-------
MivotInstance: the updated instance or None
it he able end has been reached
[MivotInstance]
List of updated instances or None
it he able end has been reached
"""

@@ -218,8 +227,7 @@ self.next_table_row()

return None
self._init_instances()
if self._dm_instance is None:
xml_instance = self.xml_viewer.view
self._dm_instance = MivotInstance(**MivotUtils.xml_to_dict(xml_instance))
self._dm_instance.update(self._current_data_row)
return self._dm_instance
for dm_instance in self._dm_instances:
dm_instance.update(self._current_data_row)
return self._dm_instances

@@ -234,21 +242,2 @@ def get_table_ids(self):

def get_globals_models(self):
"""
Get collection types in GLOBALS.
Collection types are GLOBALS/COLLECTION/INSTANCE@dmtype:
used for collections of static objects.
Returns
-------
dict
A dictionary containing the dmtypes of all the top-level INSTANCE/COLLECTION of GLOBALS.
The structure of the dictionary is {'COLLECTION': [dmtypes], 'INSTANCE': [dmtypes]}.
"""
if self._annotation_seeker is None:
return None
globals_models = {}
globals_models[Ele.COLLECTION] = self._annotation_seeker.get_globals_collection_dmtypes()
globals_models[Ele.INSTANCE] = self._annotation_seeker.get_globals_instance_dmtypes()
return globals_models
def get_models(self):

@@ -267,20 +256,2 @@ """

def get_templates_models(self):
"""
Get dmtypes (except ivoa:..) of all INSTANCE/COLLECTION of all TEMPLATES.
Note: COLLECTION not implemented yet.
Returns
-------
dict: A dictionary containing dmtypes of all INSTANCE/COLLECTION of all TEMPLATES.
The format is {'tableref': {'COLLECTIONS': [dmtypes], 'INSTANCE': [dmtypes]}, ...}.
"""
if self._annotation_seeker is None:
return None
templates_models = {}
gni = self._annotation_seeker.get_instance_dmtypes()[Ele.TEMPLATES]
for tid, tmplids in gni.items():
templates_models[tid] = {Ele.COLLECTION: [], Ele.INSTANCE: tmplids}
return templates_models
def next_table_row(self):

@@ -305,45 +276,44 @@ """

def get_first_instance_dmtype(self, tableref=None):
def _get_templates_child_instances(self, tableref=None):
"""
Return the dmtype of the head INSTANCE (first TEMPLATES child).
If no INSTANCE is found, take the first COLLECTION.
Returns
-------
[`xml.etree.ElementTree.Element`]
List of all INSTANCES elements children of the current TEMPLATES block
"""
if self._annotation_seeker is None:
return None
templates_block = self._annotation_seeker.get_templates_block(tableref)
return XPath.x_path(templates_block, ".//" + Ele.INSTANCE)
def get_dm_instance_dmtypes(self, tableref):
"""
Return the dmtypes of the INSTANCEs children of the
TEMPLATES block mapping the data table identified by tableref.
Parameters
----------
tableref : str or None, optional
Identifier of the table.
tableref : str or None
Identifier of the data table.
Returns
-------
~`xml.etree.ElementTree.Element`
The first child of TEMPLATES.
[string]
list of dmtypes
Raises
------
MivotError
if no INSTANCE can be found
"""
if self._annotation_seeker is None:
return None
child_template = self._annotation_seeker.get_templates_block(tableref)
child = child_template.findall("*")
collection = XPath.x_path(self._annotation_seeker.get_templates_block(tableref),
".//" + Ele.COLLECTION)
instance = XPath.x_path(self._annotation_seeker.get_templates_block(tableref), ".//" + Ele.INSTANCE)
if len(collection) >= 1:
collection[0].set(Att.dmtype, Constant.ROOT_COLLECTION)
(self._annotation_seeker.get_templates_block(tableref).find(".//" + Ele.COLLECTION)
.set(Att.dmtype, Constant.ROOT_COLLECTION))
if len(child) > 1:
if len(instance) >= 1:
for inst in instance:
if inst in child:
return inst.get(Att.dmtype)
elif len(collection) >= 1:
for coll in collection:
if coll in child:
return coll.get(Att.dmtype)
elif len(child) == 1:
if child[0] in instance:
return child[0].get(Att.dmtype)
elif child[0] in collection:
return collection[0].get(Att.dmtype)
else:
raise MivotError("Can't find the first " + Ele.INSTANCE
+ "/" + Ele.COLLECTION + " in " + Ele.TEMPLATES)
dmtypes = []
templates_block = self._annotation_seeker.get_templates_block(tableref)
instances = XPath.x_path(templates_block, ".//" + Ele.INSTANCE)
for instance in instances:
dmtypes.append(instance.get(Att.dmtype))
if not dmtypes:
raise MivotError("Can't find " + Ele.INSTANCE + " in " + Ele.TEMPLATES)
return dmtypes
def _connect_table(self, tableref=None):

@@ -392,8 +362,16 @@ """

def _get_model_view(self):
def _get_model_view(self, xml_instance):
"""
Return an XML model view of the last read row.
This function resolves references by default.
- References are possibly resolved here.
- ``ATTRIBUTE@value`` are set with actual data row values
Returns
-------
`xml.etree.ElementTree.Element`
XML model view of the last read row.
"""
templates_copy = deepcopy(self._templates)
templates_copy = deepcopy(xml_instance)
if self._resolve_ref is True:

@@ -411,3 +389,2 @@ while StaticReferenceResolver.resolve(self._annotation_seeker, self._connected_tableref,

.get_id_unit_mapping(self._connected_tableref))
# for ele in templates_copy.xpath("//ATTRIBUTE"):
for ele in XPath.x_path(templates_copy, ".//ATTRIBUTE"):

@@ -420,21 +397,41 @@ ref = ele.get(Att.ref)

def _init_instance(self):
def _init_instances(self):
"""
Read the first table row and build the MivotInstance (_instance attribute) from it.
The table row iterator in rewind at he end to make sure we won't lost the first data row.
Read the first table row and build all MivotInstances (_dm_instances attribute) from it.
The table row iterator in rewind at the end to make sure we won't lost the first data row.
"""
if self._dm_instance is None:
if not self._dm_instances:
self.next_table_row()
first_instance = self.get_first_instance_dmtype(tableref=self.connected_table_ref)
xml_instance = self.xml_viewer.get_instance_by_type(first_instance)
self._dm_instance = MivotInstance(**MivotUtils.xml_to_dict(xml_instance))
xml_instances = self._get_templates_child_instances(self.connected_table_ref)
self._dm_instances = []
for xml_instance in xml_instances:
self._dm_instances.append(
MivotInstance(
**MivotUtils.xml_to_dict(self._get_model_view(xml_instance))
))
self.rewind()
return self._dm_instance
def _init_globals_instances(self):
"""
Build one MivotInstance for each GLOBALS/INSTANCE. Internal references are always resolved
Globals MivotInstance are stored in the _dm_globals_instances list
"""
if not self._dm_globals_instances:
globals_copy = deepcopy(self._annotation_seeker.globals_block)
while StaticReferenceResolver.resolve(self._annotation_seeker, None,
globals_copy) > 0:
pass
for ele in XPath.x_path(globals_copy, "./" + Ele.INSTANCE):
self._dm_globals_instances.append(
MivotInstance(
**MivotUtils.xml_to_dict(ele)
))
def _set_mapped_tables(self):
"""
Set the mapped tables with a list of the TEMPLATES tablerefs.
Set the _mapped_tables list with the TEMPLATES tablerefs.
"""
if not self.resource_seeker:
self._mapped_table = []
self._mapped_tables = []
else:

@@ -494,3 +491,3 @@ self._mapped_tables = self._annotation_seeker.get_templates()

Add column ranks to attribute having a ref.
Using ranks allow identifying columns even numpy raw have been serialised as []
Using ranks allow identifying columns even when numpy raw have been serialised as []
"""

@@ -497,0 +494,0 @@ index_map = self._resource_seeker.get_id_index_mapping(self._connected_tableref)

@@ -173,3 +173,3 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst

The ID to associate with the <TEMPLATES> block. Defaults to None.
schema_check : boolean, optional
schema_check : boolean, optional (default True)
Skip the XSD validation if False (use to make test working in local mode).

@@ -176,0 +176,0 @@

@@ -200,3 +200,3 @@ """

else:
return ucd.startswith(dict_entry)
return ucd and ucd.startswith(dict_entry)

@@ -203,0 +203,0 @@ def _check_obs_date(field):

@@ -699,3 +699,3 @@ '''

def pack_into_votable(self, *, report_msg="", sparse=False):
def pack_into_votable(self, *, report_msg="", sparse=False, schema_check=True):
"""

@@ -706,7 +706,10 @@ Pack all mapped objects in the annotation block and put it in the VOTable.

----------
report_msg: string, optional (default to an empty string)
report_msg : string, optional (default to an empty string)
Content of the REPORT Mivot tag
sparse: boolean, optional (default to False)
sparse : boolean, optional (default to False)
If True, all properties are added in a independent way to the the TEMPLATES.
They are packed in a MangoObject otherwise.
schema_check : boolean, optional (default to True)
If True the MIVOT block is validated against its schema.
This may test failing due to remote accesses.
"""

@@ -723,3 +726,3 @@ self._annotation.set_report(True, report_msg)

self._annotation.build_mivot_block()
self._annotation.build_mivot_block(schema_check=schema_check)
self._annotation.insert_into_votable(self._votable, override=True)

@@ -12,5 +12,5 @@ <!-- XML Schema for the VODML lite mapping L. Michel 06/2020 -->

<!-- Required to validate mapping block within a VOTable (LM 08/2021) -->
<xs:import namespace="http://www.ivoa.net/xml/VOTable/v1.3" schemaLocation="http://www.ivoa.net/xml/VOTable/v1.3"/>
<xs:import namespace="http://www.ivoa.net/xml/VOTable/v1.2" schemaLocation="http://www.ivoa.net/xml/VOTable/v1.2"/>
<xs:import namespace="http://www.ivoa.net/xml/VOTable/v1.1" schemaLocation="http://www.ivoa.net/xml/VOTable/v1.1"/>
<xs:import namespace="http://www.ivoa.net/xml/VOTable/v1.3" schemaLocation="v1.3.xsd"/>
<xs:import namespace="http://www.ivoa.net/xml/VOTable/v1.2" schemaLocation="v1.2.xsd"/>
<xs:import namespace="http://www.ivoa.net/xml/VOTable/v1.1" schemaLocation="v1.1.xsd"/>

@@ -17,0 +17,0 @@ <!-- Top level structure of the mapping block -->

@@ -431,6 +431,12 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst

"standard_id like 'ivo://ivoa.net/std/sia#query-2.%'")
elif std == 'hats':
self.extra_fragments.append(
"standard_id like 'ivo://ivoa.net/std/hats#hats-%'")
elif std == 'hips':
self.extra_fragments.append(
"standard_id like 'ivo://ivoa.net/std/hips#hipslist-%'")
else:
raise dalq.DALQueryError("Service type {} is neither a full"
" standard URI nor one of the bespoke identifiers"
" {}, sia2".format(std, ", ".join(SERVICE_TYPE_MAP)))
" {}, sia2, hats, hips".format(std, ", ".join(SERVICE_TYPE_MAP)))

@@ -437,0 +443,0 @@ def clone(self):

@@ -715,3 +715,3 @@ #!/usr/bin/env python

assert len(w) == 1
assert str(w[0].message).startswith("Partial result set.")
assert str(w[0].message).startswith("Result set limited")

@@ -718,0 +718,0 @@

@@ -169,3 +169,3 @@ #!/usr/bin/env python

" image, sia, sia1, spectrum, ssap, ssa, scs, conesearch, line, slap,"
" table, tap, sia2")
" table, tap, sia2, hats, hips")

@@ -172,0 +172,0 @@ def test_legacy_term(self):

@@ -8,2 +8,2 @@ # Note that we need to fall back to the hard-coded version if either

except Exception:
version = '1.7.1'
version = '1.8'
{
"_PKTable": {
"COLLECTION": [],
"INSTANCE": [
"cube:SparseCube"
]
},
"Results": {
"COLLECTION": [],
"INSTANCE": [
"cube:NDPoint",
"cube:Observable",
"meas:Time",
"coords:MJD",
"cube:Observable",
"meas:GenericMeasure",
"coords:PhysicalCoordinate",
"cube:Observable",
"meas:GenericMeasure",
"coords:PhysicalCoordinate",
"meas:Error",
"meas:Symmetrical"
]
}
}
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Test for mivot.viewer.model_viewer_level3.py and mivot.viewer.mivot_time.py
"""
import os
import pytest
from urllib.request import urlretrieve
from pyvo.mivot.version_checker import check_astropy_version
from pyvo.mivot.viewer import MivotViewer
from pyvo.mivot.utils.mivot_utils import MivotUtils
@pytest.mark.remote_data
@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+")
def test_model_viewer3(votable_test, simple_votable):
"""
Recursively compare an XML element with an element of MIVOT
class with the function recursive_xml_check.
This test run on 2 votables : votable_test and simple_votable.
"""
m_viewer_simple_votable = MivotViewer(votable_path=simple_votable)
MivotInstance = m_viewer_simple_votable.dm_instance
xml_simple_votable = m_viewer_simple_votable.xml_view
assert xml_simple_votable.tag == 'TEMPLATES'
recusive_xml_check(xml_simple_votable, MivotInstance)
m_viewer_votable_test = MivotViewer(votable_path=votable_test)
m_viewer_votable_test.next_row_view()
mivot_instance = m_viewer_votable_test.dm_instance
xml_votable_test = m_viewer_votable_test.xml_view
assert xml_simple_votable.tag == 'TEMPLATES'
recusive_xml_check(xml_votable_test, mivot_instance)
@pytest.mark.remote_data
@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+")
def recusive_xml_check(xml_simple_votable, MivotInstance):
if xml_simple_votable.tag == 'TEMPLATES':
recusive_xml_check(xml_simple_votable[0], MivotInstance)
else:
for child in xml_simple_votable:
if child.tag == 'INSTANCE':
for key, value in child.attrib.items():
if key == 'dmrole':
if value == '':
if child.tag == 'ATTRIBUTE':
recusive_xml_check(child,
getattr(MivotInstance,
MivotInstance._remove_model_name(
child.get('dmrole'))))
elif child.tag == 'INSTANCE':
recusive_xml_check(child, getattr(MivotInstance,
MivotInstance._remove_model_name
(child.get('dmrole'))))
else:
if child.tag == 'ATTRIBUTE':
recusive_xml_check(child, getattr(MivotInstance,
MivotInstance._remove_model_name(
child.get('dmrole'))))
elif child.tag == 'INSTANCE':
recusive_xml_check(child, getattr(MivotInstance,
MivotInstance._remove_model_name(
child.get('dmrole'))))
elif child.tag == 'COLLECTION':
recusive_xml_check(child, getattr(MivotInstance,
MivotInstance._remove_model_name(
child.get('dmrole'))))
elif child.tag == 'COLLECTION':
for key, value in child.attrib.items():
assert len(getattr(MivotInstance,
MivotInstance._remove_model_name(child.get('dmrole')))) == len(child)
i = 0
for child2 in child:
recusive_xml_check(child2, getattr(MivotInstance, MivotInstance._remove_model_name
(child.get('dmrole')))[i])
i += 1
elif child.tag == 'ATTRIBUTE':
MivotInstance_attribute = getattr(MivotInstance,
MivotInstance._remove_model_name(child.get('dmrole')))
for key, value in child.attrib.items():
if key == 'dmtype':
assert MivotInstance_attribute.dmtype in value
elif key == 'value':
if (MivotInstance_attribute.value is not None
and not isinstance(MivotInstance_attribute.value, bool)):
if isinstance(MivotInstance_attribute.value, float):
pytest.approx(float(value), MivotInstance_attribute.value, 0.0001)
else:
assert value == MivotInstance_attribute.value
elif child.tag.startswith("REFERENCE"):
# Viewer not in resolve_ref mode: REFRENCEs are not filtered
pass
else:
print(child.tag)
assert False
@pytest.mark.remote_data
@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+")
def test_dict_model_viewer3(votable_test, simple_votable):
"""
To test the generation of the MIVOT class, the function builds a ModelViewerLevel3
with his MIVOT class and his previous dictionary from XML.
Then, it calls the function recursive_check which recursively compares an element of MIVOT class
with the dictionary on which it was built.
MIVOT class is itself a dictionary with only essential information of the ModelViewerLevel3._dict.
This test run on 2 votables : votable_test and simple_votable.
"""
m_viewer_votable_test = MivotViewer(votable_path=votable_test)
m_viewer_votable_test.next_row_view()
mivot_instance = m_viewer_votable_test.dm_instance
_dict = MivotUtils.xml_to_dict(m_viewer_votable_test.xml_viewer.view)
recursive_check(mivot_instance, **_dict)
mivot_instance = m_viewer_votable_test.dm_instance
_dict = MivotUtils.xml_to_dict(m_viewer_votable_test.xml_view)
recursive_check(mivot_instance, **_dict)
def recursive_check(MivotInstance, **kwargs):
for key, value in kwargs.items():
# the root instance ha no role: this makes an empty value in the unpacked dict
if key == '':
continue
if isinstance(value, list):
nbr_item = 0
for item in value:
if isinstance(item, dict):
assert 'dmtype' in item.keys()
recursive_check(getattr(MivotInstance,
MivotInstance._remove_model_name(key))[nbr_item],
**item
)
nbr_item += 1
elif isinstance(value, dict) and 'value' not in value:
# for INSTANCE of INSTANCEs dmrole needs model_name
assert MivotInstance._remove_model_name(key, True) in vars(MivotInstance).keys()
recursive_check(getattr(MivotInstance, MivotInstance._remove_model_name(key, True)), **value)
else:
if isinstance(value, dict) and MivotInstance._is_leaf(**value):
assert value.keys().__contains__('dmtype' and 'value' and 'unit' and 'ref')
lower_dmtype = value['dmtype'].lower()
if "real" in lower_dmtype or "double" in lower_dmtype or "float" in lower_dmtype:
assert isinstance(value['value'], float)
elif "bool" in lower_dmtype:
assert isinstance(value['value'], bool)
elif value['dmtype'] is None:
assert (value['value'] in
('notset', 'noset', 'null', 'none', 'NotSet', 'NoSet', 'Null', 'None'))
else:
if value['value'] is not None:
assert isinstance(value['value'], str)
recursive_check(getattr(MivotInstance, MivotInstance._remove_model_name(key)), **value)
else:
assert key == 'dmtype' or 'value'
@pytest.fixture
def votable_test(data_path, data_sample_url):
votable_name = "vizier_csc2_gal.annot.xml"
votable_path = os.path.join(data_path, "data", votable_name)
urlretrieve(data_sample_url + votable_name,
votable_path)
yield votable_path
os.remove(votable_path)
@pytest.fixture
def simple_votable(data_path, data_sample_url):
votable_name = "simple-annotation-votable.xml"
votable_path = os.path.join(data_path, "data", votable_name)
urlretrieve(data_sample_url + votable_name,
votable_path)
yield votable_path
os.remove(votable_path)
@pytest.fixture
def data_path():
return os.path.dirname(os.path.realpath(__file__))
@pytest.fixture
def data_sample_url():
return "https://raw.githubusercontent.com/ivoa/dm-usecases/main/pyvo-ci-sample/"
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Test for mivot.viewer.model_viewer_level2.py
"""
import pytest
try:
from defusedxml.ElementTree import Element as element
except ImportError:
from xml.etree.ElementTree import Element as element
from astropy.utils.data import get_pkg_data_filename
from pyvo.mivot.version_checker import check_astropy_version
from pyvo.mivot.viewer import MivotViewer
from pyvo.mivot.utils.exceptions import MivotError
@pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+")
def test_xml_viewer(m_viewer):
m_viewer.next_row_view()
xml_viewer = m_viewer.xml_viewer
with pytest.raises(MivotError,
match="Cannot find dmrole wrong_role in any instances of the VOTable"):
xml_viewer.get_instance_by_role("wrong_role")
with pytest.raises(MivotError,
match="Cannot find dmrole wrong_role in any instances of the VOTable"):
xml_viewer.get_instance_by_role("wrong_role", all_instances=True)
with pytest.raises(MivotError,
match="Cannot find dmtype wrong_dmtype in any instances of the VOTable"):
xml_viewer.get_instance_by_type("wrong_dmtype")
with pytest.raises(MivotError,
match="Cannot find dmtype wrong_dmtype in any instances of the VOTable"):
xml_viewer.get_instance_by_type("wrong_dmtype", all_instances=True)
with pytest.raises(MivotError,
match="Cannot find dmrole wrong_role in any collections of the VOTable"):
xml_viewer.get_collection_by_role("wrong_role")
with pytest.raises(MivotError,
match="Cannot find dmrole wrong_role in any collections of the VOTable"):
xml_viewer.get_collection_by_role("wrong_role", all_instances=True)
instances_list_role = xml_viewer.get_instance_by_role("cube:MeasurementAxis.measure")
assert isinstance(instances_list_role, element)
instances_list_role = xml_viewer.get_instance_by_role("cube:MeasurementAxis.measure", all_instances=True)
assert len(instances_list_role) == 3
instances_list_type = xml_viewer.get_instance_by_type("cube:Observable")
assert isinstance(instances_list_type, element)
instances_list_type = xml_viewer.get_instance_by_type("cube:Observable", all_instances=True)
assert len(instances_list_type) == 3
collections_list_role = xml_viewer.get_collection_by_role("cube:NDPoint.observable")
assert isinstance(collections_list_role, element)
collections_list_role = xml_viewer.get_collection_by_role("cube:NDPoint.observable", all_instances=True)
assert len(collections_list_role) == 1
@pytest.fixture
def m_viewer():
return MivotViewer(get_pkg_data_filename("data/test.mivot_viewer.xml"),
tableref="Results")
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
XMLViewer provides several getters on XML instances built by
`pyvo.mivot.viewer.mivot_viewer`.
"""
from pyvo.mivot.utils.exceptions import MivotError
from pyvo.mivot.utils.xpath_utils import XPath
from pyvo.utils.prototype import prototype_feature
@prototype_feature('MIVOT')
class XMLViewer:
"""
The XMLViewer is used by `~pyvo.mivot.viewer.mivot_viewer`
to extract from the XML serialization of the model,
elements that will be used to build the dictionary from which
the Python class holding the mapped model will be generated.
"""
def __init__(self, xml_view):
self._xml_view = xml_view
@property
def view(self):
"""
getter returning the XML model view
returns
-------
XML model view to be parsed
by different methods
"""
return self._xml_view
def get_instance_by_role(self, dmrole, all_instances=False):
"""
If all_instances is False, return the first INSTANCE matching with @dmrole.
If all_instances is True, return a list of all instances matching with @dmrole.
Parameters
----------
dmrole : str
The @dmrole to look for.
all_instances : bool, optional
If True, returns a list of all instances, otherwise returns the first instance.
Default is False.
Returns
-------
Union[`xml.etree.ElementTree.Element`, List[`xml.etree.ElementTree.Element`], None]
If all_instances is False, returns the instance matching with @dmrole.
If all_instances is True, returns a list of all instances matching with @dmrole.
If no matching instance is found, returns None.
Raises
------
MivotElementNotFound
If dmrole is not found.
"""
instances = XPath.select_elements_by_atttribute(
self._xml_view,
"INSTANCE",
"dmrole",
dmrole)
if len(instances) == 0:
raise MivotError(
f"Cannot find dmrole {dmrole} in any instances of the VOTable")
if all_instances is False:
return instances[0]
else:
return instances
def get_instance_by_type(self, dmtype, all_instances=False):
"""
Return the instance matching with @dmtype.
If all_instances is False, returns the first INSTANCE matching with @dmtype.
If all_instances is True, returns a list of all instances matching with @dmtype.
Parameters
----------
dmtype : str
The @dmtype to look for.
all : bool, optional
If True, returns a list of all instances, otherwise returns the first instance.
Default is False.
Returns
-------
Union[~`xml.etree.ElementTree.Element`, List[~`xml.etree.ElementTree.Element`], None]
If all_instances is False, returns the instance matching with @dmtype.
If all_instances is True, returns a list of all instances matching with @dmtype.
If no matching instance is found, returns None.
Raises
------
MivotElementNotFound
If dmtype is not found.
"""
instances = XPath.select_elements_by_atttribute(
self._xml_view,
"INSTANCE",
"dmtype",
dmtype)
if len(instances) == 0:
raise MivotError(
f"Cannot find dmtype {dmtype} in any instances of the VOTable")
if all_instances is False:
return instances[0]
else:
return instances
def get_collection_by_role(self, dmrole, all_instances=False):
"""
Return the collection matching with @dmrole.
If all_instances is False, returns the first COLLECTION matching with @dmrole.
If all_instances is True, returns a list of all COLLECTION matching with @dmrole.
Parameters
----------
dmrole : str
The @dmrole to look for.
all_instances : bool, optional
If True, returns a list of all COLLECTION, otherwise returns the first COLLECTION.
Default is False.
Returns
-------
Union[~`xml.etree.ElementTree.Element`, List[~`xml.etree.ElementTree.Element`], None]
If all_instances is False, returns the collection matching with @dmrole.
If all_instances is True, returns a list of all collections matching with @dmrole.
If no matching collection is found, returns None.
Raises
------
MivotElementNotFound
If dmrole is not found.
"""
collections = XPath.select_elements_by_atttribute(
self._xml_view,
"COLLECTION",
"dmrole",
dmrole)
if len(collections) == 0:
raise MivotError(
f"Cannot find dmrole {dmrole} in any collections of the VOTable")
if all_instances is False:
return collections[0]
else:
return collections