From 58f02c5cd637a3874bc4b1bc4fbbd09f6578d734 Mon Sep 17 00:00:00 2001
From: Joshua Fraustro <36318163+jwfraustro@users.noreply.github.com>
Date: Wed, 2 Oct 2024 12:12:58 -0400
Subject: [PATCH] VOSICapabilities / TAPRegExt / VOResource (#25)
Addition of VOSI Capabilities, TAPRegExt and VOResource standards
---
README.md | 4 +
docs/source/pages/api/index.rst | 5 +-
docs/source/pages/api/tapregext_api.rst | 12 +
docs/source/pages/api/voresource_api.rst | 8 +
docs/source/pages/api/vosi_api.rst | 8 +
docs/source/pages/protocols/index.rst | 6 +-
docs/source/pages/protocols/tapregext.rst | 36 +
docs/source/pages/protocols/voresource.rst | 105 ++
docs/source/pages/protocols/vosi.rst | 28 +-
examples/snippets/tapregext/tapregext.py | 81 +
examples/snippets/voresource/voresource.py | 208 +++
examples/snippets/vosi/capabilities.py | 166 ++
pyproject.toml | 2 +-
.../TAPRegExt-v1.0-with-erratum1.xsd | 384 +++++
tests/tapregext/tapregext_models_test.py | 364 +++++
tests/voresource/VOResource-v1.1.xsd | 1345 +++++++++++++++++
tests/voresource/voresource_models_test.py | 799 ++++++++++
tests/vosi/VOSICapabilities-v1.0.xsd | 54 +
tests/vosi/capabilities_test.py | 245 +++
vo_models/stc/__init__.py | 0
vo_models/stc/models.py | 12 +
vo_models/tapregext/__init__.py | 19 +
vo_models/tapregext/models.py | 196 +++
vo_models/uws/models.py | 15 +-
vo_models/vodataservice/__init__.py | 3 +
vo_models/vodataservice/models.py | 83 +-
vo_models/voresource/__init__.py | 35 +-
vo_models/voresource/models.py | 472 ++++++
vo_models/voresource/types.py | 67 +-
vo_models/vosi/availability/models.py | 2 +-
vo_models/vosi/capabilities/__init__.py | 5 +
vo_models/vosi/capabilities/models.py | 33 +
32 files changed, 4776 insertions(+), 26 deletions(-)
create mode 100644 docs/source/pages/api/tapregext_api.rst
create mode 100644 docs/source/pages/protocols/tapregext.rst
create mode 100644 docs/source/pages/protocols/voresource.rst
create mode 100644 examples/snippets/tapregext/tapregext.py
create mode 100644 examples/snippets/voresource/voresource.py
create mode 100644 examples/snippets/vosi/capabilities.py
create mode 100644 tests/tapregext/TAPRegExt-v1.0-with-erratum1.xsd
create mode 100644 tests/tapregext/tapregext_models_test.py
create mode 100644 tests/voresource/VOResource-v1.1.xsd
create mode 100644 tests/voresource/voresource_models_test.py
create mode 100644 tests/vosi/VOSICapabilities-v1.0.xsd
create mode 100644 tests/vosi/capabilities_test.py
create mode 100644 vo_models/stc/__init__.py
create mode 100644 vo_models/stc/models.py
create mode 100644 vo_models/tapregext/__init__.py
create mode 100644 vo_models/tapregext/models.py
create mode 100644 vo_models/voresource/models.py
create mode 100644 vo_models/vosi/capabilities/__init__.py
create mode 100644 vo_models/vosi/capabilities/models.py
diff --git a/README.md b/README.md
index 73983e4..63761de 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,7 @@ The following IVOA protocols are currently supported:
- **VOSI (IVOA Support Interfaces) version 1.1**
- VOSI Availability
- VOSI Tables
+ - VOSI Capabilities
- **VODataService version 1.2 (limited)**
- DataType
- FKColumn
@@ -26,6 +27,9 @@ The following IVOA protocols are currently supported:
- TableParam
- TableSchema
- TableSet
+ - others
+- **VOResource version 1.1**
+- **TAPRegExt version 1.0**
You can read more about using these models in our documentation: https://vo-models.readthedocs.io/
diff --git a/docs/source/pages/api/index.rst b/docs/source/pages/api/index.rst
index 1d8493b..845abb1 100644
--- a/docs/source/pages/api/index.rst
+++ b/docs/source/pages/api/index.rst
@@ -1,6 +1,6 @@
.. _api:
-Developer Documentation
+API Reference
~~~~~~~~~~~~~~~~~~~~~~~
This section contains documentation on the package's modules and classes.
@@ -11,4 +11,5 @@ This section contains documentation on the package's modules and classes.
uws_api
vosi_api
voresource_api
- vodataservice_api
\ No newline at end of file
+ vodataservice_api
+ tapregext_api
\ No newline at end of file
diff --git a/docs/source/pages/api/tapregext_api.rst b/docs/source/pages/api/tapregext_api.rst
new file mode 100644
index 0000000..703574f
--- /dev/null
+++ b/docs/source/pages/api/tapregext_api.rst
@@ -0,0 +1,12 @@
+.. _tapregext_api:
+
+TAPRegExt API
+--------------
+
+Models
+^^^^^^
+
+.. automodule:: vo_models.tapregext.models
+ :members:
+ :no-inherited-members:
+ :exclude-members: model_config, model_fields
\ No newline at end of file
diff --git a/docs/source/pages/api/voresource_api.rst b/docs/source/pages/api/voresource_api.rst
index 400e449..56dc1e9 100644
--- a/docs/source/pages/api/voresource_api.rst
+++ b/docs/source/pages/api/voresource_api.rst
@@ -3,6 +3,14 @@
VOResource API
--------------
+Models
+^^^^^^
+
+.. automodule:: vo_models.voresource.models
+ :members:
+ :no-inherited-members:
+ :exclude-members: model_config, model_fields
+
Simple Types
^^^^^^^^^^^^
.. automodule:: vo_models.voresource.types
diff --git a/docs/source/pages/api/vosi_api.rst b/docs/source/pages/api/vosi_api.rst
index 2b8d574..71173bf 100644
--- a/docs/source/pages/api/vosi_api.rst
+++ b/docs/source/pages/api/vosi_api.rst
@@ -15,6 +15,14 @@ Tables
^^^^^^
.. automodule:: vo_models.vosi.tables.models
+ :members:
+ :no-inherited-members:
+ :exclude-members: model_config, model_fields,
+
+Capabilities
+^^^^^^^^^^^^
+
+.. automodule:: vo_models.vosi.capabilities.models
:members:
:no-inherited-members:
:exclude-members: model_config, model_fields,
\ No newline at end of file
diff --git a/docs/source/pages/protocols/index.rst b/docs/source/pages/protocols/index.rst
index 5811eea..1f6a0d4 100644
--- a/docs/source/pages/protocols/index.rst
+++ b/docs/source/pages/protocols/index.rst
@@ -3,11 +3,13 @@
Supported Protocols
~~~~~~~~~~~~~~~~~~~
-The following IVOA protocols are currently supported:
+The pages below contain some examples for each supported protocol. For a full list of all models, see the :ref:`api`.
.. toctree::
:maxdepth: 3
uws
vosi
- vodataservice
\ No newline at end of file
+ vodataservice
+ voresource
+ tapregext
\ No newline at end of file
diff --git a/docs/source/pages/protocols/tapregext.rst b/docs/source/pages/protocols/tapregext.rst
new file mode 100644
index 0000000..9203f0e
--- /dev/null
+++ b/docs/source/pages/protocols/tapregext.rst
@@ -0,0 +1,36 @@
+.. _tapregext:
+
+TAPRegExt
+----------
+
+TAPRegExt is an IVOA XML encoding standard for describing TAP service metadata. It is used by the TAP standard to describe the capabilities of a TAP service.
+
+`vo-models` currently supports the full TAPRegExt v1.0 standard. The key model is the ``TableAccess`` model, which represents the capabilities of a TAP server:
+
+Models
+^^^^^^
+
+TableAccess
+***********
+
+This model represents the capabilities of a TAP server, used as part of the VOSI Capabilities standard.
+
+.. grid:: 2
+ :gutter: 2
+
+ .. grid-item-card:: Model
+
+ .. literalinclude:: ../../../../examples/snippets/tapregext/tapregext.py
+ :language: python
+ :start-after: TableAccess-model-start
+ :end-before: TableAccess-model-end
+
+ .. grid-item-card:: XML Output
+
+ .. literalinclude:: ../../../../examples/snippets/tapregext/tapregext.py
+ :language: xml
+ :lines: 2-
+ :start-after: TableAccess-xml-start
+ :end-before: TableAccess-xml-end
+
+See the :ref:`tapregext_api` documentation for more information on the models and types available.
\ No newline at end of file
diff --git a/docs/source/pages/protocols/voresource.rst b/docs/source/pages/protocols/voresource.rst
new file mode 100644
index 0000000..bfa1999
--- /dev/null
+++ b/docs/source/pages/protocols/voresource.rst
@@ -0,0 +1,105 @@
+.. _voresource:
+
+VOResource
+----------
+
+VOResource is an IVOA XML encoding standard for describing resource metadata. It is used by various IVOA standards, such as VODataService, VOSI, and TAPRegExt to describe resources and the services that provide access to them.
+
+`vo-models` currently supports the full VOResource v1.1 standard. Some of the key elements include:
+
+Models
+^^^^^^
+
+Resource
+********
+
+Any entity or component of a VO application that is describable and identifiable by an IVOA Identifier.
+
+.. grid:: 2
+ :gutter: 2
+
+ .. grid-item-card:: Model
+
+ .. literalinclude:: ../../../../examples/snippets/voresource/voresource.py
+ :language: python
+ :start-after: Resource-model-start
+ :end-before: Resource-model-end
+
+ .. grid-item-card:: XML Output
+
+ .. literalinclude:: ../../../../examples/snippets/voresource/voresource.py
+ :language: xml
+ :lines: 2-
+ :start-after: Resource-xml-start
+ :end-before: Resource-xml-end
+
+Service
+*******
+
+A resource that can be invoked by a client to perform some action on its behalf.
+
+.. grid:: 2
+ :gutter: 2
+
+ .. grid-item-card:: Model
+
+ .. literalinclude:: ../../../../examples/snippets/voresource/voresource.py
+ :language: python
+ :start-after: Service-model-start
+ :end-before: Service-model-end
+
+ .. grid-item-card:: XML Output
+
+ .. literalinclude:: ../../../../examples/snippets/voresource/voresource.py
+ :language: xml
+ :lines: 2-
+ :start-after: Service-xml-start
+ :end-before: Service-xml-end
+
+Capability
+**********
+
+A description of what the service does (in terms of context-specific behavior), and how to use it (in terms of an interface).
+
+.. grid:: 2
+ :gutter: 2
+
+ .. grid-item-card:: Model
+
+ .. literalinclude:: ../../../../examples/snippets/voresource/voresource.py
+ :language: python
+ :start-after: Capability-model-start
+ :end-before: Capability-model-end
+
+ .. grid-item-card:: XML Output
+
+ .. literalinclude:: ../../../../examples/snippets/voresource/voresource.py
+ :language: xml
+ :lines: 2-
+ :start-after: Capability-xml-start
+ :end-before: Capability-xml-end
+
+Interface
+*********
+
+A description of a service interface.
+
+.. grid:: 2
+ :gutter: 2
+
+ .. grid-item-card:: Model
+
+ .. literalinclude:: ../../../../examples/snippets/voresource/voresource.py
+ :language: python
+ :start-after: Interface-model-start
+ :end-before: Interface-model-end
+
+ .. grid-item-card:: XML Output
+
+ .. literalinclude:: ../../../../examples/snippets/voresource/voresource.py
+ :language: xml
+ :lines: 2-
+ :start-after: Interface-xml-start
+ :end-before: Interface-xml-end
+
+See the :ref:`voresource_api` documentation for more information on the available models and types.
diff --git a/docs/source/pages/protocols/vosi.rst b/docs/source/pages/protocols/vosi.rst
index abe58c8..c6aee49 100644
--- a/docs/source/pages/protocols/vosi.rst
+++ b/docs/source/pages/protocols/vosi.rst
@@ -8,7 +8,7 @@ VOSI (VO Support Interface)
Availability
^^^^^^^^^^^^
-The Availability model is used to represent the response given by a UWS service to a
+The Availability model is used to represent the response given by a service to a
``GET /availability`` request.
.. grid:: 2
@@ -81,4 +81,28 @@ For requests to the ``GET /tables`` endpoint, you can use the ``TableSet`` model
:language: xml
:lines: 2-
:start-after: tableset-xml-start
- :end-before: tableset-xml-end
\ No newline at end of file
+ :end-before: tableset-xml-end
+
+Capabilities
+^^^^^^^^^^^^
+
+The VOSICapabilities model is used to represent the response given by a service to a
+``GET /capabilities`` request. Below is a relatively full example of a VOSI capabilities document for a TAP service.
+
+.. grid:: 2
+ :gutter: 2
+
+ .. grid-item-card:: Model
+
+ .. literalinclude:: ../../../../examples/snippets/vosi/capabilities.py
+ :language: python
+ :start-after: capabilities-model-start
+ :end-before: capabilities-model-end
+
+ .. grid-item-card:: XML Output
+
+ .. literalinclude:: ../../../../examples/snippets/vosi/capabilities.py
+ :language: xml
+ :lines: 2-
+ :start-after: capabilities-xml-start
+ :end-before: capabilities-xml-end
diff --git a/examples/snippets/tapregext/tapregext.py b/examples/snippets/tapregext/tapregext.py
new file mode 100644
index 0000000..1c99de3
--- /dev/null
+++ b/examples/snippets/tapregext/tapregext.py
@@ -0,0 +1,81 @@
+"""Snippets for TAPRegExt models and XML serialization."""
+from vo_models.tapregext.models import (
+ DataLimits,
+ DataModelType,
+ Language,
+ LanguageFeature,
+ LanguageFeatureList,
+ OutputFormat,
+ TableAccess,
+ TimeLimits,
+ Version,
+)
+
+# pylint: disable=invalid-name
+
+# [TableAccess-model-start]
+table_access_model = TableAccess(
+ data_model=[DataModelType(value="VOTable", ivo_id="ivo://ivoa.net/std/VOTable")],
+ language=[
+ Language(
+ name="ADQL",
+ version=[Version(value="2.0", ivo_id="ivo://ivoa.net/std/ADQL-2.0")],
+ description="Astronomical Data Query Language",
+ language_features=[
+ LanguageFeatureList(
+ feature=[
+ LanguageFeature(form="Formal notation", description="A description"),
+ LanguageFeature(form="Informal notation", description="Another description"),
+ ],
+ type="adql-some-feature",
+ )
+ ],
+ )
+ ],
+ output_format=[
+ OutputFormat(
+ mime="application/x-votable+xml",
+ alias=["VOTABLE"],
+ )
+ ],
+ retention_period=TimeLimits(default=10, hard=100),
+ output_limit=DataLimits(
+ default={"value": 10, "unit": "row"},
+ hard={"value": 100, "unit": "row"},
+ ),
+)
+# [TableAccess-model-end]
+
+# [TableAccess-xml-start]
+table_access_xml = """
+
+ VOTable
+
+ ADQL
+ 2.0
+ Astronomical Data Query Language
+
+
+
+ A description
+
+
+
+ Another description
+
+
+
+
+ application/x-votable+xml
+ VOTABLE
+
+
+ 10
+ 100
+
+
+ 10
+ 100
+
+
+""" # [TableAccess-xml-end]
diff --git a/examples/snippets/voresource/voresource.py b/examples/snippets/voresource/voresource.py
new file mode 100644
index 0000000..11d6be0
--- /dev/null
+++ b/examples/snippets/voresource/voresource.py
@@ -0,0 +1,208 @@
+"""Snippets for VOResource models and XML serialization."""
+from datetime import timezone as tz
+
+from vo_models.voresource.models import (
+ AccessURL,
+ Capability,
+ Contact,
+ Content,
+ Creator,
+ Curation,
+ Date,
+ Interface,
+ MirrorURL,
+ Relationship,
+ Resource,
+ ResourceName,
+ Rights,
+ SecurityMethod,
+ Service,
+ Source,
+ Validation,
+)
+from vo_models.voresource.types import UTCTimestamp
+
+# pylint: disable=invalid-name
+
+# [Resource-model-start]
+resource = Resource(
+ created=UTCTimestamp(1996, 3, 11, 19, 0, 0, tzinfo=tz.utc),
+ updated=UTCTimestamp(1996, 3, 11, 19, 0, 0, tzinfo=tz.utc),
+ status="active",
+ version="1.0",
+ validation_level=[Validation(value=0, validated_by="https://example.edu")],
+ title="Example Resource",
+ short_name="example",
+ identifier="https://example.edu",
+ alt_identifier=["bibcode:2008ivoa.spec.0222P"],
+ curation=Curation(
+ publisher=ResourceName(value="STScI"),
+ creator=[Creator(name=ResourceName(value="Doe, J."))],
+ contributor=[ResourceName(value="Example Resource")],
+ date=[Date(value="2021-01-01T00:00:00Z", role="update")],
+ version="1.0",
+ contact=[Contact(name=ResourceName(value="John Doe"))],
+ ),
+ content=Content(
+ subject=["Astronomy"],
+ description="Example description",
+ source=Source(value="https://example.edu", format="bibcode"),
+ reference_url="https://example.edu",
+ type=["Education"],
+ content_level=["General"],
+ relationship=[
+ Relationship(
+ relationship_type="isPartOf",
+ related_resource=[ResourceName(value="Example Resource", ivo_id="ivo://example.edu/resource")],
+ )
+ ],
+ ),
+)
+# [Resource-model-end]
+
+# [Resource-xml-start]
+resource_xml = """
+
+ 0
+ Example Resource
+ example
+ https://example.edu/
+ bibcode:2008ivoa.spec.0222P
+
+ STScI
+
+ Doe, J.
+
+ Example Resource
+ 2021-01-01T00:00:00.000Z
+ 1.0
+
+ John Doe
+
+
+
+ Astronomy
+ Example description
+ https://example.edu/
+ https://example.edu/
+ Education
+ General
+
+ isPartOf
+ Example Resource
+
+
+
+""" # [Resource-xml-end]
+
+# [Service-model-start]
+service = Service(
+ created=UTCTimestamp(1996, 3, 11, 19, 0, 0, tzinfo=tz.utc),
+ updated=UTCTimestamp(1996, 3, 11, 19, 0, 0, tzinfo=tz.utc),
+ status="active",
+ title="Example Service",
+ identifier="https://example.edu",
+ curation=Curation(
+ publisher=ResourceName(value="STScI"),
+ creator=[Creator(name=ResourceName(value="Doe, J."))],
+ contributor=[ResourceName(value="Example Resource")],
+ date=[Date(value="2021-01-01T00:00:00Z", role="update")],
+ version="1.0",
+ contact=[Contact(name=ResourceName(value="John Doe"))],
+ ),
+ content=Content(
+ subject=["Astronomy"],
+ description="Example description",
+ source=Source(value="https://example.edu", format="bibcode"),
+ reference_url="https://example.edu",
+ type=["Education"],
+ content_level=["General"],
+ relationship=[
+ Relationship(
+ relationship_type="isPartOf",
+ related_resource=[ResourceName(value="Example Resource", ivo_id="ivo://example.edu/resource")],
+ )
+ ],
+ ),
+ rights=[Rights(value="CC BY 4.0", rights_uri="https://creativecommons.org/licenses/by/4.0/")],
+ capability=[Capability(standard_id="ivo://ivoa.net/std/TAP")],
+)
+# [Service-model-end]
+
+# [Service-xml-start]
+service_xml = """
+
+ Example Service
+ https://example.edu/
+
+ STScI
+
+ Doe, J.
+
+ Example Resource
+ 2021-01-01T00:00:00.000Z
+ 1.0
+
+ John Doe
+
+
+
+ Astronomy
+ Example description
+ https://example.edu/
+ https://example.edu/
+ Education
+ General
+
+ isPartOf
+ Example Resource
+
+
+ CC BY 4.0
+
+
+""" # [Service-xml-end]
+
+# [Capability-model-start]
+capability_model = Capability(
+ standard_id="ivo://ivoa.net/std/TAP",
+ validation_level=[Validation(value=0, validated_by="https://example.edu")],
+ description="Example description",
+ interface=[Interface(version="1.0", role="std", access_url=[AccessURL(value="https://example.edu", use="full")])],
+)
+# [Capability-model-end]
+
+# [Capability-xml-start]
+capability_xml = """
+
+ 0
+ Example description
+
+ https://example.edu/
+
+
+""" # [Capability-xml-end]
+
+# [Interface-model-start]
+interface_model = Interface(
+ version="1.0",
+ role="std",
+ access_url=[AccessURL(value="https://example.edu", use="full")],
+ mirror_url=[MirrorURL(value="https://example.edu", title="Mirror")],
+ security_method=[SecurityMethod(standard_id="ivo://ivoa.net/std/Security#basic")],
+ test_querystring="test",
+)
+# [Interface-model-end]
+
+# [Interface-xml-start]
+interface_xml = """
+
+ https://example.edu/
+ https://example.edu/
+
+ test
+
+""" # [Interface-xml-end]
diff --git a/examples/snippets/vosi/capabilities.py b/examples/snippets/vosi/capabilities.py
new file mode 100644
index 0000000..257f127
--- /dev/null
+++ b/examples/snippets/vosi/capabilities.py
@@ -0,0 +1,166 @@
+"""Example snippets for VOSI capabilities."""
+
+from vo_models.tapregext.models import (
+ DataLimit,
+ DataLimits,
+ Language,
+ LanguageFeature,
+ LanguageFeatureList,
+ OutputFormat,
+ TableAccess,
+ Version,
+)
+from vo_models.vodataservice.models import ParamHTTP
+from vo_models.voresource.models import AccessURL, Capability, WebBrowser
+from vo_models.vosi.capabilities.models import VOSICapabilities
+
+# pylint: disable=invalid-name
+
+# [capabilities-model-start]
+vosi_capabilities_model = VOSICapabilities(
+ capability=[
+ TableAccess(
+ type="tr:TableAccess",
+ interface=[
+ ParamHTTP(
+ role="std",
+ version="1.1",
+ access_url=[AccessURL(use="full", value="https://someservice.edu/tap")],
+ )
+ ],
+ language=[
+ Language(
+ name="ADQL",
+ version=[Version(value="2.0", ivo_id="ivo://ivoa.net/std/ADQL#v2.0")],
+ description="ADQL-2.0. Positional queries using CONTAINS with POINT and CIRCLE are supported.",
+ language_features=[
+ LanguageFeatureList(
+ type="ivo://ivoa.net/std/TAPRegExt#features-adql-geo",
+ feature=[
+ LanguageFeature(form="POINT"),
+ LanguageFeature(form="CIRCLE"),
+ ],
+ ),
+ ],
+ )
+ ],
+ output_format=[
+ OutputFormat(
+ mime="application/x-votable+xml",
+ alias=["votable"],
+ ivo_id="ivo://ivoa.net/std/TAPRegExt#output-votable-td",
+ ),
+ OutputFormat(
+ mime="text/csv;header=present",
+ alias=["csv"],
+ ),
+ ],
+ output_limit=DataLimits(
+ default=DataLimit(unit="row", value=100000),
+ hard=DataLimit(unit="row", value=100000),
+ ),
+ ),
+ Capability(
+ standard_id="ivo://ivoa.net/std/VOSI#capabilities",
+ interface=[
+ ParamHTTP(
+ role="std",
+ access_url=[AccessURL(use="full", value="https://someservice.edu/tap/capabilities")],
+ )
+ ],
+ ),
+ Capability(
+ standard_id="ivo://ivoa.net/std/VOSI#availability",
+ interface=[
+ ParamHTTP(
+ role="std",
+ access_url=[AccessURL(use="full", value="https://someservice.edu/tap/availability")],
+ )
+ ],
+ ),
+ Capability(
+ standard_id="ivo://ivoa.net/std/VOSI#tables",
+ interface=[
+ ParamHTTP(
+ role="std",
+ version="1.1",
+ access_url=[AccessURL(use="full", value="https://someservice.edu/tap/tables")],
+ )
+ ],
+ ),
+ Capability(
+ standard_id="ivo://ivoa.net/std/DALI#examples",
+ interface=[
+ WebBrowser(
+ access_url=[AccessURL(use="full", value="https://someservice.edu/tap/examples")],
+ )
+ ],
+ ),
+ ]
+)
+# [capabilities-model-end]
+
+# [capabilities-xml-start]
+capabilities_xml = """
+
+
+ https://someservice.edu/tap
+
+
+ ADQL
+ 2.0
+
+ ADQL-2.0. Positional queries using CONTAINS with POINT and CIRCLE are supported.
+
+
+
+
+
+
+
+
+
+
+
+ application/x-votable+xml
+ votable
+
+
+ text/csv;header=present
+ csv
+
+
+ 100000
+ 100000
+
+
+
+
+
+ https://someservice.edu/tap/capabilities
+
+
+
+
+
+
+ https://someservice.edu/tap/availability
+
+
+
+
+
+
+ https://someservice.edu/tap/tables
+
+
+
+
+
+
+ https://someservice.edu/tap/examples
+
+
+
+
+""" # [capabilities-xml-end]
diff --git a/pyproject.toml b/pyproject.toml
index b9bbae1..e3fc39b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "vo-models"
-version = "0.3.1"
+version = "0.4.0"
authors = [
{name = "Joshua Fraustro", email="jfraustro@stsci.edu"},
{name = "MAST Archive Developers", email="archive@stsci.edu"}
diff --git a/tests/tapregext/TAPRegExt-v1.0-with-erratum1.xsd b/tests/tapregext/TAPRegExt-v1.0-with-erratum1.xsd
new file mode 100644
index 0000000..95d2e2b
--- /dev/null
+++ b/tests/tapregext/TAPRegExt-v1.0-with-erratum1.xsd
@@ -0,0 +1,384 @@
+
+
+
+
+
+
+
+ TAPRegExt
+ xs
+
+ tr
+
+ A description of the capabilities metadata for TAP services.
+
+
+
+
+ An abstract capability that fixes the standardID to the IVOA ID for the TAP
+ standard.
+ See vr:Capability for documentation on inherited children.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The capabilities of a TAP server.
+ The capabilities attempt to define most issues that the TAP standard leaves
+ to the implementors ("may", "should").
+
+
+
+
+
+
+
+ Identifier of IVOA-approved data model supported by the service.
+
+
+
+
+
+ Language supported by the service.
+
+
+
+
+
+ Output format supported by the service.
+
+
+
+
+
+ Upload method supported by the service.
+ The absence of upload methods indicates that the service does not
+ support uploads at all.
+
+
+
+
+
+ Limits on the time between job creation and destruction time.
+
+
+
+
+
+ Limits on executionDuration.
+
+
+
+
+
+ Limits on the size of data returned.
+
+
+
+
+
+ Limits on the size of uploaded data.
+
+
+
+
+
+
+
+
+
+
+
+ An IVOA defined data model, identified by an IVORN intended for machine
+ consumption and a short label intended for human comsumption.
+
+
+
+
+
+
+ The IVORN of the data model.
+
+
+
+
+
+
+
+
+ A query language supported by the service.
+ Each language element can describe one or more versions of a language.
+ Either name alone or name-version can be used as values for the server's LANG parameter.
+
+
+
+
+
+ The name of the language without a version suffix.
+
+
+
+
+
+ A version of the language supported by the server.
+
+
+
+
+
+ A short, human-readable description of the query language.
+
+
+
+
+
+ Optional features of the query language, grouped by feature type.
+ This includes listing user defined functions, geometry support, or
+ similar concepts.
+
+
+
+
+
+
+
+ One version of the language supported by the service.
+ If the service supports more than one version of the language, include
+ multiple version elements. It is recommended that you use a version numbering scheme like
+ MAJOR.MINOR in such a way that sorting by ascending character codes will leave the most
+ recent version at the bottom of the list.
+
+
+
+
+
+
+ An optional IVORN of the language.
+ To more formally define a language supported by a service, a resource
+ record for the language can be created, either centrally on the Registry of Registries
+ or by other registry operators. When such a record exists, the language element's
+ ivo-id should point to it.
+
+
+
+
+
+
+
+
+ An enumeration of non-standard or non-mandatory features of a specific type
+ implemented by the language.
+ A feature type is a language-dependent concept like "user defined
+ function", "geometry support", or possibly "units supported". A featureList gives all
+ features of a given type applicable for the service. Multiple featureLists are possible. All
+ feature in a given list are of the same type. This type is declared using the mandatory type
+ attribute, the value of which will typically be an IVORN. To see values defined in
+ TAPRegExt, retrieve the ivo://ivoa.net/std/TAPRegExt resource record and look for keys
+ starting with "features-".
+
+
+
+
+ A language feature of the type given by this element's type attribute.
+
+
+
+
+
+ The type of the features given here.
+ This is in general an IVORN. TAPRegExt itself gives IVORNs for defining
+ user defined functions and geometry support.
+
+
+
+
+
+
+ A non-standard or non-mandatory feature implemented by the language..
+
+
+
+
+ Formal notation for the language feature.
+ The syntax for the content of this element is defined by the type
+ attribute of its parent language list.
+
+
+
+
+ Human-readable freeform documentation for the language feature.
+
+
+
+
+
+
+
+ An output format supported by the service.
+ All TAP services must support VOTable output, preserving the MIME type of
+ the input. Other output formats are optional. The primary identifier for an output format is
+ the MIME type. If you want to register an output format, you must use a MIME type (or make
+ one up using the x- syntax), although the concrete MIME syntax is not enforced by the
+ schema. For more detailed specification, an IVORN may be used.
+
+
+
+
+
+ The MIME type of this format.
+ The format of this string is specified by RFC 2045. The service has to
+ accept this string as a value of the FORMAT parameter.
+
+
+
+
+
+ Other values of FORMAT ("shorthands") that make the service return
+ documents with the MIME type.
+
+
+
+
+
+
+
+ An optional IVORN of the output format.
+ When the MIME type does not uniquely define the format (or a generic MIME
+ like application/octet-stream or text/plain is given), the IVORN can point to a key or
+ StandardsRegExt document defining the format more precisely. To see values defined in
+ TAPRegExt, retrieve the ivo://ivoa.net/std/TAPRegExt resource record and look for keys
+ starting with "output-".
+
+
+
+
+
+
+
+ An upload method as defined by IVOA.
+ Upload methods are always identified by an IVORN. Descriptions can be
+ obtained by dereferencing this IVORN. To see values defined in TAPRegExt, retrieve the
+ ivo://ivoa.net/std/TAPRegExt resource record and look for keys starting with "upload-". You
+ can register custom upload methods, but you must use the standard IVORNs for the upload
+ methods defined in the TAP specification.
+
+
+
+
+
+
+ The IVORN of the upload method.
+
+
+
+
+
+
+
+
+
+ Time-valued limits, all values given in seconds.
+
+
+
+
+
+ The value of this limit for newly-created jobs, given in seconds.
+
+
+
+
+ The value this limit cannot be raised above, given in seconds.
+
+
+
+
+
+
+
+ Limits on data sizes, given in rows or bytes.
+
+
+
+
+
+ The value of this limit for newly-created jobs.
+
+
+
+
+ The value this limit cannot be raised above.
+
+
+
+
+
+
+
+ A limit on some data size, either in rows or in bytes.
+
+
+
+
+
+
+ The unit of the limit specified.
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/tapregext/tapregext_models_test.py b/tests/tapregext/tapregext_models_test.py
new file mode 100644
index 0000000..923f3f1
--- /dev/null
+++ b/tests/tapregext/tapregext_models_test.py
@@ -0,0 +1,364 @@
+"""Tests for TAPRegExt models."""
+
+from unittest import TestCase
+from xml.etree.ElementTree import canonicalize
+
+from lxml import etree
+
+from vo_models.tapregext.models import (
+ DataLimit,
+ DataLimits,
+ DataModelType,
+ Language,
+ LanguageFeature,
+ LanguageFeatureList,
+ OutputFormat,
+ TableAccess,
+ TimeLimits,
+ UploadMethod,
+ Version,
+)
+
+TAPREGEXT_NAMESPACE_HEADER = """xmlns:xs="http://www.w3.org/2001/XMLSchema"
+xmlns:vm="http://www.ivoa.net/xml/VOMetadata/v0.1"
+xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+"""
+
+with open("tests/tapregext/TAPRegExt-v1.0-with-erratum1.xsd") as schema_file:
+ tapregext_schema = etree.XMLSchema(etree.parse(schema_file))
+
+
+class TestVersion(TestCase):
+ """Tests the Version model."""
+
+ test_version_model = Version(value="1.0", ivo_id="ivo://ivoa.net/std/TAP")
+ test_version_xml = f'1.0'
+
+ def test_read_from_xml(self):
+ """Test reading a Version element from XML."""
+ version = Version.from_xml(self.test_version_xml)
+ self.assertEqual(version.value, "1.0")
+ self.assertEqual(version.ivo_id, "ivo://ivoa.net/std/TAP")
+
+ def test_write_xml(self):
+ """Test we can write a Version element to XML."""
+ test_xml = self.test_version_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml),
+ canonicalize(self.test_version_xml),
+ )
+
+
+class TestLanguageFeature(TestCase):
+ """Tests the LanguageFeature model."""
+
+ test_language_feature_model = LanguageFeature(form="Formal notation", description="A description")
+ test_language_feature_xml = (
+ f""
+ "A description"
+ )
+
+ def test_read_from_xml(self):
+ """Test reading a LanguageFeature element from XML."""
+ language_feature = LanguageFeature.from_xml(self.test_language_feature_xml)
+ self.assertEqual(language_feature.form, "Formal notation")
+ self.assertEqual(language_feature.description, "A description")
+
+ def test_write_xml(self):
+ """Test we can write a LanguageFeature element to XML."""
+ test_xml = self.test_language_feature_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml),
+ canonicalize(self.test_language_feature_xml),
+ )
+
+
+class TestOutputFormat(TestCase):
+ """Tests the OutputFormat model."""
+
+ test_output_format_model = OutputFormat(
+ mime="application/x-votable+xml",
+ alias=["VOTABLE"],
+ )
+ test_output_format_xml = (
+ f""
+ "application/x-votable+xml"
+ "VOTABLE"
+ ""
+ )
+
+ def test_read_from_xml(self):
+ """Test reading an OutputFormat element from XML."""
+ output_format = OutputFormat.from_xml(self.test_output_format_xml)
+ self.assertEqual(output_format.mime, "application/x-votable+xml")
+ self.assertEqual(output_format.alias[0], "VOTABLE")
+
+ def test_write_xml(self):
+ """Test we can write an OutputFormat element to XML."""
+ output_format_xml = self.test_output_format_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(etree.tostring(etree.fromstring(self.test_output_format_xml))),
+ canonicalize(
+ etree.tostring(etree.fromstring(output_format_xml)),
+ ),
+ )
+
+
+class TestUploadMethod(TestCase):
+ """Tests the UploadMethod model."""
+
+ test_upload_method_model = UploadMethod(
+ ivo_id="ivo://ivoa.net/std/TAP",
+ )
+ test_upload_method_xml = f''
+
+ def test_read_from_xml(self):
+ """Test reading an UploadMethod element from XML."""
+ upload_method = UploadMethod.from_xml(self.test_upload_method_xml)
+ self.assertEqual(upload_method.ivo_id, "ivo://ivoa.net/std/TAP")
+
+ def test_write_xml(self):
+ """Test we can write an UploadMethod element to XML."""
+ test_xml = self.test_upload_method_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(self.test_upload_method_xml),
+ canonicalize(test_xml),
+ )
+
+
+class TestTimeLimits(TestCase):
+ """Tests the TimeLimits model."""
+
+ test_time_limits_model = TimeLimits(default=10, hard=100)
+ test_time_limits_xml = (
+ f"10100"
+ )
+
+ def test_read_from_xml(self):
+ """Test reading a TimeLimits element from XML."""
+ time_limits = TimeLimits.from_xml(self.test_time_limits_xml)
+ self.assertEqual(time_limits.default, 10)
+ self.assertEqual(time_limits.hard, 100)
+
+ def test_write_xml(self):
+ """Test we can write a TimeLimits element to XML."""
+ test_xml = self.test_time_limits_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml),
+ canonicalize(self.test_time_limits_xml),
+ )
+
+
+class TestDataLimits(TestCase):
+ """Tests the DataLimits model."""
+
+ test_data_limits_model = DataLimits(
+ default={"value": 10, "unit": "row"},
+ hard={"value": 100, "unit": "row"},
+ )
+ test_data_limits_xml = (
+ f""
+ '10'
+ '100'
+ ""
+ )
+
+ def test_read_from_xml(self):
+ """Test reading a DataLimits element from XML."""
+ data_limits = DataLimits.from_xml(self.test_data_limits_xml)
+ self.assertEqual(data_limits.default.value, 10)
+ self.assertEqual(data_limits.hard.value, 100)
+
+ def test_write_xml(self):
+ """Test we can write a DataLimits element to XML."""
+ test_xml = self.test_data_limits_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml),
+ canonicalize(self.test_data_limits_xml),
+ )
+
+
+class TestDataLimit(TestCase):
+ """Tests the DataLimit model."""
+
+ test_data_limit_model = DataLimit(value=10, unit="byte")
+ test_data_limit_xml = f'10'
+
+ def test_read_from_xml(self):
+ """Test reading a DataLimit element from XML."""
+ data_limit = DataLimit.from_xml(self.test_data_limit_xml)
+ self.assertEqual(data_limit.value, 10)
+ self.assertEqual(data_limit.unit, "byte")
+
+ def test_write_xml(self):
+ """Test we can write a DataLimit element to XML."""
+ test_xml = self.test_data_limit_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml),
+ canonicalize(self.test_data_limit_xml),
+ )
+
+
+class TestLanguageFeatureList(TestCase):
+ """Tests the LanguageFeatureList model."""
+
+ test_language_feature_list_model = LanguageFeatureList(
+ feature=[
+ LanguageFeature(form="Formal notation", description="A description"),
+ LanguageFeature(form="Informal notation", description="Another description"),
+ ],
+ type="adql-some-feature",
+ )
+ test_language_feature_list_xml = (
+ f''
+ "A description"
+ "Another description"
+ ""
+ )
+
+ def test_read_from_xml(self):
+ """Test reading a LanguageFeatureList element from XML."""
+ language_feature_list = LanguageFeatureList.from_xml(self.test_language_feature_list_xml)
+ self.assertEqual(language_feature_list.feature[0].form, "Formal notation")
+ self.assertEqual(language_feature_list.feature[0].description, "A description")
+ self.assertEqual(language_feature_list.feature[1].form, "Informal notation")
+ self.assertEqual(language_feature_list.feature[1].description, "Another description")
+
+ def test_write_xml(self):
+ """Test we can write a LanguageFeatureList element to XML."""
+ test_xml = self.test_language_feature_list_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml),
+ canonicalize(self.test_language_feature_list_xml),
+ )
+
+
+class TestLanguage(TestCase):
+ """Tests the Language model."""
+
+ test_language_model = Language(
+ name="ADQL",
+ version=[Version(value="2.0", ivo_id="ivo://ivoa.net/std/ADQL")],
+ description="Astronomical Data Query Language",
+ language_features=[
+ LanguageFeatureList(
+ feature=[
+ LanguageFeature(form="Formal notation", description="A description"),
+ LanguageFeature(form="Informal notation", description="Another description"),
+ ],
+ type="adql-some-feature",
+ )
+ ],
+ )
+ test_language_xml = (
+ f""
+ "ADQL"
+ "2.0"
+ "Astronomical Data Query Language"
+ ''
+ "A description"
+ "Another description"
+ ""
+ ""
+ )
+
+ def test_read_from_xml(self):
+ """Test reading a Language element from XML."""
+ language = Language.from_xml(self.test_language_xml)
+ self.assertEqual(language.name, "ADQL")
+ self.assertEqual(language.version[0].value, "2.0")
+ self.assertEqual(language.description, "Astronomical Data Query Language")
+ self.assertEqual(language.language_features[0].feature[0].form, "Formal notation")
+ self.assertEqual(language.language_features[0].feature[0].description, "A description")
+ self.assertEqual(language.language_features[0].feature[1].form, "Informal notation")
+ self.assertEqual(language.language_features[0].feature[1].description, "Another description")
+
+ def test_write_xml(self):
+ """Test we can write a Language element to XML."""
+ test_xml = self.test_language_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml),
+ canonicalize(self.test_language_xml),
+ )
+
+
+class TestTableAccess(TestCase):
+ """Tests the TableAccess model."""
+
+ test_table_access_model = TableAccess(
+ data_model=[DataModelType(value="VOTable", ivo_id="ivo://ivoa.net/std/VOTable")],
+ language=[
+ Language(
+ name="ADQL",
+ version=[Version(value="2.0", ivo_id="ivo://ivoa.net/std/ADQL-2.0")],
+ description="Astronomical Data Query Language",
+ language_features=[
+ LanguageFeatureList(
+ feature=[
+ LanguageFeature(form="Formal notation", description="A description"),
+ LanguageFeature(form="Informal notation", description="Another description"),
+ ],
+ type="adql-some-feature",
+ )
+ ],
+ )
+ ],
+ output_format=[
+ OutputFormat(
+ mime="application/x-votable+xml",
+ alias=["VOTABLE"],
+ )
+ ],
+ retention_period=TimeLimits(default=10, hard=100),
+ output_limit=DataLimits(
+ default={"value": 10, "unit": "row"},
+ hard={"value": 100, "unit": "row"},
+ ),
+ )
+ test_table_access_xml = (
+ f''
+ "VOTable"
+ ""
+ "ADQL"
+ '2.0'
+ "Astronomical Data Query Language"
+ ''
+ "A description"
+ "Another description"
+ ""
+ ""
+ "application/x-votable+xmlVOTABLE"
+ "10100"
+ ""
+ '10'
+ '100'
+ ""
+ ""
+ )
+
+ def test_read_from_xml(self):
+ """Test reading a TableAccess element from XML."""
+ table_access = TableAccess.from_xml(self.test_table_access_xml)
+ self.assertEqual(table_access.data_model[0].value, "VOTable")
+ self.assertEqual(table_access.data_model[0].ivo_id, "ivo://ivoa.net/std/VOTable")
+ self.assertEqual(table_access.language[0].name, "ADQL")
+ self.assertEqual(table_access.language[0].version[0].value, "2.0")
+ self.assertEqual(table_access.language[0].description, "Astronomical Data Query Language")
+ self.assertEqual(table_access.language[0].language_features[0].feature[0].form, "Formal notation")
+ self.assertEqual(table_access.language[0].language_features[0].feature[0].description, "A description")
+ self.assertEqual(table_access.language[0].language_features[0].feature[1].form, "Informal notation")
+ self.assertEqual(table_access.language[0].language_features[0].feature[1].description, "Another description")
+ self.assertEqual(table_access.output_format[0].mime, "application/x-votable+xml")
+ self.assertEqual(table_access.output_format[0].alias[0], "VOTABLE")
+ self.assertEqual(table_access.retention_period.default, 10)
+ self.assertEqual(table_access.retention_period.hard, 100)
+ self.assertEqual(table_access.output_limit.default.value, 10)
+ self.assertEqual(table_access.output_limit.hard.value, 100)
+
+ def test_write_xml(self):
+ """Test we can write a TableAccess element to XML."""
+ test_xml = self.test_table_access_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml),
+ canonicalize(self.test_table_access_xml),
+ )
diff --git a/tests/voresource/VOResource-v1.1.xsd b/tests/voresource/VOResource-v1.1.xsd
new file mode 100644
index 0000000..ff6b4a4
--- /dev/null
+++ b/tests/voresource/VOResource-v1.1.xsd
@@ -0,0 +1,1345 @@
+
+
+
+
+
+
+
+ VOResource
+ xs
+ vr
+
+
+ An XML Schema describing a resource to be used in the Virtual
+ Observatory Project.
+
+ Please see http://www.ivoa.net/documents/latest/VOResource.html
+ for further information on the standard governing this
+ schema.
+
+
+
+
+
+
+ A timestamp that is compliant with ISO8601 and fixes
+ the timezone indicator, if present, to "Z" (UTC). VOResource
+ writers should always include the timezone marker. VOResource
+ readers must interpret timestamps without a timezone marker as
+ UTC.
+
+
+
+
+
+
+
+
+
+
+
+ A date stamp that can be given to a precision of either a
+ day (type xs:date) or seconds (type xs:dateTime). Where only a
+ date is given, it is to be interpreted as the span of the day
+ on the UTC timezone if such distinctions are relevant.
+
+
+
+
+
+
+
+
+ Any entity or component of a VO application that is
+ describable and identifiable by an IVOA Identifier.
+
+
+
+
+
+
+ A numeric grade describing the quality of the
+ resource description, when applicable,
+ to be used to indicate the confidence an end-user
+ can put in the resource as part of a VO application
+ or research study.
+
+
+ See vr:Validation for an explanation of the
+ allowed levels.
+
+
+ Note that when this resource is a Service, this
+ grade applies to the core set of metadata.
+ Capability and interface metadata, as well as the
+ compliance of the service with the interface
+ standard, is rated by validationLevel tag in the
+ capability element (see the vr:Service complex
+ type).
+
+
+
+
+
+
+
+ Title
+
+
+ the full name given to the resource
+
+
+
+
+
+
+
+ A short name or abbreviation given to the resource.
+
+
+ This name will be used where brief annotations for
+ the resource name are required. Applications may
+ use to refer to this resource in a compact display.
+
+
+ One word or a few letters is recommended. No more
+ than sixteen characters are allowed.
+
+
+
+
+
+
+
+ Identifier
+
+
+ Unambiguous reference to the resource conforming to the IVOA
+ standard for identifiers
+
+
+
+
+
+
+
+ A reference to this resource in a non-IVOA identifier
+ scheme, e.g., DOI or bibcode. Always use the an URI scheme
+ here, e.g., doi:10.1016/j.epsl.2011.11.037. For bibcodes,
+ use a form like bibcode:2008ivoa.spec.0222P.
+
+
+
+
+
+
+
+ Information regarding the general curation of the resource
+
+
+
+
+
+
+
+ Information regarding the general content of the resource
+
+
+
+
+
+
+
+
+
+ The UTC date and time this resource metadata description
+ was created.
+
+
+ This timestamp must not be in the future. This time is
+ not required to be accurate; it should be at least
+ accurate to the day. Any non-significant time fields
+ should be set to zero.
+
+
+
+
+
+
+
+ The UTC date this resource metadata description was last updated.
+
+
+ This timestamp must not be in the future. This time is
+ not required to be accurate; it should be at least
+ accurate to the day. Any non-significant time fields
+ should be set to zero.
+
+
+
+
+
+
+
+ a tag indicating whether this resource is believed to be still
+ actively maintained.
+
+
+
+
+
+
+
+ resource is believed to be currently maintained, and its
+ description is up to date (default).
+
+
+
+
+
+
+ resource is apparently not being maintained at the present.
+
+
+
+
+
+
+ resource publisher has explicitly deleted the resource.
+
+
+
+
+
+
+
+
+
+
+ The VOResource XML schema version
+ against which this instance was written.
+ Implementors should set this to the value of the version
+ attribute of their schema's root (xs:schema) element.
+ Clients may assume version 1.0 if this attribute is
+ missing.
+
+
+
+
+
+
+
+
+ The allowed values for describing the resource descriptions
+ and interfaces.
+
+
+ See the RM (v1.1, section 4) for more guidance on the use of
+ these values.
+
+
+
+
+
+
+
+ The resource has a description that is stored in a
+ registry. This level does not imply a compliant
+ description.
+
+
+
+
+
+
+ In addition to meeting the level 0 definition, the
+ resource description conforms syntactically to this
+ standard and to the encoding scheme used.
+
+
+
+
+
+
+ In addition to meeting the level 1 definition, the
+ resource description refers to an existing resource that
+ has demonstrated to be functionally compliant.
+
+
+ When the resource is a service, it is considered to exist
+ and functionally compliant if use of the
+ service accessURL responds without error when used as
+ intended by the resource. If the service is a standard
+ one, it must also demonstrate the response is syntactically
+ compliant with the service standard in order to be
+ considered functionally compliant. If the resource is
+ not a service, then the ReferenceURL must be shown to
+ return a document without error.
+
+
+
+
+
+
+ In addition to meeting the level 2 definition, the
+ resource description has been inspected by a human and
+ judged to comply semantically to this standard as well
+ as meeting any additional minimum quality criteria (e.g.,
+ providing values for important but non-required
+ metadata) set by the human inspector.
+
+
+
+
+
+
+ In addition to meeting the level 3 definition, the
+ resource description meets additional quality criteria
+ set by the human inspector and is therefore considered
+ an excellent description of the resource. Consequently,
+ the resource is expected to operate well as part of a
+ VO application or research study.
+
+
+
+
+
+
+
+
+
+ a validation stamp combining a validation level and the ID of
+ the validator.
+
+
+
+
+
+
+
+ The IVOA ID of the registry or organisation that
+ assigned the validation level.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ A reference to a registry record.
+
+
+ This type should only be used if what is referenced
+ must actually be a true Registry record; vr:IdentifierURI
+ does not allow query or fragment parts and is hence
+ not suitable for everything defined by IVOA Identifiers,
+ in particular not standard keys (which are used for versions
+ of standards, for instance) or dataset identifiers.
+
+ When something does not need to be locked down to a
+ reference to a single registry record, xs:anyURI should
+ be used.
+
+
+
+
+
+
+
+
+
+
+ A short name or abbreviation given to something.
+
+
+ This name will be used where brief annotations for
+ the resource name are required. Applications may
+ use to refer to this resource in a compact display.
+
+
+ One word or a few letters is recommended. No more
+ than sixteen characters are allowed.
+
+
+
+
+
+
+
+
+
+
+
+ Information regarding the general curation of a resource
+
+
+
+
+
+
+
+ Publisher
+
+
+ Entity (e.g. person or organisation) responsible for making the
+ resource available
+
+
+
+
+
+
+
+ Creator
+
+
+ The entity/ies (e.g. person(s) or organisation) primarily responsible
+ for creating the content or constitution of the resource.
+
+
+ This is the equivalent of the author of a publication.
+
+
+
+
+
+
+
+ Contributor
+
+
+ Entity responsible for contributions to the content of
+ the resource
+
+
+
+
+
+
+
+ Date
+
+
+ Date associated with an event in the life cycle of the
+ resource.
+
+
+ This will typically be associated with the creation or
+ availability (i.e., most recent release or version) of
+ the resource. Use the role attribute to clarify.
+
+
+
+
+
+
+
+ Label associated with creation or availablilty of a version of
+ a resource.
+
+
+
+
+
+
+
+ Information that can be used for contacting someone with
+ regard to this resource.
+
+
+
+
+
+
+
+
+
+
+ The name of a potentially registered resource. That is, the entity
+ referred to may have an associated identifier.
+
+
+
+
+
+
+
+
+
+ The IVOA identifier for the resource referred to.
+
+
+
+
+
+
+
+
+
+
+
+ Information allowing establishing contact, e.g., for purposes
+ of support.
+
+
+
+
+
+
+ the name or title of the contact person.
+
+
+ This can be a person's name, e.g. “John P. Jones” or
+ a group, “Archive Support Team”.
+
+
+
+
+
+
+ the contact mailing address
+
+ All components of the mailing address are given in one
+ string, e.g. “3700 San Martin Drive, Baltimore, MD 21218 USA”.
+
+
+
+
+
+
+ the contact email address
+
+
+
+
+
+ the contact telephone number
+
+ Complete international dialing codes should be given, e.g.
+ “+1-410-338-1234”.
+
+
+
+
+
+
+
+ A reference to this entitiy in a non-IVOA identifier
+ scheme, e.g., orcid. Always use a URI form including
+ a scheme here.
+
+
+
+
+
+
+
+
+
+ An IVOA identifier for the contact (typically when it is
+ an organization).
+
+
+
+
+
+
+
+
+
+ The entity (e.g. person or organisation) primarily responsible
+ for creating something
+
+
+
+
+
+
+
+ the name or title of the creating person or organisation
+
+
+ Users of the creation should use this name in
+ subsequent credits and acknowledgements.
+
+ This should be exactly one name, preferably last name
+ first (as in "van der Waals, Johannes Diderik").
+
+
+
+
+
+
+
+ URL pointing to a graphical logo, which may be used to help
+ identify the information source
+
+
+ A logo needs only be provided for the first occurrence.
+ When multiple logos are supplied via multiple creator
+ elements, the application is free to choose which to
+ use.
+
+
+
+
+
+
+
+ A reference to this entitiy in a non-IVOA identifier
+ scheme, e.g., orcid. Always use a URI form including
+ a scheme here.
+
+
+
+
+
+
+
+
+
+ An IVOA identifier for the creator (typically when it is
+ an organization).
+
+
+
+
+
+
+
+
+
+
+
+
+ A string indicating what the date refers to.
+
+
+ The value of role should be taken from the vocabulary
+ maintained at
+ http://www.ivoa.net/rdf/voresource/date_role.
+ This includes the traditional and deprecated strings
+ “creation”, indicating the date that the resource
+ itself was created, and “update”, indicating when the
+ resource was updated last, and the default value,
+ “representative”, meaning the date is a rough
+ representation of the time coverage of the resource.
+ The preferred terms from that vocabulary are the DataCite
+ Metadata terms. It is expected that the vocabulary will
+ be kept synchronous with the corresponding list of terms
+ in the DataCite Metadata schema.
+
+
+ Note that this date refers to the resource; dates describing
+ the metadata description of the resource are handled by
+ the “created” and “updated” attributes of the Resource
+ element.
+
+
+
+
+
+
+
+
+
+
+ Information regarding the general content of a resource
+
+
+
+
+
+
+
+ Subject
+
+
+ a topic, object type, or other descriptive keywords
+ about the resource.
+
+
+ Terms for Subject should be drawn from the Unified
+ Astronomy Thesaurus (http://astrothesaurus.org).
+
+
+
+
+
+
+
+ Description
+
+
+ An account of the nature of the resource
+
+
+ The description may include but is not limited to an abstract,
+ table of contents, reference to a graphical representation of
+ content or a free-text account of the content.
+
+ Note that description is xs:string-typed, which means that
+ whitespace is considered significant. Clients should
+ render empty lines as paragraph boundaries and ideally
+ refrain from reflowing material that looks formatted (i.e.,
+ is broken to about 80-character lines).
+
+
+
+
+
+
+
+ Source
+
+
+ a bibliographic reference from which the present resource is
+ derived or extracted.
+
+
+ This is intended to point to an article in the published
+ literature. An ADS Bibcode is recommended as a value when
+ available.
+
+
+
+
+
+
+
+ URL pointing to a human-readable document describing this
+ resource.
+
+
+
+
+
+
+
+ Type
+
+
+ Nature or genre of the content of the resource. Values for
+ type should be taken from the controlled vocabulary
+ http://www.ivoa.net/rdf/voresource/content_type
+
+
+
+
+
+
+
+ Subject
+ Subject.ContentLevel
+
+
+ Description of the content level or intended audience.
+ Values for contentLevel should be taken from the controlled
+ vocabulary
+ http://www.ivoa.net/rdf/voresource/content_level.
+
+
+
+
+
+
+
+ a description of a relationship to another resource.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The reference format. Recognized values include “bibcode”,
+ referring to a standard astronomical bibcode
+ (http://cdsweb.u-strasbg.fr/simbad/refcode.html).
+
+
+
+
+
+
+
+
+
+
+ A description of the relationship between one resource and one or
+ more other resources.
+
+
+
+
+
+
+
+ the named type of relationship
+
+
+ The value of relationshipType should be taken from the
+ vocabulary at
+ http://www.ivoa.net/rdf/voresource/relationship_type.
+
+
+
+
+
+
+
+ the name of resource that this resource is related to.
+
+
+
+
+
+
+
+
+
+
+
+ A named group of one or more persons brought together to pursue
+ participation in VO applications.
+
+
+ According to the Resource Metadata Recommendation, organisations
+ “can be hierarchical and range in size and scope. At a high level,
+ an organisation could be a university, observatory, or government
+ agency. At a finer level, it could be a specific scientific
+ project, mission, or individual researcher.”
+
+
+ The main purpose of an organisation as a registered resource is
+ to serve as a publisher of other resources.
+
+
+
+
+
+
+
+
+
+ Subject
+
+
+ the observatory or facility used to collect the data
+ contained or managed by this resource.
+
+
+
+
+
+
+
+ Subject
+ Subject.Instrument
+
+
+ the Instrument used to collect the data contain or
+ managed by a resource.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ a resource that can be invoked by a client to perform some action
+ on its behalf.
+
+
+
+
+
+
+
+
+
+ Rights
+
+
+ Information about rights held in and over the resource.
+
+
+ Mainly for compatibility with DataCite, this element
+ is repeatable. Resource record authors are advised
+ that within the Virtual Observatory clients will
+ typically only display and/or use the rights
+ element occurring first and ignore later elements.
+
+
+
+
+
+
+
+ a description of a general capability of the
+ service and how to use it.
+
+
+ This describes a general function of the
+ service, usually in terms of a standard
+ service protocol (e.g. SIA), but not
+ necessarily so.
+
+
+ A service can have many capabilities
+ associated with it, each reflecting different
+ aspects of the functionality it provides.
+
+
+
+
+
+
+
+
+
+
+
+ A statement of usage conditions. This will typically
+ include a license,
+ which should be given as a full string (e.g., Creative Commons
+ Attribution 3.0 International). Further free-text information,
+ e.g., on how to attribute or on embargo periods is allowed.
+
+
+
+
+
+
+
+ A URI identifier for a license
+
+
+ Where formal licenses are available, this URI can
+ reference the full license text. The IVOA may define
+ standard URIs for a set of recommended
+ licenses, in which case these should be used here.
+
+
+
+
+
+
+
+
+
+
+ a description of what the service does (in terms of
+ context-specific behavior), and how to use it (in terms of
+ an interface)
+
+
+
+
+
+
+
+ A numeric grade describing the quality of the
+ capability description and interface, when applicable,
+ to be used to indicate the confidence an end-user
+ can put in the resource as part of a VO application
+ or research study.
+
+
+ See vr:ValidationLevel for an explanation of the
+ allowed levels.
+
+
+
+
+
+
+
+ A human-readable description of what this capability
+ provides as part of the over-all service
+
+
+ Use of this optional element is especially encouraged when
+ this capability is non-standard and is one of several
+ capabilities listed.
+
+
+
+
+
+
+
+ a description of how to call the service to access
+ this capability
+
+
+ Since the Interface type is abstract, one must describe
+ the interface using a subclass of Interface, denoting
+ it via xsi:type.
+
+
+ Multiple occurences can describe different interfaces to
+ the logically same capability, i.e. data or functionality.
+ That is, the inputs accepted and the output provides should
+ be logically the same. For example, a WebBrowser interface
+ given in addition to a WebService interface would simply
+ provide an interactive, human-targeted interface to the
+ underlying WebService interface.
+
+
+
+
+
+
+
+
+ A URI identifier for a standard service.
+
+
+ This provides a unique way to refer to a service
+ specification standard, such as a Simple Image Access service.
+ The use of an IVOA identifier here implies that a
+ VOResource description of the standard is registered and
+ accessible.
+
+
+
+
+
+
+
+
+ A description of a service interface.
+
+
+ Since this type is abstract, one must use an Interface subclass
+ to describe an actual interface.
+
+
+ Additional interface subtypes (beyond WebService and WebBrowser) are
+ defined in the VODataService schema.
+
+
+
+
+
+
+
+ The URL (or base URL) that a client uses to access the
+ service. How this URL is to be interpreted and used
+ depends on the specific Interface subclass
+
+
+ Although the schema allows multiple occurrences of
+ accessURL, multiple accessURLs are deprecated. Each
+ interface should have exactly one access URL. Where an
+ interface has several mirrors, the accessURL should
+ reflect the “primary” (fastest, best-connected,
+ best-maintained) site, the one that non-sophisticated
+ clients will go to.
+
+ Additional accessURLs should be put into mirrorURLs.
+ Advanced clients can retrieve the mirrorURLs and
+ empirically determine interfaces closer to their
+ network location.
+
+
+
+
+
+
+
+ A (base) URL of a mirror of this interface. As with
+ accessURL, how this URL is to be interpreted and used
+ depends on the specific Interface subclass
+
+
+ This is intended exclusively for true mirrors, i.e.,
+ interfaces that are functionally identical to the
+ original interface and that are operated by the same
+ publisher. Other arrangements should be represented as
+ separate services linked by mirror-of relationships.
+
+
+
+
+
+
+
+ The mechanism the client must employ to authenticate
+ to the service.
+
+
+ Services not requiring authentication must provide
+ at least one interface definition without a
+ securityMethod defined.
+
+
+
+
+
+
+
+ Test data for exercising the service.
+
+
+ This contains data that can be passed to the interface to
+ retrieve a non-empty result. This can be used by validators
+ within test suites.
+
+ Exactly how agents should use the data contained in
+ the testQueryString depends on the concrete interface class.
+ For interfaces employing the HTTP GET method, however,
+ this will typically be urlencoded parameters (as for
+ the application/x-www-form-urlencoded media type).
+
+
+
+
+
+
+
+
+ The version of a standard interface specification that this
+ interface complies with. Most VO standards indicate the
+ version in the standardID attribute of the capability. For
+ these standards, the version attribute should not be used.
+
+
+
+
+
+
+
+ A tag name that identifies the role the interface plays
+ in the particular capability. If the value is equal to
+ "std" or begins with "std:", then the interface refers
+ to a standard interface defined by the standard
+ referred to by the capability's standardID attribute.
+
+
+ For an interface complying with some registered
+ standard (i.e. has a legal standardID), the role can be
+ matched against interface roles enumerated in standard
+ resource record. The interface descriptions in
+ the standard record can provide default descriptions
+ so that such details need not be repeated here.
+
+
+
+
+
+
+
+
+
+
+
+ A flag indicating whether this should be interpreted as a base
+ URL, a full URL, or a URL to a directory that will produce a
+ listing of files.
+
+
+ The default value assumed when one is not given depends on the
+ context.
+
+
+
+
+
+
+
+ Assume a full URL--that is, one that can be invoked
+ directly without alteration. This usually returns a
+ single document or file.
+
+
+
+
+
+
+ Assume a base URL--that is, one requiring an extra portion
+ to be appended before being invoked.
+
+
+
+
+
+
+ Assume URL points to a directory that will return a listing
+ of files.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ A URL of a mirror (i.e., a functionally identical additional
+ service interface) to
+
+
+
+
+
+
+
+ A terse, human-readable phrase indicating the function
+ or location of this mirror, e.g., “Primary Backup” or
+ “European Mirror”.
+
+
+
+
+
+
+
+
+
+
+ a description of a security mechanism.
+
+
+ This type only allows one to refer to the mechanism via a
+ URI. Derived types would allow for more metadata.
+
+
+
+
+
+
+
+
+ A URI identifier for a standard security mechanism.
+
+
+ This provides a unique way to refer to a security
+ specification standard. The use of an IVOA identifier here
+ implies that a VOResource description of the standard is
+ registered and accessible.
+
+
+
+
+
+
+
+
+
+ A (form-based) interface intended to be accesed interactively
+ by a user via a web browser.
+
+
+ The accessURL represents the URL of the web form itself.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ A Web Service that is describable by a WSDL document.
+
+
+ The accessURL element gives the Web Service's endpoint URL.
+
+
+
+
+
+
+
+
+
+ The location of the WSDL that describes this
+ Web Service. If not provided, the location is
+ assumed to be the accessURL with "?wsdl" appended.
+
+
+ Multiple occurrences should represent mirror copies of
+ the same WSDL file.
+
+
+
+
+
+
+
+
+
diff --git a/tests/voresource/voresource_models_test.py b/tests/voresource/voresource_models_test.py
new file mode 100644
index 0000000..387bcd1
--- /dev/null
+++ b/tests/voresource/voresource_models_test.py
@@ -0,0 +1,799 @@
+"""Tests for VOResource models."""
+
+from datetime import timezone as tz
+from unittest import TestCase
+from xml.etree.ElementTree import canonicalize
+
+from pydantic.networks import AnyUrl
+
+from vo_models.voresource.models import (
+ AccessURL,
+ Capability,
+ Contact,
+ Content,
+ Creator,
+ Curation,
+ Date,
+ Interface,
+ MirrorURL,
+ Organisation,
+ Relationship,
+ Resource,
+ ResourceName,
+ Rights,
+ SecurityMethod,
+ Service,
+ Source,
+ Validation,
+ WebService,
+)
+from vo_models.voresource.types import UTCDateTime, UTCTimestamp, ValidationLevel
+
+VORESOURCE_NAMESPACE_HEADER = """
+ xmlns:xml="http://www.w3.org/XML/1998/namespace",
+ xmlns="http://www.w3.org/2001/XMLSchema",
+ xmlns:xs="http://www.w3.org/2001/XMLSchema",
+ xmlns="http://www.ivoa.net/xml/VOResource/v1.0",
+ xmlns:vm="http://www.ivoa.net/xml/VOMetadata/v0.1",
+"""
+
+
+class TestValidation(TestCase):
+ """Test VOResource Validation model."""
+
+ test_validation_model = Validation(value=0, validated_by="https://example.edu")
+ test_validation_xml = (
+ '0'
+ )
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ validation = Validation.from_xml(self.test_validation_xml)
+ self.assertEqual(validation.value, ValidationLevel(0))
+ self.assertEqual(validation.validated_by, AnyUrl("https://example.edu"))
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_validation_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_validation_xml, strip_text=True),
+ )
+
+
+class TestResourceName(TestCase):
+ """Test VOResource ResourceName model."""
+
+ test_resource_name_model = ResourceName(value="Example Resource", ivo_id="ivo://example.edu/resource")
+ test_resource_name_xml = 'Example Resource'
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ resource_name = ResourceName.from_xml(self.test_resource_name_xml)
+ self.assertEqual(resource_name.value, "Example Resource")
+ self.assertEqual(resource_name.ivo_id, "ivo://example.edu/resource")
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_resource_name_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(self.test_resource_name_xml, strip_text=True),
+ canonicalize(test_xml, strip_text=True),
+ )
+
+
+class TestDate(TestCase):
+ """Test VOResource Date model"""
+
+ test_date_model = Date(value="2021-01-01T00:00:00Z", role="update")
+ test_date_xml = (
+ '2021-01-01T00:00:00.000Z'
+ )
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ date = Date.from_xml(self.test_date_xml)
+ self.assertEqual(date.value.isoformat(), "2021-01-01T00:00:00.000Z")
+ self.assertEqual(date.role, "update")
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_date_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(self.test_date_xml, strip_text=True),
+ canonicalize(test_xml, strip_text=True),
+ )
+
+
+class TestSource(TestCase):
+ """Test VOResource Source model"""
+
+ test_source_model = Source(value="https://example.edu", format="bibcode")
+ test_source_xml = (
+ ''
+ )
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ source = Source.from_xml(self.test_source_xml)
+ self.assertEqual(source.value, AnyUrl("https://example.edu"))
+ self.assertEqual(source.format, "bibcode")
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_source_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_source_xml, strip_text=True),
+ )
+
+
+class TestRights(TestCase):
+ """Test VOResource Rights model"""
+
+ test_rights_model = Rights(value="CC BY 4.0", rights_uri="https://creativecommons.org/licenses/by/4.0/")
+ test_rights_xml = 'CC BY 4.0'
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ rights = Rights.from_xml(self.test_rights_xml)
+ self.assertEqual(rights.value, "CC BY 4.0")
+ self.assertEqual(rights.rights_uri, AnyUrl("https://creativecommons.org/licenses/by/4.0/"))
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_rights_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_rights_xml, strip_text=True),
+ )
+
+
+class TestAccessURL(TestCase):
+ """Test VOResource AccessURL model"""
+
+ test_access_url_model = AccessURL(value="https://example.edu", use="full")
+ test_access_url_xml = (
+ 'https://example.edu/'
+ )
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ access_url = AccessURL.from_xml(self.test_access_url_xml)
+ self.assertEqual(access_url.value, AnyUrl("https://example.edu"))
+ self.assertEqual(access_url.use, "full")
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_access_url_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_access_url_xml, strip_text=True),
+ )
+
+
+class TestMirrorURL(TestCase):
+ """Test VOResource MirrorURL model"""
+
+ test_mirror_url_model = MirrorURL(value="https://example.edu", title="Mirror")
+ test_mirror_url_xml = (
+ 'https://example.edu/'
+ )
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ mirror_url = MirrorURL.from_xml(self.test_mirror_url_xml)
+ self.assertEqual(mirror_url.value, AnyUrl("https://example.edu"))
+ self.assertEqual(mirror_url.title, "Mirror")
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_mirror_url_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_mirror_url_xml, strip_text=True),
+ )
+
+
+class TestContact(TestCase):
+ """Test VOResource Contact model"""
+
+ test_contact_model = Contact(
+ name=ResourceName(value="John Doe"),
+ address="1234 Example St.",
+ email="jdoe@mail.com",
+ telephone="555-555-5555",
+ alt_identifier=["http://orcid.org/0000-0001-9718-6515"],
+ )
+ test_contact_xml = (
+ ''
+ "John Doe"
+ "1234 Example St."
+ "jdoe@mail.com"
+ "555-555-5555"
+ "http://orcid.org/0000-0001-9718-6515"
+ ""
+ )
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ contact = Contact.from_xml(self.test_contact_xml)
+ self.assertEqual(contact.name.value, "John Doe")
+ self.assertEqual(contact.address, "1234 Example St.")
+ self.assertEqual(contact.email, "jdoe@mail.com")
+ self.assertEqual(contact.telephone, "555-555-5555")
+ self.assertEqual(contact.alt_identifier, [AnyUrl("http://orcid.org/0000-0001-9718-6515")])
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_contact_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_contact_xml, strip_text=True),
+ )
+
+
+class TestCreator(TestCase):
+ """Test VOResource Creator model"""
+
+ test_creator_model = Creator(name=ResourceName(value="Doe, J."), logo="https://example.edu/logo.png")
+ test_creator_xml = (
+ ''
+ "Doe, J."
+ "https://example.edu/logo.png"
+ ""
+ )
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ creator = Creator.from_xml(self.test_creator_xml)
+ self.assertEqual(creator.name.value, "Doe, J.")
+ self.assertEqual(creator.logo, AnyUrl("https://example.edu/logo.png"))
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_creator_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_creator_xml, strip_text=True),
+ )
+
+
+class TestRelationship(TestCase):
+ """Test VOResource Relationship model"""
+
+ test_relationship_model = Relationship(
+ relationship_type="isPartOf",
+ related_resource=[ResourceName(value="Example Resource", ivo_id="ivo://example.edu/resource")],
+ )
+ test_relationship_xml = (
+ ''
+ "isPartOf"
+ 'Example Resource'
+ ""
+ )
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ relationship = Relationship.from_xml(self.test_relationship_xml)
+ self.assertEqual(relationship.relationship_type, "isPartOf")
+ self.assertEqual(relationship.related_resource[0].value, "Example Resource")
+ self.assertEqual(relationship.related_resource[0].ivo_id, "ivo://example.edu/resource")
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_relationship_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_relationship_xml, strip_text=True),
+ )
+
+
+class TestSecurityMethod(TestCase):
+ """Test VOResource SecurityMethod model"""
+
+ test_security_method_model = SecurityMethod(standard_id="ivo://ivoa.net/std/Security#basic")
+ test_security_method_xml = ''
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ security_method = SecurityMethod.from_xml(self.test_security_method_xml)
+ self.assertEqual(security_method.standard_id, AnyUrl("ivo://ivoa.net/std/Security#basic"))
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_security_method_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_security_method_xml, strip_text=True),
+ )
+
+
+class TestCuration(TestCase):
+ """Test VOResource Curation model"""
+
+ test_curation_model = Curation(
+ publisher=ResourceName(value="STScI"),
+ creator=[Creator(name=ResourceName(value="Doe, J."))],
+ contributor=[ResourceName(value="Example Resource")],
+ date=[Date(value="2021-01-01T00:00:00Z", role="update")],
+ version="1.0",
+ contact=[Contact(name=ResourceName(value="John Doe"))],
+ )
+
+ test_curation_xml = (
+ ''
+ "STScI"
+ "Doe, J."
+ "Example Resource"
+ '2021-01-01T00:00:00.000Z'
+ "1.0"
+ "John Doe"
+ ""
+ )
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ curation = Curation.from_xml(self.test_curation_xml)
+ self.assertEqual(curation.publisher.value, "STScI")
+ self.assertEqual(curation.creator[0].name.value, "Doe, J.")
+ self.assertEqual(curation.contributor[0].value, "Example Resource")
+ self.assertEqual(curation.date[0].value.isoformat(), "2021-01-01T00:00:00.000Z")
+ self.assertEqual(curation.date[0].role, "update")
+ self.assertEqual(curation.version, "1.0")
+ self.assertEqual(curation.contact[0].name.value, "John Doe")
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_curation_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_curation_xml, strip_text=True),
+ )
+
+
+class TestContent(TestCase):
+ """Test VOResource Content model"""
+
+ test_content_model = Content(
+ subject=["Astronomy"],
+ description="Example description",
+ source=Source(value="https://example.edu", format="bibcode"),
+ reference_url="https://example.edu",
+ type=["Education"],
+ content_level=["General"],
+ relationship=[
+ Relationship(
+ relationship_type="isPartOf",
+ related_resource=[ResourceName(value="Example Resource", ivo_id="ivo://example.edu/resource")],
+ )
+ ],
+ )
+
+ test_content_xml = (
+ ''
+ "Astronomy"
+ "Example description"
+ ''
+ "https://example.edu/"
+ "Education"
+ "General"
+ ""
+ "isPartOf"
+ 'Example Resource'
+ ""
+ ""
+ )
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ content = Content.from_xml(self.test_content_xml)
+ self.assertEqual(content.subject[0], "Astronomy")
+ self.assertEqual(content.description, "Example description")
+ self.assertEqual(content.source.value, AnyUrl("https://example.edu"))
+ self.assertEqual(content.source.format, "bibcode")
+ self.assertEqual(content.reference_url, AnyUrl("https://example.edu"))
+ self.assertEqual(content.type[0], "Education")
+ self.assertEqual(content.content_level[0], "General")
+ self.assertEqual(content.relationship[0].relationship_type, "isPartOf")
+ self.assertEqual(content.relationship[0].related_resource[0].value, "Example Resource")
+ self.assertEqual(content.relationship[0].related_resource[0].ivo_id, "ivo://example.edu/resource")
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_content_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_content_xml, strip_text=True),
+ )
+
+
+class TestInterface(TestCase):
+ """Test the VOResource Interface model."""
+
+ test_interface_model = Interface(
+ version="1.0",
+ role="std",
+ access_url=[AccessURL(value="https://example.edu", use="full")],
+ mirror_url=[MirrorURL(value="https://example.edu", title="Mirror")],
+ security_method=[SecurityMethod(standard_id="ivo://ivoa.net/std/Security#basic")],
+ test_querystring="test",
+ )
+ test_interface_xml = (
+ ''
+ 'https://example.edu/'
+ 'https://example.edu/'
+ ''
+ "test"
+ ""
+ )
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ interface = Interface.from_xml(self.test_interface_xml)
+ self.assertEqual(interface.version, "1.0")
+ self.assertEqual(interface.role, "std")
+ self.assertEqual(interface.access_url[0].value, AnyUrl("https://example.edu"))
+ self.assertEqual(interface.access_url[0].use, "full")
+ self.assertEqual(interface.mirror_url[0].value, AnyUrl("https://example.edu"))
+ self.assertEqual(interface.mirror_url[0].title, "Mirror")
+ self.assertEqual(interface.security_method[0].standard_id, AnyUrl("ivo://ivoa.net/std/Security#basic"))
+ self.assertEqual(interface.test_querystring, "test")
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_interface_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_interface_xml, strip_text=True),
+ )
+
+
+class TestWebService(TestCase):
+ """Test the VOResource WebService model."""
+
+ test_web_service_model = WebService(
+ wsdl_url=["https://example.edu/wsdl/"],
+ access_url=[AccessURL(value="https://example.edu/", use="full")],
+ )
+ test_web_service_xml = (
+ ''
+ 'https://example.edu/'
+ "https://example.edu/wsdl/"
+ ""
+ )
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ web_service = WebService.from_xml(self.test_web_service_xml)
+ self.assertEqual(web_service.wsdl_url[0], AnyUrl("https://example.edu/wsdl/"))
+ self.assertEqual(web_service.access_url[0].value, AnyUrl("https://example.edu/"))
+ self.assertEqual(web_service.access_url[0].use, "full")
+ self.assertEqual(web_service.type, "vr:WebService")
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_web_service_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_web_service_xml, strip_text=True),
+ )
+
+
+class TestResource(TestCase):
+ """Test the VOResource Resource model."""
+
+ test_resource_model = Resource(
+ created=UTCTimestamp(1996, 3, 11, 19, 0, 0, tzinfo=tz.utc),
+ updated=UTCTimestamp(1996, 3, 11, 19, 0, 0, tzinfo=tz.utc),
+ status="active",
+ version="1.0",
+ validation_level=[Validation(value=0, validated_by="https://example.edu")],
+ title="Example Resource",
+ short_name="example",
+ identifier="https://example.edu",
+ alt_identifier=["bibcode:2008ivoa.spec.0222P"],
+ curation=Curation(
+ publisher=ResourceName(value="STScI"),
+ creator=[Creator(name=ResourceName(value="Doe, J."))],
+ contributor=[ResourceName(value="Example Resource")],
+ date=[Date(value="2021-01-01T00:00:00Z", role="update")],
+ version="1.0",
+ contact=[Contact(name=ResourceName(value="John Doe"))],
+ ),
+ content=Content(
+ subject=["Astronomy"],
+ description="Example description",
+ source=Source(value="https://example.edu", format="bibcode"),
+ reference_url="https://example.edu",
+ type=["Education"],
+ content_level=["General"],
+ relationship=[
+ Relationship(
+ relationship_type="isPartOf",
+ related_resource=[ResourceName(value="Example Resource", ivo_id="ivo://example.edu/resource")],
+ )
+ ],
+ ),
+ )
+
+ test_resource_xml = (
+ ''
+ '0'
+ "Example Resource"
+ "example"
+ "https://example.edu/"
+ "bibcode:2008ivoa.spec.0222P"
+ ""
+ "STScI"
+ "Doe, J."
+ "Example Resource"
+ '2021-01-01T00:00:00.000Z'
+ "1.0"
+ "John Doe"
+ ""
+ ""
+ "Astronomy"
+ "Example description"
+ ''
+ "https://example.edu/"
+ "Education"
+ "General"
+ ""
+ "isPartOf"
+ 'Example Resource'
+ ""
+ ""
+ ""
+ )
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ resource = Resource.from_xml(self.test_resource_xml)
+ self.assertEqual(resource.created.isoformat(), UTCDateTime("1996-03-11T19:00:00.000Z"))
+ self.assertEqual(resource.updated.isoformat(), UTCDateTime("1996-03-11T19:00:00.000Z"))
+ self.assertEqual(resource.status, "active")
+ self.assertEqual(resource.version, "1.0")
+ self.assertEqual(resource.validation_level[0].value, ValidationLevel(0))
+ self.assertEqual(resource.validation_level[0].validated_by, AnyUrl("https://example.edu"))
+ self.assertEqual(resource.title, "Example Resource")
+ self.assertEqual(resource.short_name, "example")
+ self.assertEqual(resource.identifier, AnyUrl("https://example.edu"))
+ self.assertEqual(resource.alt_identifier, [AnyUrl("bibcode:2008ivoa.spec.0222P")])
+ self.assertEqual(resource.curation.publisher.value, "STScI")
+ self.assertEqual(resource.curation.creator[0].name.value, "Doe, J.")
+ self.assertEqual(resource.curation.contributor[0].value, "Example Resource")
+ self.assertEqual(resource.curation.date[0].value.isoformat(), "2021-01-01T00:00:00.000Z")
+ self.assertEqual(resource.curation.date[0].role, "update")
+ self.assertEqual(resource.curation.version, "1.0")
+ self.assertEqual(resource.curation.contact[0].name.value, "John Doe")
+ self.assertEqual(resource.content.subject, ["Astronomy"])
+ self.assertEqual(resource.content.description, "Example description")
+ self.assertEqual(resource.content.source.value, AnyUrl("https://example.edu"))
+ self.assertEqual(resource.content.source.format, "bibcode")
+ self.assertEqual(resource.content.reference_url, AnyUrl("https://example.edu"))
+ self.assertEqual(resource.content.type, ["Education"])
+ self.assertEqual(resource.content.content_level, ["General"])
+ self.assertEqual(resource.content.relationship[0].relationship_type, "isPartOf")
+ self.assertEqual(resource.content.relationship[0].related_resource[0].value, "Example Resource")
+ self.assertEqual(resource.content.relationship[0].related_resource[0].ivo_id, "ivo://example.edu/resource")
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_resource_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_resource_xml, strip_text=True),
+ )
+
+
+class TestOrganization(TestCase):
+ """Test the VOResource Organization model."""
+
+ test_organization_model = Organisation(
+ title="Example Organization",
+ identifier="https://example.edu",
+ curation=Curation(
+ publisher=ResourceName(value="Example Publisher"), contact=[Contact(name=ResourceName(value="John Doe"))]
+ ),
+ content=Content(
+ subject=["Astronomy"],
+ description="Example description",
+ reference_url="https://example.edu",
+ ),
+ created=UTCDateTime("1996-03-11T19:00:00Z"),
+ updated=UTCDateTime("1996-03-11T19:00:00Z"),
+ status="active",
+ version="1.0",
+ facility=[ResourceName(value="Example Facility", ivo_id="ivo://example.edu/facility")],
+ instrument=[ResourceName(value="Example Instrument", ivo_id="ivo://example.edu/instrument")],
+ )
+
+ test_organization_xml = (
+ ''
+ "Example Organization"
+ "https://example.edu/"
+ ""
+ "Example Publisher"
+ "John Doe"
+ ""
+ ""
+ "Astronomy"
+ "Example description"
+ "https://example.edu/"
+ ""
+ 'Example Facility'
+ 'Example Instrument'
+ ""
+ )
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ organization = Organisation.from_xml(self.test_organization_xml)
+ self.assertEqual(organization.title, "Example Organization")
+ self.assertEqual(organization.identifier, AnyUrl("https://example.edu"))
+ self.assertEqual(organization.curation.publisher.value, "Example Publisher")
+ self.assertEqual(organization.curation.contact[0].name.value, "John Doe")
+ self.assertEqual(organization.content.subject, ["Astronomy"])
+ self.assertEqual(organization.content.description, "Example description")
+ self.assertEqual(organization.content.reference_url, AnyUrl("https://example.edu"))
+ self.assertEqual(organization.created.isoformat(), UTCDateTime("1996-03-11T19:00:00.000Z"))
+ self.assertEqual(organization.updated.isoformat(), UTCDateTime("1996-03-11T19:00:00.000Z"))
+ self.assertEqual(organization.status, "active")
+ self.assertEqual(organization.version, "1.0")
+ self.assertEqual(organization.facility[0].value, "Example Facility")
+ self.assertEqual(organization.facility[0].ivo_id, "ivo://example.edu/facility")
+ self.assertEqual(organization.instrument[0].value, "Example Instrument")
+ self.assertEqual(organization.instrument[0].ivo_id, "ivo://example.edu/instrument")
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_organization_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_organization_xml, strip_text=True),
+ )
+
+
+class TestCapability(TestCase):
+ """Test the VOResource Capability model."""
+
+ test_capability_model = Capability(
+ standard_id="ivo://ivoa.net/std/TAP",
+ validation_level=[Validation(value=0, validated_by="https://example.edu")],
+ description="Example description",
+ interface=[
+ Interface(version="1.0", role="std", access_url=[AccessURL(value="https://example.edu", use="full")])
+ ],
+ )
+
+ test_capability_xml = (
+ ''
+ '0'
+ "Example description"
+ ''
+ 'https://example.edu/'
+ ""
+ ""
+ )
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ capability = Capability.from_xml(self.test_capability_xml)
+ self.assertEqual(capability.standard_id, AnyUrl("ivo://ivoa.net/std/TAP"))
+ self.assertEqual(capability.validation_level[0].value, ValidationLevel(0))
+ self.assertEqual(capability.validation_level[0].validated_by, AnyUrl("https://example.edu"))
+ self.assertEqual(capability.description, "Example description")
+ self.assertEqual(capability.interface[0].version, "1.0")
+ self.assertEqual(capability.interface[0].role, "std")
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_capability_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_capability_xml, strip_text=True),
+ )
+
+
+class TestService(TestCase):
+ """Test the VOResource Service model."""
+
+ test_service_model = Service(
+ created=UTCTimestamp(1996, 3, 11, 19, 0, 0, tzinfo=tz.utc),
+ updated=UTCTimestamp(1996, 3, 11, 19, 0, 0, tzinfo=tz.utc),
+ status="active",
+ title="Example Service",
+ identifier="https://example.edu",
+ curation=Curation(
+ publisher=ResourceName(value="STScI"),
+ creator=[Creator(name=ResourceName(value="Doe, J."))],
+ contributor=[ResourceName(value="Example Resource")],
+ date=[Date(value="2021-01-01T00:00:00Z", role="update")],
+ version="1.0",
+ contact=[Contact(name=ResourceName(value="John Doe"))],
+ ),
+ content=Content(
+ subject=["Astronomy"],
+ description="Example description",
+ source=Source(value="https://example.edu", format="bibcode"),
+ reference_url="https://example.edu",
+ type=["Education"],
+ content_level=["General"],
+ relationship=[
+ Relationship(
+ relationship_type="isPartOf",
+ related_resource=[ResourceName(value="Example Resource", ivo_id="ivo://example.edu/resource")],
+ )
+ ],
+ ),
+ rights=[Rights(value="CC BY 4.0", rights_uri="https://creativecommons.org/licenses/by/4.0/")],
+ capability=[Capability(standard_id="ivo://ivoa.net/std/TAP")],
+ )
+
+ test_service_xml = (
+ ''
+ "Example Service"
+ "https://example.edu/"
+ ""
+ "STScI"
+ "Doe, J."
+ "Example Resource"
+ '2021-01-01T00:00:00.000Z'
+ "1.0"
+ "John Doe"
+ ""
+ ""
+ "Astronomy"
+ "Example description"
+ ''
+ "https://example.edu/"
+ "Education"
+ "General"
+ ""
+ "isPartOf"
+ 'Example Resource'
+ ""
+ ""
+ "CC BY 4.0"
+ ""
+ ""
+ )
+
+ def test_read_from_xml(self):
+ """Test reading from XML."""
+ service = Service.from_xml(self.test_service_xml)
+ self.assertEqual(service.created.isoformat(), "1996-03-11T19:00:00.000Z")
+ self.assertEqual(service.updated.isoformat(), "1996-03-11T19:00:00.000Z")
+ self.assertEqual(service.status, "active")
+ self.assertEqual(service.capability[0].standard_id, AnyUrl("ivo://ivoa.net/std/TAP"))
+ self.assertEqual(service.title, "Example Service")
+ self.assertEqual(service.identifier, AnyUrl("https://example.edu"))
+ self.assertEqual(service.curation.publisher.value, "STScI")
+ self.assertEqual(service.curation.creator[0].name.value, "Doe, J.")
+ self.assertEqual(service.curation.contributor[0].value, "Example Resource")
+ self.assertEqual(service.curation.date[0].value.isoformat(), "2021-01-01T00:00:00.000Z")
+ self.assertEqual(service.curation.date[0].role, "update")
+ self.assertEqual(service.curation.version, "1.0")
+ self.assertEqual(service.curation.contact[0].name.value, "John Doe")
+ self.assertEqual(service.content.subject, ["Astronomy"])
+ self.assertEqual(service.content.description, "Example description")
+ self.assertEqual(service.content.source.value, AnyUrl("https://example.edu"))
+ self.assertEqual(service.content.source.format, "bibcode")
+ self.assertEqual(service.content.reference_url, AnyUrl("https://example.edu"))
+ self.assertEqual(service.content.type, ["Education"])
+ self.assertEqual(service.content.content_level, ["General"])
+ self.assertEqual(service.content.relationship[0].relationship_type, "isPartOf")
+ self.assertEqual(service.content.relationship[0].related_resource[0].value, "Example Resource")
+ self.assertEqual(service.content.relationship[0].related_resource[0].ivo_id, "ivo://example.edu/resource")
+
+ def test_write_to_xml(self):
+ """Test writing to XML."""
+ test_xml = self.test_service_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_service_xml, strip_text=True),
+ )
diff --git a/tests/vosi/VOSICapabilities-v1.0.xsd b/tests/vosi/VOSICapabilities-v1.0.xsd
new file mode 100644
index 0000000..52862c2
--- /dev/null
+++ b/tests/vosi/VOSICapabilities-v1.0.xsd
@@ -0,0 +1,54 @@
+
+
+
+
+
+ A schema for formatting service capabilities as returned by a
+ capabilities resource, defined by the IVOA Support Interfaces
+ specification (VOSI).
+ See http://www.ivoa.net/Documents/latest/VOSI.html.
+
+
+
+
+
+
+
+
+
+ A listing of capabilities supported by a service
+
+
+
+
+
+
+
+
+
+ A capability supported by the service.
+
+
+ A protocol-specific capability is included by specifying a
+ vr:Capability sub-type via an xsi:type attribute on this
+ element.
+
+
+
+
+
+
+
+
+
diff --git a/tests/vosi/capabilities_test.py b/tests/vosi/capabilities_test.py
new file mode 100644
index 0000000..6ae36e7
--- /dev/null
+++ b/tests/vosi/capabilities_test.py
@@ -0,0 +1,245 @@
+"""Tests for VOSI Capabilities models."""
+
+from unittest import TestCase
+from xml.etree.ElementTree import canonicalize
+
+from vo_models.tapregext.models import (
+ DataLimit,
+ DataLimits,
+ Language,
+ LanguageFeature,
+ LanguageFeatureList,
+ OutputFormat,
+ TableAccess,
+ Version,
+)
+from vo_models.vodataservice.models import ParamHTTP
+from vo_models.voresource.models import AccessURL, Capability, Interface, WebBrowser
+from vo_models.vosi.capabilities.models import VOSICapabilities
+
+CAPABILITIES_HEADER = """xmlns:vosi="http://www.ivoa.net/xml/VOSICapabilities/v1.0"
+xmlns:vs="http://www.ivoa.net/xml/VODataService/v1.0"
+xmlns:vr="http://www.ivoa.net/xml/VOResource/v1.0"
+xmlns:tr="http://www.ivoa.net/xml/TAPRegExt/v1.0"
+xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+"""
+
+
+class TestVOSICapabilities(TestCase):
+ """Tests the VOSI Capabilities complex type.
+
+ This test uses a simulated capabilities document for a TAP service.
+ """
+
+ test_capabilities_xml = f"""
+
+
+ https://someservice.edu/tap
+
+
+ ADQL
+ 2.0
+
+ ADQL-2.0. Positional queries using CONTAINS with POINT and CIRCLE are supported.
+
+
+
+
+
+
+
+
+
+
+
+ application/x-votable+xml
+ votable
+
+
+ text/csv;header=present
+ csv
+
+
+ 100000
+ 100000
+
+
+
+
+
+ https://someservice.edu/tap/capabilities
+
+
+
+
+
+
+ https://someservice.edu/tap/availability
+
+
+
+
+
+
+ https://someservice.edu/tap/tables
+
+
+
+
+
+
+ https://someservice.edu/tap/examples
+
+
+
+
+ """
+
+ test_tap_capabilities = TableAccess(
+ type="tr:TableAccess",
+ interface=[
+ ParamHTTP(
+ role="std",
+ version="1.1",
+ access_url=[AccessURL(use="full", value="https://someservice.edu/tap")],
+ )
+ ],
+ language=[
+ Language(
+ name="ADQL",
+ version=[Version(value="2.0", ivo_id="ivo://ivoa.net/std/ADQL#v2.0")],
+ description="ADQL-2.0. Positional queries using CONTAINS with POINT and CIRCLE are supported.",
+ language_features=[
+ LanguageFeatureList(
+ type="ivo://ivoa.net/std/TAPRegExt#features-adql-geo",
+ feature=[
+ LanguageFeature(form="POINT"),
+ LanguageFeature(form="CIRCLE"),
+ ],
+ ),
+ ],
+ )
+ ],
+ output_format=[
+ OutputFormat(
+ mime="application/x-votable+xml",
+ alias=["votable"],
+ ivo_id="ivo://ivoa.net/std/TAPRegExt#output-votable-td",
+ ),
+ OutputFormat(
+ mime="text/csv;header=present",
+ alias=["csv"],
+ ),
+ ],
+ output_limit=DataLimits(
+ default=DataLimit(unit="row", value=100000),
+ hard=DataLimit(unit="row", value=100000),
+ ),
+ )
+
+ test_vosi_capabilities = Capability(
+ standard_id="ivo://ivoa.net/std/VOSI#capabilities",
+ interface=[
+ ParamHTTP(
+ role="std",
+ access_url=[AccessURL(use="full", value="https://someservice.edu/tap/capabilities")],
+ )
+ ],
+ )
+ test_vosi_availability = Capability(
+ standard_id="ivo://ivoa.net/std/VOSI#availability",
+ interface=[
+ ParamHTTP(
+ role="std",
+ access_url=[AccessURL(use="full", value="https://someservice.edu/tap/availability")],
+ )
+ ],
+ )
+ test_vosi_tables = Capability(
+ standard_id="ivo://ivoa.net/std/VOSI#tables",
+ interface=[
+ ParamHTTP(
+ role="std",
+ version="1.1",
+ access_url=[AccessURL(use="full", value="https://someservice.edu/tap/tables")],
+ )
+ ],
+ )
+ test_dali_examples = Capability(
+ standard_id="ivo://ivoa.net/std/DALI#examples",
+ interface=[
+ WebBrowser(
+ access_url=[AccessURL(use="full", value="https://someservice.edu/tap/examples")],
+ )
+ ],
+ )
+ test_vosi_capabilities_model = VOSICapabilities(
+ capability=[
+ test_tap_capabilities,
+ test_vosi_capabilities,
+ test_vosi_availability,
+ test_vosi_tables,
+ test_dali_examples,
+ ]
+ )
+
+ def _get_capability(self, capabilities: VOSICapabilities, standard_id: str) -> Capability:
+ """Get a capability from the test capabilities."""
+ for cap in capabilities.capability:
+ if str(cap.standard_id) == standard_id:
+ return cap
+ return None
+
+ def test_read_from_xml(self):
+ """Test reading VOSI Capabilities from XML."""
+ capabilities = VOSICapabilities.from_xml(self.test_capabilities_xml)
+
+ self.assertEqual(len(capabilities.capability), 5)
+
+ # Check the TAP capability
+ tap_capability: TableAccess = self._get_capability(capabilities, "ivo://ivoa.net/std/TAP")
+ self.assertIsNotNone(tap_capability)
+ self.assertEqual(len(tap_capability.interface), 1)
+ self.assertIsInstance(tap_capability.interface[0], Interface)
+ self.assertEqual(tap_capability.interface[0].type, "vs:ParamHTTP")
+ self.assertIsNotNone(tap_capability.output_limit)
+ self.assertEqual(tap_capability.output_limit.default.value, 100000)
+ self.assertEqual(len(tap_capability.language), 1)
+ self.assertEqual(len(tap_capability.language[0].language_features), 1)
+ self.assertEqual(len(tap_capability.output_format), 2)
+
+ # Check the VOSI capabilities
+ vosi_capabilities = self._get_capability(capabilities, "ivo://ivoa.net/std/VOSI#capabilities")
+ self.assertIsNotNone(vosi_capabilities)
+ self.assertEqual(len(vosi_capabilities.interface), 1)
+ self.assertIsInstance(vosi_capabilities.interface[0], Interface)
+ self.assertEqual(vosi_capabilities.interface[0].type, "vs:ParamHTTP")
+
+ # Check the VOSI availability
+ vosi_availability = self._get_capability(capabilities, "ivo://ivoa.net/std/VOSI#availability")
+ self.assertIsNotNone(vosi_availability)
+ self.assertEqual(len(vosi_availability.interface), 1)
+ self.assertIsInstance(vosi_availability.interface[0], Interface)
+ self.assertEqual(vosi_availability.interface[0].type, "vs:ParamHTTP")
+
+ # Check the VOSI tables
+ vosi_tables = self._get_capability(capabilities, "ivo://ivoa.net/std/VOSI#tables")
+ self.assertIsNotNone(vosi_tables)
+ self.assertEqual(len(vosi_tables.interface), 1)
+ self.assertIsInstance(vosi_tables.interface[0], Interface)
+ self.assertEqual(vosi_tables.interface[0].type, "vs:ParamHTTP")
+
+ # Check the DALI examples
+ dali_examples = self._get_capability(capabilities, "ivo://ivoa.net/std/DALI#examples")
+ self.assertIsNotNone(dali_examples)
+ self.assertEqual(len(dali_examples.interface), 1)
+ self.assertIsInstance(dali_examples.interface[0], Interface)
+ self.assertEqual(dali_examples.interface[0].type, "vr:WebBrowser")
+
+ def test_write_to_xml(self):
+ """Test writing VOSI Capabilities to XML."""
+ test_xml = self.test_vosi_capabilities_model.to_xml(encoding=str, skip_empty=True)
+ self.assertEqual(
+ canonicalize(test_xml, strip_text=True),
+ canonicalize(self.test_capabilities_xml, strip_text=True),
+ )
diff --git a/vo_models/stc/__init__.py b/vo_models/stc/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/vo_models/stc/models.py b/vo_models/stc/models.py
new file mode 100644
index 0000000..0fbd79e
--- /dev/null
+++ b/vo_models/stc/models.py
@@ -0,0 +1,12 @@
+"""STC models. TODO: NOT IMPLEMENTED"""
+from pydantic_xml import BaseXmlModel
+
+# pylint: disable=too-few-public-methods
+
+
+class STCResourceProfile(BaseXmlModel):
+ """NOT IMPLEMENTED"""
+
+
+class STCDescriptionType(BaseXmlModel):
+ """NOT IMPLEMENTED"""
diff --git a/vo_models/tapregext/__init__.py b/vo_models/tapregext/__init__.py
new file mode 100644
index 0000000..b9a301b
--- /dev/null
+++ b/vo_models/tapregext/__init__.py
@@ -0,0 +1,19 @@
+"""
+Module containing VO TapRegExt classes.
+
+IVOA UWS Spec: https://ivoa.net/documents/TAPRegExt/20120827/REC-TAPRegExt-1.0.html
+"""
+
+from vo_models.tapregext.models import (
+ DataLimit,
+ DataLimits,
+ DataModelType,
+ Language,
+ LanguageFeature,
+ LanguageFeatureList,
+ OutputFormat,
+ TableAccess,
+ TimeLimits,
+ UploadMethod,
+ Version,
+)
diff --git a/vo_models/tapregext/models.py b/vo_models/tapregext/models.py
new file mode 100644
index 0000000..2b33f1b
--- /dev/null
+++ b/vo_models/tapregext/models.py
@@ -0,0 +1,196 @@
+"""TAPRegExt classes."""
+
+from typing import Literal, Optional
+
+from pydantic_xml import BaseXmlModel, attr, element
+
+from vo_models.voresource.models import NSMAP as VORESOURCE_NSMAP
+from vo_models.voresource.models import Capability
+
+NSMAP = {
+ "xs": "http://www.w3.org/2001/XMLSchema",
+ "vm": "http://www.ivoa.net/xml/VOMetadata/v0.1",
+ "tr": "http://www.ivoa.net/xml/TAPRegExt/v1.0",
+ "xsi": "http://www.w3.org/2001/XMLSchema-instance",
+} | VORESOURCE_NSMAP
+
+
+class DataModelType(BaseXmlModel, nsmap=NSMAP):
+ """IVOA defined data model, identified by an IVORN.
+
+ Parameters:
+ value:
+ (content) - The human-readable name of the data model.
+ ivo_id:
+ (attribute) - The IVORN of the data model.
+ """
+
+ value: str
+ ivo_id: str = attr(name="ivo-id")
+
+
+class Version(BaseXmlModel, nsmap=NSMAP):
+ """One version of the language supported by the service.
+
+ Parameters:
+ value:
+ (content) - The version of the language.
+ ivo_id:
+ (attribute) - An optional IVORN of the language.
+ """
+
+ value: str
+ ivo_id: Optional[str] = attr(name="ivo-id", default=None)
+
+
+class LanguageFeature(BaseXmlModel, nsmap=NSMAP):
+ """A non-standard or non-mandatory feature implemented by the language.
+
+ Parameters:
+ form:
+ (element) - Formal notation for the language feature.
+ description:
+ (element) - Human-readable freeform documentation for the language feature.
+ """
+
+ form: str = element(tag="form")
+ description: Optional[str] = element(tag="description", default=None)
+
+
+class OutputFormat(BaseXmlModel, nsmap=NSMAP):
+ """An output format supported by the service.
+
+ Parameters:
+ mime:
+ (element) - The MIME type of this format.
+ alias:
+ (element) - Other values of FORMAT that make the service return documents with this MIME type.
+ ivo_id:
+ (attr) - An optional IVORN of the output format.
+ """
+
+ mime: str = element(tag="mime")
+ alias: Optional[list[str]] = element(tag="alias", default_factory=list)
+ ivo_id: Optional[str] = attr(name="ivo-id", default=None)
+
+
+class UploadMethod(BaseXmlModel, nsmap=NSMAP):
+ """An upload method as defined by IVOA.
+
+ Parameters:
+ ivo_id:
+ (attribute) - The IVORN of the upload method.
+ """
+
+ ivo_id: str = attr(name="ivo-id")
+
+
+class TimeLimits(BaseXmlModel, nsmap=NSMAP):
+ """Time-valued limits, all values given in seconds.
+
+ Parameters:
+ default:
+ (element) - The value of this limit for newly-created jobs, given in seconds.
+ hard:
+ (element) - The value this limit cannot be raised above, given in seconds.
+ """
+
+ default: Optional[int] = element(tag="default", default=None)
+ hard: Optional[int] = element(tag="hard", default=None)
+
+
+class DataLimit(BaseXmlModel, nsmap=NSMAP):
+ """A limit on some data size, either in rows or in bytes.
+
+ Parameters:
+ value:
+ (content) - The value of this limit.
+ unit:
+ (attribute) - The unit of the limit specified.
+ """
+
+ value: int
+ unit: Literal["byte", "row"] = attr(name="unit")
+
+
+class DataLimits(BaseXmlModel, nsmap=NSMAP):
+ """Limits on data sizes, given in rows or bytes.
+
+ Parameters:
+ default:
+ (element) - The value of this limit for newly-created jobs.
+ hard:
+ (element) - The value this limit cannot be raised above.
+ """
+
+ default: Optional[DataLimit] = element(tag="default", default=None)
+ hard: Optional[DataLimit] = element(tag="hard", default=None)
+
+
+class LanguageFeatureList(BaseXmlModel, nsmap=NSMAP):
+ """An enumeration of non-standard or non-mandatory features of a specific type implemented by the language.
+
+ Parameters:
+ feature:
+ (element) - A language feature of the type given by this element's type attribute.
+ type:
+ (attribute) - The type of the language feature.
+ """
+
+ feature: Optional[list[LanguageFeature]] = element(tag="feature", default_factory=list)
+ type: str = attr(name="type")
+
+
+class Language(BaseXmlModel, nsmap=NSMAP):
+ """A query language supported by the service.
+
+ Parameters:
+ name:
+ (element) - The name of the language without a version suffix.
+ version:
+ (element) - A version of the language supported by the server.
+ description:
+ (element) - A short, human-readable description of the query language.
+ language_features:
+ (element) - Optional features of the query language, grouped by feature type.
+ """
+
+ name: str = element(tag="name")
+ version: list[Version] = element(tag="version")
+ description: Optional[str] = element(tag="description", default=None)
+ language_features: Optional[list[LanguageFeatureList]] = element(tag="languageFeatures", default_factory=[])
+
+
+class TableAccess(Capability, tag="capability", nsmap=NSMAP):
+ """The capabilities of a TAP server.
+
+ Parameters:
+ data_model:
+ (element) - Identifier of IVOA-approved data model supported by the service.
+ language:
+ (element) - Language supported by the service.
+ output_format:
+ (element) - Output format supported by the service.
+ upload_method:
+ (element) - Upload method supported by the service.
+ retention_period:
+ (element) - Limits on the time between job creation and destruction time.
+ execution_duration:
+ (element) - Limits on executionDuration.
+ output_limit:
+ (element) - Limits on the size of data returned.
+ upload_limit:
+ (element) - Limits on the size of uploaded data.
+ """
+
+ standard_id: Literal["ivo://ivoa.net/std/TAP"] = attr(name="standardID", default="ivo://ivoa.net/std/TAP")
+ type: Literal["tr:TableAccess"] = attr(default="tr:TableAccess", ns="xsi")
+
+ data_model: Optional[list[DataModelType]] = element(tag="dataModel", default_factory=list)
+ language: list[Language] = element(tag="language")
+ output_format: list[OutputFormat] = element(tag="outputFormat")
+ upload_method: Optional[list[UploadMethod]] = element(tag="uploadMethod", default_factory=list)
+ retention_period: Optional[TimeLimits] = element(tag="retentionPeriod", default=None)
+ execution_duration: Optional[TimeLimits] = element(tag="executionDuration", default=None)
+ output_limit: Optional[DataLimits] = element(tag="outputLimit", default=None)
+ upload_limit: Optional[DataLimits] = element(tag="uploadLimit", default=None)
diff --git a/vo_models/uws/models.py b/vo_models/uws/models.py
index 21e2368..11d741f 100644
--- a/vo_models/uws/models.py
+++ b/vo_models/uws/models.py
@@ -1,10 +1,7 @@
"""UWS Job Schema using Pydantic-XML models"""
from typing import Annotated, Dict, Generic, Optional, TypeAlias, TypeVar
-
-from pydantic import BeforeValidator
-from pydantic import ConfigDict
-
+from pydantic import BeforeValidator, ConfigDict
from pydantic_xml import BaseXmlModel, attr, element
from vo_models.uws.types import ErrorType, ExecutionPhase, UWSVersion
@@ -28,8 +25,9 @@ class Parameter(BaseXmlModel, tag="parameter", ns="uws", nsmap=NSMAP):
Parameters:
value:
(content) - the value of the parameter.
- by_reference: (attr) - If this attribute is true then the content of the parameter represents a URL to retrieve the
- actual parameter value.
+ by_reference:
+ (attr) - If this attribute is true then the content of the parameter represents a URL to retrieve
+ the actual parameter value.
id:
(attr) - The identifier of the parameter.
is_post:
@@ -46,8 +44,7 @@ class Parameter(BaseXmlModel, tag="parameter", ns="uws", nsmap=NSMAP):
MultiValuedParameter: TypeAlias = Annotated[
- list[Parameter],
- BeforeValidator(lambda v: v if isinstance(v, list) else [v])
+ list[Parameter], BeforeValidator(lambda v: v if isinstance(v, list) else [v])
]
"""Type for a multi-valued parameter.
@@ -276,7 +273,7 @@ class JobSummary(BaseXmlModel, Generic[ParametersType], tag="job", ns="uws", nsm
parameters: Optional[ParametersType] = element(tag="parameters", default=None)
results: Optional[Results] = element(tag="results", default=Results())
error_summary: Optional[ErrorSummary] = element(tag="errorSummary", default=None)
- job_info: Optional[list[str]] = element(tag="jobInfo", default=[])
+ job_info: Optional[list[str]] = element(tag="jobInfo", default_factory=list)
version: Optional[UWSVersion] = attr(default=UWSVersion.V1_1)
diff --git a/vo_models/vodataservice/__init__.py b/vo_models/vodataservice/__init__.py
index bdcd20d..fce3be9 100644
--- a/vo_models/vodataservice/__init__.py
+++ b/vo_models/vodataservice/__init__.py
@@ -1,8 +1,11 @@
"""Module containing models and resources for IVOA VODataService objects."""
from vo_models.vodataservice.models import (
+ BaseParam,
DataType,
FKColumn,
ForeignKey,
+ InputParam,
+ ParamHTTP,
Table,
TableParam,
TableSchema,
diff --git a/vo_models/vodataservice/models.py b/vo_models/vodataservice/models.py
index 709e009..0ac37c5 100644
--- a/vo_models/vodataservice/models.py
+++ b/vo_models/vodataservice/models.py
@@ -3,13 +3,14 @@
TODO: This is an incomplete spec, covering only elements needed for VOSITables
https://github.com/spacetelescope/vo-models/issues/17
"""
-from typing import Any, Optional
+from typing import Any, Literal, Optional
from xml.sax.saxutils import escape
from pydantic import field_validator
from pydantic_xml import BaseXmlModel, attr, element
from vo_models.adql.misc import ADQL_SQL_KEYWORDS
+from vo_models.voresource.models import Interface
# pylint: disable=no-self-argument
@@ -128,7 +129,7 @@ class TableParam(BaseXmlModel, ns="", tag="column"):
utype: Optional[str] = element(tag="utype", default=None)
xtype: Optional[str] = element(tag="xtype", default=None)
datatype: Optional[DataType] = element(tag="dataType", default=None)
- flag: Optional[list[str]] = element(tag="flag", default=None)
+ flag: Optional[list[str]] = element(tag="flag", default_factory=list)
def __init__(__pydantic_self__, **data: Any) -> None:
data["datatype"] = __pydantic_self__.__make_datatype_element(data)
@@ -225,8 +226,8 @@ class Table(BaseXmlModel, tag="table", ns="", skip_empty=True):
description: Optional[str] = element(tag="description", ns="", default=None)
utype: Optional[str] = element(tag="utype", ns="", default=None)
nrows: Optional[int] = element(tag="nrows", gte=0, ns="", default=None)
- column: Optional[list[TableParam]] = element(tag="column", ns="", default=None)
- foreign_key: Optional[list[ForeignKey]] = element(tag="foreignKey", ns="", default=None)
+ column: Optional[list[TableParam]] = element(tag="column", ns="", default_factory=list)
+ foreign_key: Optional[list[ForeignKey]] = element(tag="foreignKey", ns="", default_factory=list)
def __init__(__pydantic_self__, **data: Any) -> None:
"""Escape any keys that are passed in."""
@@ -269,7 +270,7 @@ class TableSchema(BaseXmlModel, tag="schema", ns="", skip_empty=True):
title: Optional[str] = element(tag="title", default=None)
description: Optional[str] = element(tag="description", default=None)
utype: Optional[str] = element(tag="utype", default=None)
- table: Optional[list[Table]] = element(tag="table", default=None)
+ table: Optional[list[Table]] = element(tag="table", default_factory=list)
def __init__(__pydantic_self__, **data: Any) -> None:
"""Escape any keys that are passed in."""
@@ -309,3 +310,75 @@ def validate_tableset_schema(cls, value):
if not isinstance(value, list):
value = [value]
return value
+
+
+class BaseParam(BaseXmlModel):
+ """A description of a parameter that places no restriction on the parameter's data type.
+
+ TODO: Set as base for TableParam when implementing VODataservice fully.
+
+ Parameters:
+ name:
+ (elem) - The name of the parameter.
+ description:
+ (elem) - A free-text description of the parameter's contents.
+ unit:
+ (elem) - The unit associated with the values in the parameter.
+ ucd:
+ (elem) - The name of a unified content descriptor that describes the scientific content of the parameter.
+ utype:
+ (elem) - An identifier for a concept in a data model that the data in this parameter represent.
+ """
+
+ description: Optional[str] = element(tag="description", default=None)
+ unit: Optional[str] = element(tag="unit", default=None)
+ ucd: Optional[str] = element(tag="ucd", default=None)
+ utype: Optional[str] = element(tag="utype", default=None)
+
+
+ParamUse = Literal["required", "optional", "ignored"]
+
+
+class InputParam(BaseParam):
+ """A description of a service or function parameter having a fixed data type.
+
+ Parameters:
+ datatype:
+ (elem) - A type of data contained in the parameter.
+ use:
+ (attr) - An indication of whether this parameter is required to be provided for the application or service
+ to work properly.
+ std:
+ (attr) - If true, the meaning and behavior of this parameter is reserved and defined by a
+ standard interface.
+ """
+
+ datatype: Optional[DataType] = element(tag="dataType", default=None)
+ use: ParamUse = attr(name="use", default="optional")
+ std: Optional[bool] = attr(name="std", default=True)
+
+
+HTTPQueryType = Literal["GET", "POST"]
+
+
+class ParamHTTP(Interface):
+ """A service invoked via an HTTP Query (either Get or Post) with a set of arguments consisting of keyword
+ name-value pairs.
+
+ Parameters:
+ queryType:
+ (element) - The type of HTTP request, either 'GET' or 'POST'. Max occurs 2.
+ resultType:
+ (element) - The MIME media type of a document returned in the HTTP response.
+ param:
+ (element) - A description of a input parameter that can be provided as a name=value argument.
+ testQuery:
+ (element) - An ampersand-delimited list of arguments that can be used to test this service interface.
+ """
+
+ type: Literal["vs:ParamHTTP"] = attr(name="type", default="vs:ParamHTTP", ns="xsi")
+
+ query_type: Optional[list[HTTPQueryType]] = element(tag="queryType", max_length=2, default=None)
+ result_type: Optional[str] = element(tag="resultType", default=None)
+ param: Optional[list[InputParam]] = element(tag="param", default_factory=list)
+ test_query: Optional[str] = element(tag="testQuery", default=None)
diff --git a/vo_models/voresource/__init__.py b/vo_models/voresource/__init__.py
index 5f7879b..5a8f92b 100644
--- a/vo_models/voresource/__init__.py
+++ b/vo_models/voresource/__init__.py
@@ -1,3 +1,32 @@
-"""
-Module containing VOResource classes.
-"""
+"""IVOA VOResource-v1.1.xsd pydantic-xml models"""
+
+from vo_models.voresource.models import (
+ AccessURL,
+ Capability,
+ Contact,
+ Content,
+ Creator,
+ Curation,
+ Date,
+ Interface,
+ MirrorURL,
+ Organisation,
+ Relationship,
+ Resource,
+ ResourceName,
+ Rights,
+ SecurityMethod,
+ Service,
+ Source,
+ Validation,
+ WebBrowser,
+ WebService,
+)
+from vo_models.voresource.types import (
+ AuthorityID,
+ IdentifierURI,
+ ResourceKey,
+ UTCDateTime,
+ UTCTimestamp,
+ ValidationLevel,
+)
diff --git a/vo_models/voresource/models.py b/vo_models/voresource/models.py
new file mode 100644
index 0000000..6278606
--- /dev/null
+++ b/vo_models/voresource/models.py
@@ -0,0 +1,472 @@
+"""Pydantic-xml models for IVOA schema VOResource-v1.1.xsd"""
+import datetime
+from typing import Literal, Optional
+
+from pydantic import field_validator, networks
+from pydantic_xml import BaseXmlModel, attr, element
+
+from vo_models.voresource.types import IdentifierURI, UTCTimestamp, ValidationLevel
+
+# pylint: disable=no-self-argument
+# pylint: disable=too-few-public-methods
+
+NSMAP = {
+ "vr": "http://www.ivoa.net/xml/VOResource/v1.0",
+ "xs": "http://www.w3.org/2001/XMLSchema",
+ "vm": "http://www.ivoa.net/xml/VOMetadata/v0.1",
+ "xsi": "http://www.w3.org/2001/XMLSchema-instance",
+}
+
+
+class Validation(BaseXmlModel, nsmap=NSMAP):
+ """A validation stamp combining a validation level and the ID of the validator.
+
+ Parameters:
+ validated_by:
+ (attr) - The IVOA ID of the registry or organisation that assigned the validation level.
+ """
+
+ value: ValidationLevel
+
+ validated_by: networks.AnyUrl = attr(
+ name="validatedBy",
+ )
+
+ @field_validator("value", mode="before")
+ def _validate_value(cls, values):
+ """Ensure value is a ValidationLevel instance"""
+ if isinstance(values, str):
+ if values.isdigit():
+ return ValidationLevel(int(values))
+ return values
+
+
+class ResourceName(BaseXmlModel, nsmap=NSMAP):
+ """The name of a potentially registered resource.
+
+ That is, the entity referred to may have an associated identifier.
+
+ Parameters:
+ ivo_id:
+ (attr) - The IVOA identifier for the resource referred to.
+ value:
+ (content) - The name of the resource.
+ """
+
+ value: str
+ ivo_id: Optional[IdentifierURI] = attr(name="ivo-id", default=None)
+
+
+class Date(BaseXmlModel, nsmap=NSMAP):
+ """A string indicating what the date refers to.
+
+ The value of role should be taken from the vocabulary maintained at http://www.ivoa.net/rdf/voresource/date_role.
+
+ Parameters:
+ value: The date and time of the event.
+ role:
+ (attr) - A string indicating what the date refers to.
+ """
+
+ value: UTCTimestamp
+ role: Optional[str] = attr(
+ name="role",
+ default="representative",
+ )
+
+
+class Source(BaseXmlModel, nsmap=NSMAP):
+ """A bibliographic reference from which the present resource is derived or extracted.
+
+ Parameters:
+ value: The bibliographic reference.
+ format:
+ (attr) - The reference format.
+ Recognized values include "bibcode", referring to a standard astronomical bibcode
+ (http://cdsweb.u-strasbg.fr/simbad/refcode.html).
+ """
+
+ value: networks.AnyUrl
+ format: Optional[str] = attr(name="format", default=None)
+
+
+class Rights(BaseXmlModel, nsmap=NSMAP):
+ """A statement of usage conditions.
+
+ This will typically include a license, which should be given as a full string
+ (e.g., Creative Commons Attribution 3.0 International). Further free-text information, e.g., on how to attribute or
+ on embargo periods is allowed.
+
+ Parameters:
+ value: The statement of usage conditions.
+ rights_uri:
+ (attr) - A URI identifier for a license
+ """
+
+ value: str
+ rights_uri: Optional[networks.AnyUrl] = attr(name="rightsURI", default=None)
+
+
+class AccessURL(BaseXmlModel, nsmap=NSMAP):
+ """The URL (or base URL) that a client uses to access the service.
+
+ Parameters:
+ value: The URL (or base URL) that a client uses to access the service.
+ use:
+ (attr) - A flag indicating whether this should be interpreted as a base URL, a full URL, or a URL to a
+ directory that will produce a listing of files.
+ """
+
+ value: networks.AnyUrl
+
+ use: Literal["full", "base", "dir"] = attr(name="use")
+
+
+class MirrorURL(BaseXmlModel, nsmap=NSMAP):
+ """A URL of a mirror (i.e., a functionally identical additional service interface) to
+
+ Parameters:
+ value: A URL of a mirror
+ title:
+ (attr) - A terse, human-readable phrase indicating the function or location of this mirror, e.g.,
+ "Primary Backup" or "European Mirror".
+ """
+
+ value: networks.AnyUrl
+ title: Optional[str] = attr(name="title", default=None)
+
+
+class Contact(BaseXmlModel, nsmap=NSMAP):
+ """Information allowing establishing contact, e.g., for purposes of support.
+
+ Parameters:
+ ivo_id:
+ (attr) - An IVOA identifier for the contact (typically when it is an organization).
+ name:
+ (element) - The name or title of the contact person.
+ This can be a person's name, e.g. “John P. Jones” or a group, “Archive Support Team”.
+ address:
+ (element) - The contact mailing address.
+ All components of the mailing address are given in one string,
+ e.g. “3700 San Martin Drive, Baltimore, MD 21218 USA”.
+ email:
+ (element) - The contact email address.
+ telephone:
+ (element) - The contact telephone number.
+ Complete international dialing codes should be given, e.g. “+1-410-338-1234”.
+ alt_identifier:
+ (element) - A reference to this entitiy in a non-IVOA identifier scheme, e.g., orcid.
+ Always use a URI form including a scheme here.
+ """
+
+ ivo_id: Optional[IdentifierURI] = attr(name="ivo_id", default=None)
+
+ name: ResourceName = element(tag="name")
+ address: Optional[str] = element(tag="address", default=None)
+ email: Optional[str] = element(tag="email", default=None)
+ telephone: Optional[str] = element(tag="telephone", default=None)
+ alt_identifier: Optional[list[networks.AnyUrl]] = element(tag="altIdentifier", default_factory=list)
+
+ @field_validator("name", mode="before")
+ def _validate_name(cls, values):
+ """Ensure name is a ResourceName instance"""
+ if isinstance(values, str):
+ return ResourceName(value=values)
+ return values
+
+
+class Creator(BaseXmlModel, nsmap=NSMAP):
+ """The entity (e.g. person or organisation) primarily responsible for creating something
+
+ Parameters:
+ ivo_id:
+ (attr) - An IVOA identifier for the creator (typically when it is an organization).
+ name:
+ (element) - The name or title of the creating person or organisation
+ Users of the creation should use this name in
+ subsequent credits and acknowledgements.
+ This should be exactly one name, preferably last name
+ first (as in "van der Waals, Johannes Diderik").
+ logo:
+ (element) - URL pointing to a graphical logo, which may be used to help identify the information source.
+ alt_identifier:
+ (element) - A reference to this entitiy in a non-IVOA identifier scheme, e.g., orcid. Always use a URI form
+ including a scheme here.
+ """
+
+ ivo_id: Optional[IdentifierURI] = attr(name="ivo_id", default=None)
+
+ name: ResourceName = element(tag="name")
+ logo: Optional[networks.AnyUrl] = element(tag="logo", default=None)
+ alt_identifier: Optional[list[networks.AnyUrl]] = element(tag="altIdentifier", default_factory=list)
+
+ @field_validator("name", mode="before")
+ def _validate_name(cls, values):
+ """Ensure name is a ResourceName instance"""
+ if isinstance(values, str):
+ return ResourceName(value=values)
+ return values
+
+
+class Relationship(BaseXmlModel, nsmap=NSMAP):
+ """A description of the relationship between one resource and one or more other resources.
+
+ Parameters:
+ relationship_type:
+ (element) - The named type of relationship
+ The value of relationshipType should be taken from the vocabulary at
+ http://www.ivoa.net/rdf/voresource/relationship_type.
+ related_resource:
+ (element) - the name of resource that this resource is related to.
+ """
+
+ relationship_type: str = element(tag="relationshipType")
+
+ related_resource: list[ResourceName] = element(tag="relatedResource")
+
+
+class SecurityMethod(BaseXmlModel, nsmap=NSMAP):
+ """A description of a security mechanism.
+
+ This type only allows one to refer to the mechanism via a URI. Derived types would allow for more metadata.
+
+ Parameters:
+ standard_id:
+ (attr) - A URI identifier for a standard security mechanism.
+ """
+
+ standard_id: Optional[networks.AnyUrl] = attr(name="standardID", default=None)
+
+
+class Curation(BaseXmlModel, nsmap=NSMAP):
+ """Information regarding the general curation of a resource
+
+ Parameters:
+ publisher:
+ (element) - Entity (e.g. person or organisation) responsible for making the resource available
+ creator:
+ (element) - The entity/ies (e.g. person(s) or organisation) primarily responsible for creating the content
+ or constitution of the resource.
+ contributor:
+ (element) - Entity responsible for contributions to the content of the resource
+ date:
+ (element) - Date associated with an event in the life cycle of the resource.
+ version:
+ (element) - Label associated with creation or availablilty of a version of a resource.
+ contact:
+ (element) - Information that can be used for contacting someone with regard to this resource.
+ """
+
+ publisher: ResourceName = element(tag="publisher")
+ creator: Optional[list[Creator]] = element(tag="creator", default_factory=list)
+ contributor: Optional[list[ResourceName]] = element(tag="contributor", default_factory=list)
+ date: Optional[list[Date]] = element(tag="date", default_factory=list)
+ version: Optional[str] = element(tag="version", default=None)
+ contact: list[Contact] = element(tag="contact")
+
+
+class Content(BaseXmlModel, nsmap=NSMAP):
+ """Information regarding the general content of a resource
+
+ Parameters:
+ subject:
+ (element) - A topic, object type, or other descriptive keywords about the resource.
+ Terms for Subject should be drawn from the Unified Astronomy Thesaurus (http://astrothesaurus.org).
+ description:
+ (element) - An account of the nature of the resource.
+ source:
+ (element) - A bibliographic reference from which the present resource is derived or extracted.
+ reference_url:
+ (element) - URL pointing to a human-readable document describing this resource.
+ type:
+ (element) - Nature or genre of the content of the resource.
+ Values for type should be taken from the controlled vocabulary
+ http://www.ivoa.net/rdf/voresource/content_type
+ content_level:
+ (element) - Description of the content level or intended audience.
+ Values for contentLevel should be taken from the controlled vocabulary
+ http://www.ivoa.net/rdf/voresource/content_level.
+ relationship:
+ (element) - a description of a relationship to another resource.
+ """
+
+ subject: list[str] = element(tag="subject")
+ description: str = element(tag="description")
+ source: Optional[Source] = element(tag="source", default=None)
+ reference_url: networks.AnyUrl = element(tag="referenceURL")
+ type: Optional[list[str]] = element(tag="type", default_factory=list)
+ content_level: Optional[list[str]] = element(tag="contentLevel", default_factory=list)
+ relationship: Optional[list[Relationship]] = element(tag="relationship", default_factory=list)
+
+
+class Interface(BaseXmlModel, tag="interface", nsmap=NSMAP):
+ """A description of a service interface.
+
+ Since this type is abstract, one must use an Interface subclass to describe an actual interface denoting
+ it via xsi:type.
+
+ Additional interface subtypes (beyond WebService and WebBrowser) are defined in the VODataService schema.
+
+ Parameters:
+ version:
+ (attr) - The version of a standard interface specification that this interface complies with.
+ role:
+ (attr) - A tag name that identifies the role the interface plays in the particular capability.
+ type:
+ (attr) - The xsi:type of the interface.
+ access_url:
+ (element) - The URL (or base URL) that a client uses to access the service.
+ mirror_url:
+ (element) - A (base) URL of a mirror of this interface.
+ security_method:
+ (element) - The mechanism the client must employ to authenticate to the service.
+ test_querystring:
+ (element) - Test data for exercising the service.
+ """
+
+ version: Optional[str] = attr(name="version", default=None)
+ role: Optional[str] = attr(name="role", default=None)
+ type: Optional[str] = attr(name="type", default=None, ns="xsi")
+
+ access_url: list[AccessURL] = element(tag="accessURL")
+ mirror_url: Optional[list[MirrorURL]] = element(tag="mirrorURL", default_factory=list)
+ security_method: Optional[list[SecurityMethod]] = element(tag="securityMethod", default_factory=list)
+ test_querystring: Optional[str] = element(tag="testQueryString", default=None)
+
+
+class WebBrowser(Interface, nsmap=NSMAP):
+ """A (form-based) interface intended to be accesed interactively by a user via a web browser."""
+
+ type: Literal["vr:WebBrowser"] = attr(name="type", default="vr:WebBrowser", ns="xsi")
+
+
+class WebService(Interface, nsmap=NSMAP):
+ """A Web Service that is describable by a WSDL document.
+
+ The accessURL element gives the Web Service's endpoint URL.
+
+ Parameters:
+ wsdl_url:
+ (element) - The location of the WSDL that describes this Web Service.
+ """
+
+ type: Literal["vr:WebService"] = attr(name="type", default="vr:WebService", ns="xsi")
+
+ wsdl_url: Optional[list[networks.AnyUrl]] = element(tag="wsdlURL", default_factory=list)
+
+
+class Resource(BaseXmlModel, nsmap=NSMAP):
+ """Any entity or component of a VO application that is describable and
+ identifiable by an IVOA Identifier.
+
+ Parameters:
+ created:
+ (attr) - The UTC date and time this resource metadata description was created.
+ updated:
+ (attr) - The UTC date this resource metadata description was last updated.
+ status:
+ (attr) - A tag indicating whether this resource is believed to be still actively maintained.
+ version:
+ (attr) - The VOResource XML schema version against which this instance was written.
+ validation_level:
+ (element) - A numeric grade describing the quality of the resource description, when applicable, to be used
+ to indicate the confidence an end-user can put in the resource as part of a VO application or research
+ study.
+ title:
+ (element) - The full name given to the resource
+ short_name:
+ (element) - A short name or abbreviation given to the resource.
+ One word or a few letters is recommended. No more than sixteen characters are allowed.
+ identifier:
+ (element) - Unambiguous reference to the resource conforming to the IVOA standard for identifiers
+ alt_identifier:
+ (element) - A reference to this resource in a non-IVOA identifier scheme, e.g., DOI or bibcode.
+ curation:
+ (element) - Information regarding the general curation of the resource
+ content:
+ (element) - Information regarding the general content of the resource
+ """
+
+ created: UTCTimestamp = attr(name="created")
+ updated: UTCTimestamp = attr(name="updated")
+ status: Literal["active", "inactive", "deleted"] = attr(name="status")
+ version: Optional[str] = attr(name="version", default=None)
+
+ validation_level: Optional[list[Validation]] = element(tag="validationLevel", default_factory=list)
+ title: str = element(tag="title")
+ short_name: Optional[str] = element(tag="shortName", default=None)
+ identifier: networks.AnyUrl = element(tag="identifier")
+ alt_identifier: Optional[list[networks.AnyUrl]] = element(tag="altIdentifier", default_factory=list)
+ curation: Curation = element(tag="curation")
+ content: Content = element(tag="content")
+
+ @field_validator("created", "updated")
+ def _validate_timestamps(cls, values):
+ """Ensure that the created and updated timestamps are not in the future"""
+ if values > datetime.datetime.now(datetime.timezone.utc):
+ raise ValueError(f"{values} timestamp must not be in the future")
+ return values
+
+ @field_validator("short_name")
+ def _validate_short_name(cls, values):
+ """Ensure that the short name is no more than 16 characters"""
+ if values and len(values) > 16:
+ raise ValueError("Short name must be no more than 16 characters")
+ return values
+
+
+class Organisation(Resource, nsmap=NSMAP):
+ """A named group of one or more persons brought together to pursue participation in VO applications.
+
+ Parameters:
+ facility:
+ (element) - The observatory or facility used to collect the data contained or managed by this resource.
+ instrument:
+ (element) - The instrument used to collect the data contained or managed by a resource.
+
+ """
+
+ facility: Optional[list[ResourceName]] = element(tag="facility", default_factory=list)
+ instrument: Optional[list[ResourceName]] = element(tag="instrument", default_factory=list)
+
+
+class Capability(BaseXmlModel, tag="capability", nsmap=NSMAP):
+ """A description of what the service does (in terms of context-specific behavior), and how to use it
+ (in terms of an interface)
+
+ Parameters:
+ standard_id:
+ (attr) - A URI identifier for a standard service.
+ type:
+ (attr) - A protocol-specific capability is included by specifying a vr:Capability sub-type via an xsi:type
+ attribute on this model.
+ validation_level:
+ (element) - A numeric grade describing the quality of the capability description and interface, when
+ applicable, to be used to indicate the confidence an end-user can put in the resource as part of a
+ VO application or research study.
+ description:
+ (element) - A human-readable description of what this capability provides as part of the over-all service.
+ interface:
+ (element) - A description of how to call the service to access this capability.
+ """
+
+ standard_id: networks.AnyUrl = attr(name="standardID")
+ type: Optional[str] = attr(name="type", default=None, ns="xsi")
+
+ validation_level: Optional[list[Validation]] = element(tag="validationLevel", default_factory=list)
+ description: Optional[str] = element(tag="description", default=None)
+ interface: Optional[list[Interface]] = element(tag="interface", default_factory=list)
+
+
+class Service(Resource, nsmap=NSMAP):
+ """A resource that can be invoked by a client to perform some action on its behalf.
+
+ Parameters:
+ rights:
+ (element) - Information about rights held in and over the resource.
+ capability:
+ (element) - A description of a general capability of the service and how to use it.
+ """
+
+ rights: Optional[list[Rights]] = element(tag="rights", default_factory=list)
+ capability: Optional[list[Capability]] = element(tag="capability", default_factory=list)
diff --git a/vo_models/voresource/types.py b/vo_models/voresource/types.py
index c6412ba..680b5b7 100644
--- a/vo_models/voresource/types.py
+++ b/vo_models/voresource/types.py
@@ -2,10 +2,14 @@
import re
from datetime import datetime
+from enum import Enum
+from typing import Annotated
-from pydantic import GetCoreSchemaHandler
+from pydantic import Field, GetCoreSchemaHandler
from pydantic_core import CoreSchema, core_schema
+# pylint: disable=too-few-public-methods
+
class UTCTimestamp(datetime):
"""A subclass of datetime to allow expanded handling of ISO formatted datetimes, and enforce
@@ -98,3 +102,64 @@ def isoformat(self, sep: str = "T", timespec: str = "milliseconds") -> str:
"""
iso_dt = super().isoformat(sep=sep, timespec=timespec)
return iso_dt.replace("+00:00", "Z")
+
+
+class UTCDateTime(str):
+ """A date stamp that can be given to a precision of either a day (type
+ xs:date) or seconds (type xs:dateTime). Where only a date is given,
+ it is to be interpreted as the span of the day on the UTC timezone
+ if such distinctions are relevant."""
+
+
+class ValidationLevel(Enum):
+ """
+ The allowed values for describing the resource descriptions and interfaces.
+ """
+
+ VALUE_0 = 0
+ """
+ The resource has a description that is stored in a registry. This level does not imply a compliant description.
+ """
+ VALUE_1 = 1
+ """
+ In addition to meeting the level 0 definition, the resource description conforms syntactically to this standard
+ and to the encoding scheme used.
+ """
+ VALUE_2 = 2
+ """
+ In addition to meeting the level 1 definition, the resource description refers to an existing resource that has
+ demonstrated to be functionally compliant.
+ """
+ VALUE_3 = 3
+ """
+ In addition to meeting the level 2 definition, the resource description has been inspected by a human and judged
+ to comply semantically to this standard as well as meeting any additional minimum quality criteria (e.g., providing
+ values for important but non-required metadata) set by the human inspector.
+ """
+ VALUE_4 = 4
+ """
+ In addition to meeting the level 3 definition, the resource description meets additional quality criteria set by
+ the human inspector and is therefore considered an excellent description of the resource. Consequently, the resource
+ is expected to operate well as part of a VO application or research study.
+ """
+
+
+AuthorityID = Annotated[
+ str, Field(pattern=r"[\w\d][\w\d\-_\.!~\*'\(\)\+=]{2,}", description="The authority identifier for the resource.")
+]
+
+ResourceKey = Annotated[
+ str,
+ Field(
+ pattern=r"[\w\d\-_\.!~\*'\(\)\+=]+(/[\w\d\-_\.!~\*'\(\)\+=]+)*",
+ description="The resource key for the resource.",
+ ),
+]
+
+IdentifierURI = Annotated[
+ str,
+ Field(
+ pattern=r"ivo://[\w\d][\w\d\-_\.!~\*'\(\)\+=]{2,}(/[\w\d\-_\.!~\*'\(\)\+=]+(/[\w\d\-_\.!~\*'\(\)\+=]+)*)?",
+ description="A reference to a registry record.",
+ ),
+]
diff --git a/vo_models/vosi/availability/models.py b/vo_models/vosi/availability/models.py
index c9f3f2d..b79e707 100644
--- a/vo_models/vosi/availability/models.py
+++ b/vo_models/vosi/availability/models.py
@@ -34,4 +34,4 @@ class Availability(BaseXmlModel, tag="availability", nsmap=NSMAP, skip_empty=Tru
up_since: Optional[UTCTimestamp] = element(tag="upSince", default=None)
down_at: Optional[UTCTimestamp] = element(tag="downAt", default=None)
back_at: Optional[UTCTimestamp] = element(tag="backAt", default=None)
- note: Optional[list[str]] = element(tag="note", default=None)
+ note: Optional[list[str]] = element(tag="note", default_factory=list)
diff --git a/vo_models/vosi/capabilities/__init__.py b/vo_models/vosi/capabilities/__init__.py
new file mode 100644
index 0000000..dd46bce
--- /dev/null
+++ b/vo_models/vosi/capabilities/__init__.py
@@ -0,0 +1,5 @@
+"""Module containing VOSI Capabilities classes."""
+
+from vo_models.vosi.capabilities.models import (
+ VOSICapabilities,
+)
diff --git a/vo_models/vosi/capabilities/models.py b/vo_models/vosi/capabilities/models.py
new file mode 100644
index 0000000..936c586
--- /dev/null
+++ b/vo_models/vosi/capabilities/models.py
@@ -0,0 +1,33 @@
+"""VOSICapabilities pydantic-xml models."""
+
+from typing import Union
+
+from pydantic_xml import BaseXmlModel, element
+
+from vo_models.tapregext.models import TableAccess
+from vo_models.voresource.models import NSMAP as VORESOURCE_NSMAP
+from vo_models.voresource.models import Capability
+
+NSMAP = {
+ "vosi": "http://www.ivoa.net/xml/VOSICapabilities/v1.0",
+ "xsd": "http://www.w3.org/2001/XMLSchema",
+ "xsi": "http://www.w3.org/2001/XMLSchema-instance",
+ "vs": "http://www.ivoa.net/xml/VODataService/v1.1",
+} | VORESOURCE_NSMAP
+
+
+class VOSICapabilities(BaseXmlModel, tag="capabilities", ns="vosi", nsmap=NSMAP):
+ """A listing of capabilities supported by a service
+
+ Parameters:
+ capability:
+ (element) - A capability supported by the service.
+ A protocol-specific capability is included by specifying a vr:Capability sub-type via an xsi:type
+ attribute on this element.
+ """
+
+ capability: list[Union[TableAccess, Capability]] = element(
+ tag="capability",
+ ns="",
+ default=[],
+ )