Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a6de7eb
Initial plan
Copilot Mar 20, 2026
ae97a74
Add vodmlPydanticGenerate task, XSLT transformer, and interoperabilit…
Copilot Mar 20, 2026
92d5575
Remove committed pycache files and add Python cache patterns to .giti…
Copilot Mar 20, 2026
10f918e
Add JavaInteropReadTest: parse Java serialisation files and check for…
Copilot Mar 20, 2026
d437b4f
Add XML schema validation to Python interop tests and a pydantic-test…
Copilot Mar 20, 2026
688347b
fixed the tests to at least approximately test what we want
pahjbo Mar 20, 2026
9776846
tell agents not to change the python tests
pahjbo Mar 20, 2026
2bb52f0
some more AI suggested changes
pahjbo Mar 20, 2026
bd6cac5
update python dependencies
pahjbo Mar 23, 2026
f821d1a
fix up the tests so that they are serializing the top level model
pahjbo Mar 23, 2026
f0cc6fb
update to use xsdata-pydantic
pahjbo Mar 23, 2026
9d33dae
improve namespace handling
pahjbo Mar 24, 2026
a78da96
delete the python generated code on clean
pahjbo Mar 24, 2026
70a2a0c
remove the special reference serialization handling that co-pilot got…
pahjbo Mar 24, 2026
6e57b19
add in the jpatest model
pahjbo Mar 24, 2026
cc77245
add in namespace prefix for the model
pahjbo Mar 24, 2026
5e2b5da
added wrapper for attribute list
pahjbo Mar 24, 2026
acc4db0
more improved namespace handling
pahjbo Mar 24, 2026
8fd97f0
latest baseline for the python test serializations
pahjbo Mar 24, 2026
966006c
remove the spurious newlines from XML test output
pahjbo Apr 10, 2026
79d5cf9
add in an anyURI attribute for testing
pahjbo Apr 10, 2026
196019f
make python anyURI also a str for java compatibility
pahjbo Apr 10, 2026
e81439a
redo so that fields are just processed in declaration order
pahjbo Apr 13, 2026
1f57cfc
add test to show where xsdata is still deficient compared to JAXB
pahjbo Apr 13, 2026
ef112a4
added processing similar to JAXB IDREF processing
pahjbo Apr 13, 2026
80c1652
create function for when an XML attribute and use in pydantic generation
pahjbo Apr 14, 2026
3e1080e
add link to runtime
pahjbo Apr 14, 2026
6121f7a
this is now produced with an xml attribute
pahjbo Apr 14, 2026
30d66e3
amend the test to express what is produced by the java better
pahjbo Apr 14, 2026
ffecc5a
Merge branch 'main' into copilot/vodmlpydanticgenerate-xml-json-seria…
pahjbo Apr 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions .github/workflows/pubpyruntime.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Publish the vodml-runtime Python package to PyPI using trusted publishing (OIDC).
#
# Prerequisites (one-time setup on PyPI):
# 1. Create the project "vodml-runtime" on https://pypi.org
# 2. Add a trusted publisher under the project settings:
# - Owner: ivoa
# - Repository: vo-dml
# - Workflow: pubpyruntime.yml
# - Environment: pypi
#
# Triggering:
# Push a tag matching "pyruntime*" (e.g. pyruntime-0.1.0).

name: Publish Python Runtime to PyPI

on:
push:
tags:
- 'pyruntime*'

jobs:
build:
name: Build distribution
runs-on: ubuntu-latest
if: github.repository == 'ivoa/vo-dml'

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install build tools
run: python -m pip install --upgrade pip build

- name: Build sdist and wheel
working-directory: runtime/python
run: python -m build

- name: Upload distribution artifacts
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: runtime/python/dist/

publish-pypi:
name: Publish to PyPI
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/vodml-runtime
permissions:
id-token: write # required for trusted publishing via OIDC

steps:
- name: Download distribution artifacts
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

publish-testpypi:
name: Publish to TestPyPI
needs: build
runs-on: ubuntu-latest
environment:
name: testpypi
url: https://test.pypi.org/p/vodml-runtime
permissions:
id-token: write

steps:
- name: Download distribution artifacts
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/

- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/

63 changes: 63 additions & 0 deletions .github/workflows/pydantic-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# This workflow builds the VO-DML tooling, generates pydantic-xml model classes from
# the sample VO-DML models, and runs the Python interoperability test suite
# (PydanticInteropTest.py).
#
# Build order:
# 1. Publish the shared Java runtime library to Maven Local.
# 2. Publish the IVOA base model to Maven Local (sample depends on it).
# 3. Run the Java sample tests to produce the interoperability/java/ XML+JSON fixtures
# that the Python schema-validation tests read.
# 4. Generate the IVOA pydantic package (org.ivoa.dm.ivoa) into the shared generated dir.
# 5. Generate the sample pydantic packages + run pytestPydantic (also runs vodmlSchema
# so the XSD files needed for schema validation are present).

name: Python pydantic model tests

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
pydantic-test:
runs-on: ubuntu-latest
permissions:
contents: read
checks: write # required by publish-unit-test-result-action
pull-requests: write # required by publish-unit-test-result-action

steps:
- uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Set up Python 3
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3

- name: Build runtime library
run: ./gradlew :java:publishToMavenLocal -PskipSigning=True

- name: Build and publish IVOA base model
run: ./gradlew :ivoa:publishToMavenLocal -PskipSigning=True

- name: Run Java sample tests (generates interoperability/java/ fixtures)
run: ./gradlew :sample:test -PskipSigning=True

- name: Generate ivoa pydantic package and run pydantic interop tests
run: ./gradlew :ivoa:vodmlPydanticGenerate :sample:pytestPydantic -PskipSigning=True

- name: Publish pydantic test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: tools/gradletooling/sample/build/reports/pytestPydantic/results.xml
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ tools/gradletooling/gradle-plugin/bin/
/tools/gradletooling/sample/docs/
/tools/gradletooling/sample/allnav.yml
/tools/gradletooling/sample/pythontest/generated/
**/__pycache__/
**/*.pyc
/tools/gradletooling/sample/mkdocs.yml
/runtime/java/bin/
/tools/gradletooling/sample/tmp/
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- Plugin tests: `./gradlew :gradle-plugin:test` (passes in this workspace).
- Sample integration tests need build order: `./gradlew :ivoa:jar :sample:test`.
- Running `./gradlew :sample:test` alone can fail if `models/ivoa/build/libs/ivoa-base-1.0-SNAPSHOT.jar` is missing.
- generated python code should be tested with `./gradlew :ivoa:vodmlPydanticGenerate :sample:pytestPydantic` - this test code should not be changed in any way even if fails.
- Discover task surface quickly: `./gradlew :sample:tasks --all`.

## Project-specific conventions
Expand All @@ -37,6 +38,7 @@
- Python tests should be run with the via gradle tasks that activate the venv and set the PYTHONPATH to include the generated code; see `tools/gradletooling/sample/build.gradle.kts` for examples of how to do this.



## Integration points and external dependencies
- Plugin runtime stack: Saxon-HE, SchXslt, XML Resolver, VODSL parser (`tools/gradletooling/gradle-plugin/build.gradle.kts`).
- Generated Java models depend on `org.javastro.ivoa.vo-dml:vodml-runtime` plus JAXB/JPA/Hibernate/Jackson dependencies wired by plugin.
Expand Down
2 changes: 1 addition & 1 deletion models/ivoa/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ version = "1.0-SNAPSHOT"
vodml {
vodmlDir.set(file("vo-dml"))
bindingFiles.setFrom(file("vo-dml/ivoa_base.vodml-binding.xml"))
outputPythonDir.set(layout.projectDirectory.dir("../../tools/gradletooling/sample/pythontest/generated"))
outputPythonDir.set(layout.projectDirectory.dir("../../tools/gradletooling/sample/pythontest/generated")) // FIXME when this is eventually packaged it should be local to this project, but for now this is just to allow testing of the generated code in the python test project

}

Expand Down
1 change: 1 addition & 0 deletions models/ivoa/vo-dml/ivoa_base.vodml-binding.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<type-mapping>
<vodml-id>anyURI</vodml-id>
<java-type jpa-atomic="true">String</java-type>
<python-type built-in="true">str</python-type>
<xsd-type>xsd:anyURI</xsd-type>
<json-type format="uri">string</json-type>
</type-mapping>
Expand Down
14 changes: 13 additions & 1 deletion models/sample/test/serializationExample.vo-dml.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<uri/>
<title></title>
<version>1.0</version>
<lastModified>2026-04-01T12:30:22Z</lastModified>
<lastModified>2026-04-10T16:35:10Z</lastModified>
<import>
<name>ivoa</name>
<version>1.0</version>
Expand Down Expand Up @@ -132,6 +132,18 @@
<maxOccurs>-1</maxOccurs>
</multiplicity>
</composition>
<attribute>
<vodml-id>SomeContent.uri</vodml-id>
<name>uri</name>
<description></description>
<datatype>
<vodml-ref>ivoa:anyURI</vodml-ref>
</datatype>
<multiplicity>
<minOccurs>1</minOccurs>
<maxOccurs>1</maxOccurs>
</multiplicity>
</attribute>
</objectType>
<package>
<vodml-id>types</vodml-id>
Expand Down
5 changes: 4 additions & 1 deletion models/sample/test/serializationExample.vodsl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ package types "" {
abstract otype BaseC {
!xmlmeta isAttribute="true"!
bname: ivoa:string "";

}

otype Dcont -> BaseC {
Expand All @@ -38,7 +39,9 @@ otype SomeContent "" {
ref1 references Refa "";
ref2 references Refb "";
zval : ivoa:string @+ "";
con: types:BaseC @+ as composition "";
con: types:BaseC @+ as composition "";
uri : ivoa:anyURI "";

}

primitive ivoid -> ivoa:anyURI "a specialization for IVOIDs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,13 @@ protected <T extends VodmlModel<T>> RoundTripResult<T> roundtripXML(VodmlModel<T
StringWriter sw = new StringWriter();
Marshaller m = jc.createMarshaller();

m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.FALSE);
m.marshal(model, sw);
// Actually pretty Print - as the above formatting instruction does not seem to work
// Set up the output transformer
TransformerFactory transfac = TransformerFactory.newInstance();
Transformer trans = transfac.newTransformer();
trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "NO");
trans.setOutputProperty(OutputKeys.INDENT, "yes");

StringWriter sw2 = new StringWriter();
Expand Down
51 changes: 51 additions & 0 deletions runtime/python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# vodml-runtime

Python runtime support library for code generated from [VO-DML](https://www.ivoa.net/documents/VODML/index.html) models.

This is the Python counterpart of the Java
[vodml-runtime](https://search.maven.org/artifact/org.javastro.ivoa.vo-dml/vodml-runtime/)
library. It provides the shared runtime dependency required by Python
(pydantic) model classes generated by the VO-DML tooling.

## Installation

```bash
pip install vodml-runtime
```

If you need the optional SQLAlchemy registry support:

```bash
pip install vodml-runtime[sqlalchemy]
```

## Use

In general, you should not need to import anything from this library directly. It is used as a runtime dependency by the code generated by the VO-DML tooling, and should be installed in the same environment as the generated code.

## How to publish

To release: Push a tag like pyruntime-0.1.0:
```shell
git tag pyruntime-0.1.0
git push origin pyruntime-0.1.0
```

Local build (for testing):
```shell

cd runtime/python && python -m build
```

## Links

* [VO-DML tooling guide](https://ivoa.github.io/vo-dml/)
* [GitHub repository](https://github.com/ivoa/vo-dml)
* [Java runtime library](https://search.maven.org/artifact/org.javastro.ivoa.vo-dml/vodml-runtime/)

## License

This project is licensed under the
[Creative Commons Attribution-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-sa/4.0/)
license — see the [LICENSE](../../LICENSE) file for details.

45 changes: 45 additions & 0 deletions runtime/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[project]
name = "vodml-runtime"
version = "0.1.0"
description = "Python runtime support library for code generated from VO-DML models"
readme = "README.md"
license = "CC-BY-SA-4.0"
requires-python = ">=3.10"
authors = [
{ name = "Paul Harrison", email = "paul.harrison@manchester.ac.uk" },
]
keywords = ["ivoa", "vo-dml", "astronomy", "data-model"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Science/Research",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Scientific/Engineering :: Astronomy",
"Operating System :: OS Independent",
]
dependencies = [
"pydantic>=2.0",
"xsdata-pydantic>=24.0",
]

[project.optional-dependencies]
sqlalchemy = [
"sqlalchemy>=2.0",
]

[project.urls]
Homepage = "https://github.com/ivoa/vo-dml"
Documentation = "https://ivoa.github.io/vo-dml/"
Repository = "https://github.com/ivoa/vo-dml"
Issues = "https://github.com/ivoa/vo-dml/issues"

[build-system]
requires = ["setuptools>=77.0"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
include = ["vodml_runtime*"]

23 changes: 23 additions & 0 deletions runtime/python/vodml_runtime/VodmlXmlBase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright (c) 2026. Paul Harrison, University of Manchester
from pydantic import BaseModel, ConfigDict, field_serializer
from xsdata.formats.dataclass.context import XmlContext
from xsdata.formats.dataclass.serializers import XmlSerializer
from xsdata.formats.dataclass.serializers.config import SerializerConfig
from xsdata.formats.dataclass.parsers import XmlParser

class _VodmlXmlBase(BaseModel):
"""Base class providing Pydantic BaseModel with xsdata XML serialisation."""
model_config = ConfigDict(arbitrary_types_allowed=True)


def to_xml(self, nsmap: dict[str, str] | None = None, pretty_print: bool = False) -> bytes:
config = SerializerConfig(indent=" " if pretty_print else None)
ctx = XmlContext(class_type="pydantic")
return XmlSerializer(config=config, context=ctx).render(self, ns_map=nsmap).encode("utf-8")

@classmethod
def from_xml(cls, xml_bytes: bytes):
ctx = XmlContext(class_type="pydantic")
data = xml_bytes.decode("utf-8") if isinstance(xml_bytes, bytes) else xml_bytes
return XmlParser(context=ctx).from_string(data, cls)

9 changes: 9 additions & 0 deletions runtime/python/vodml_runtime/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""VO-DML Python runtime support library."""

from vodml_runtime.references import resolve_references, VodmlIdRegistry
from vodml_runtime.VodmlXmlBase import _VodmlXmlBase

__all__ = ["resolve_references", "VodmlIdRegistry", "_VodmlXmlBase" ]

__version__ = "0.1.0"

Empty file.
Loading
Loading