This is an automated email from the ASF dual-hosted git repository.

yasithdev pushed a commit to branch feat/generic-experiment-launcher
in repository https://gitbox.apache.org/repos/asf/airavata-portals.git

commit d0c3e210cb90400736a432da72ca086720945d86
Author: yasithdev <[email protected]>
AuthorDate: Fri Apr 24 21:21:56 2026 -0400

    feat(launcher): DRF serializers + JSON contract schemas
---
 .../apps/api/launcher_serializers.py               | 48 +++++++++++++++++
 .../apps/api/tests/test_launcher_serializers.py    | 62 ++++++++++++++++++++++
 .../tests/contracts/experiment-draft.schema.json   | 51 ++++++++++++++++++
 .../tests/contracts/preview-response.schema.json   | 11 ++++
 4 files changed, 172 insertions(+)

diff --git 
a/airavata-django-portal/django_airavata/apps/api/launcher_serializers.py 
b/airavata-django-portal/django_airavata/apps/api/launcher_serializers.py
new file mode 100644
index 000000000..af186fc4d
--- /dev/null
+++ b/airavata-django-portal/django_airavata/apps/api/launcher_serializers.py
@@ -0,0 +1,48 @@
+import re
+
+from rest_framework import serializers
+
+
+WALLTIME_RE = re.compile(r"^\d{1,3}:[0-5]\d:[0-5]\d$")
+
+
+class RuntimeSerializer(serializers.Serializer):
+    compute_resource_id = serializers.CharField()
+    partition = serializers.CharField()
+    walltime = serializers.CharField()
+    nodes = serializers.IntegerField(min_value=1)
+    cpus_per_node = serializers.IntegerField(min_value=1)
+
+    def validate_walltime(self, value: str) -> str:
+        if not WALLTIME_RE.match(value):
+            raise serializers.ValidationError("walltime must match HH:MM:SS or 
HHH:MM:SS")
+        return value
+
+
+class StorageRefSerializer(serializers.Serializer):
+    storage_id = serializers.CharField()
+    path = serializers.CharField()
+
+
+class ExperimentDraftSerializer(serializers.Serializer):
+    name = serializers.CharField(min_length=1, max_length=256)
+    project_id = serializers.CharField()
+    description = serializers.CharField(required=False, allow_blank=True, 
default="")
+    app_id = serializers.CharField()
+    interface_name = serializers.CharField()
+    inputs = serializers.DictField(child=serializers.JSONField())
+    outputs = serializers.DictField(child=StorageRefSerializer())
+    runtime = RuntimeSerializer()
+
+    def validate_inputs(self, value):
+        # Each input is either a scalar (str/int/float/bool) or a {storage_id, 
path} object.
+        for name, v in value.items():
+            if isinstance(v, dict):
+                StorageRefSerializer(data=v).is_valid(raise_exception=True)
+        return value
+
+
+class PreviewResponseSerializer(serializers.Serializer):
+    invocation_command = serializers.CharField()
+    script_contents = serializers.CharField()
+    warnings = serializers.ListField(child=serializers.CharField(), 
default=list)
diff --git 
a/airavata-django-portal/django_airavata/apps/api/tests/test_launcher_serializers.py
 
b/airavata-django-portal/django_airavata/apps/api/tests/test_launcher_serializers.py
new file mode 100644
index 000000000..b1101d277
--- /dev/null
+++ 
b/airavata-django-portal/django_airavata/apps/api/tests/test_launcher_serializers.py
@@ -0,0 +1,62 @@
+import json
+from pathlib import Path
+from unittest import TestCase
+
+import jsonschema
+
+from django_airavata.apps.api import launcher_serializers
+
+
+CONTRACTS = Path(__file__).resolve().parents[4] / "tests" / "contracts"
+
+
+class ExperimentDraftSerializerTest(TestCase):
+    def _valid_draft(self) -> dict:
+        return {
+            "name": "test-run",
+            "project_id": "proj-1",
+            "app_id": "namd",
+            "interface_name": "run",
+            "inputs": {
+                "sim_dir": {"storage_id": "my-home", "path": "/home/x/sim"},
+                "steps": 1000,
+            },
+            "outputs": {"trajectory": {"storage_id": "my-home", "path": 
"/home/x/out.dcd"}},
+            "runtime": {
+                "compute_resource_id": "bridges-2",
+                "partition": "RM",
+                "walltime": "01:00:00",
+                "nodes": 1,
+                "cpus_per_node": 64,
+            },
+        }
+
+    def test_valid_draft_passes_schema(self):
+        schema = json.loads((CONTRACTS / 
"experiment-draft.schema.json").read_text())
+        jsonschema.validate(self._valid_draft(), schema)
+
+    def test_serializer_accepts_valid_draft(self):
+        serializer = 
launcher_serializers.ExperimentDraftSerializer(data=self._valid_draft())
+        self.assertTrue(serializer.is_valid(), serializer.errors)
+
+    def test_serializer_rejects_missing_runtime(self):
+        draft = self._valid_draft()
+        del draft["runtime"]
+        serializer = launcher_serializers.ExperimentDraftSerializer(data=draft)
+        self.assertFalse(serializer.is_valid())
+        self.assertIn("runtime", serializer.errors)
+
+    def test_serializer_rejects_bad_walltime(self):
+        draft = self._valid_draft()
+        draft["runtime"]["walltime"] = "not-a-time"
+        serializer = launcher_serializers.ExperimentDraftSerializer(data=draft)
+        self.assertFalse(serializer.is_valid())
+
+
+class PreviewResponseSchemaTest(TestCase):
+    def test_valid_response(self):
+        schema = json.loads((CONTRACTS / 
"preview-response.schema.json").read_text())
+        jsonschema.validate(
+            {"invocation_command": "bash run.sh", "script_contents": 
"#!/bin/bash\n", "warnings": []},
+            schema,
+        )
diff --git 
a/airavata-django-portal/tests/contracts/experiment-draft.schema.json 
b/airavata-django-portal/tests/contracts/experiment-draft.schema.json
new file mode 100644
index 000000000..0007626c3
--- /dev/null
+++ b/airavata-django-portal/tests/contracts/experiment-draft.schema.json
@@ -0,0 +1,51 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema";,
+  "title": "ExperimentDraft",
+  "type": "object",
+  "required": ["name", "project_id", "app_id", "interface_name", "inputs", 
"outputs", "runtime"],
+  "properties": {
+    "name": {"type": "string", "minLength": 1, "maxLength": 256},
+    "project_id": {"type": "string"},
+    "description": {"type": "string"},
+    "app_id": {"type": "string"},
+    "interface_name": {"type": "string"},
+    "inputs": {
+      "type": "object",
+      "additionalProperties": {
+        "oneOf": [
+          {"type": ["string", "number", "boolean"]},
+          {
+            "type": "object",
+            "required": ["storage_id", "path"],
+            "properties": {
+              "storage_id": {"type": "string"},
+              "path": {"type": "string"}
+            }
+          }
+        ]
+      }
+    },
+    "outputs": {
+      "type": "object",
+      "additionalProperties": {
+        "type": "object",
+        "required": ["storage_id", "path"],
+        "properties": {
+          "storage_id": {"type": "string"},
+          "path": {"type": "string"}
+        }
+      }
+    },
+    "runtime": {
+      "type": "object",
+      "required": ["compute_resource_id", "partition", "walltime", "nodes", 
"cpus_per_node"],
+      "properties": {
+        "compute_resource_id": {"type": "string"},
+        "partition": {"type": "string"},
+        "walltime": {"type": "string", "pattern": 
"^\\d{1,3}:[0-5]\\d:[0-5]\\d$"},
+        "nodes": {"type": "integer", "minimum": 1},
+        "cpus_per_node": {"type": "integer", "minimum": 1}
+      }
+    }
+  }
+}
diff --git 
a/airavata-django-portal/tests/contracts/preview-response.schema.json 
b/airavata-django-portal/tests/contracts/preview-response.schema.json
new file mode 100644
index 000000000..cb734f59c
--- /dev/null
+++ b/airavata-django-portal/tests/contracts/preview-response.schema.json
@@ -0,0 +1,11 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema";,
+  "title": "PreviewResponse",
+  "type": "object",
+  "required": ["invocation_command", "script_contents", "warnings"],
+  "properties": {
+    "invocation_command": {"type": "string", "minLength": 1},
+    "script_contents": {"type": "string", "minLength": 1},
+    "warnings": {"type": "array", "items": {"type": "string"}}
+  }
+}

Reply via email to