diff --git a/.github/workflows/pubpyruntime.yml b/.github/workflows/pubpyruntime.yml
new file mode 100644
index 00000000..542fc028
--- /dev/null
+++ b/.github/workflows/pubpyruntime.yml
@@ -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/
+
diff --git a/.github/workflows/pydantic-test.yml b/.github/workflows/pydantic-test.yml
new file mode 100644
index 00000000..8ec64710
--- /dev/null
+++ b/.github/workflows/pydantic-test.yml
@@ -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
diff --git a/.gitignore b/.gitignore
index 8d3c303c..eb84ce35 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/
@@ -21,6 +23,6 @@ tools/gradletooling/gradle-plugin/bin/
/tools/pythontooling/.venv/
/tools/pythontooling/src/vodmltools/schxslt/
/tools/pythontooling/dist/
-__pycache__/
-*.pyc
+/runtime/python/dist/
+/runtime/python/*.egg-info/
.pytest_cache/
diff --git a/AGENTS.md b/AGENTS.md
index 1b023e51..7c40bda0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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
@@ -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.
diff --git a/models/ivoa/build.gradle.kts b/models/ivoa/build.gradle.kts
index 18bc7454..c702ed46 100644
--- a/models/ivoa/build.gradle.kts
+++ b/models/ivoa/build.gradle.kts
@@ -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
}
diff --git a/models/ivoa/vo-dml/ivoa_base.vodml-binding.xml b/models/ivoa/vo-dml/ivoa_base.vodml-binding.xml
index 177e0265..5a03cf90 100644
--- a/models/ivoa/vo-dml/ivoa_base.vodml-binding.xml
+++ b/models/ivoa/vo-dml/ivoa_base.vodml-binding.xml
@@ -46,6 +46,7 @@
anyURI
String
+ str
xsd:anyURI
string
diff --git a/models/sample/test/serializationExample.vo-dml.xml b/models/sample/test/serializationExample.vo-dml.xml
index 16db494e..d69e78af 100644
--- a/models/sample/test/serializationExample.vo-dml.xml
+++ b/models/sample/test/serializationExample.vo-dml.xml
@@ -8,7 +8,7 @@
1.0
- 2026-04-01T12:30:22Z
+ 2026-04-10T16:35:10Z
ivoa
1.0
@@ -132,6 +132,18 @@
-1
+
+ SomeContent.uri
+ uri
+
+
+ ivoa:anyURI
+
+
+ 1
+ 1
+
+
types
diff --git a/models/sample/test/serializationExample.vodsl b/models/sample/test/serializationExample.vodsl
index d378e98a..6432887a 100644
--- a/models/sample/test/serializationExample.vodsl
+++ b/models/sample/test/serializationExample.vodsl
@@ -24,6 +24,7 @@ package types "" {
abstract otype BaseC {
!xmlmeta isAttribute="true"!
bname: ivoa:string "";
+
}
otype Dcont -> BaseC {
@@ -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"
diff --git a/runtime/java/src/main/java/org/ivoa/vodml/nav/ModelInstanceTraverser.java b/runtime/java/src/main/java/org/ivoa/vodml/nav/ModelInstanceTraverser.java
index 15108908..8ea14948 100644
--- a/runtime/java/src/main/java/org/ivoa/vodml/nav/ModelInstanceTraverser.java
+++ b/runtime/java/src/main/java/org/ivoa/vodml/nav/ModelInstanceTraverser.java
@@ -269,20 +269,20 @@ private ObjInfo(ObjBase inob) {
else {
alreadyVisited = false;
- if(ob.o.getClass().isArray()) { //FIXME think about arrays of primitives.... not yet in our models...
- children = Arrays.stream((Object[]) ob.o).map(ao->{return ao != null ? new ObjBase(ao): null;}).iterator();
+ if(ob.o.getClass().isArray()) {
+ children = Arrays.stream((Object[]) ob.o).map(ao->{return ao != null ? new ObjBase(ao,ob.vodmlt): null;}).iterator();
}
else if(ob.o instanceof Collection) {
Collection