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

yuqi4733 pushed a commit to branch branch-1.0
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/branch-1.0 by this push:
     new 6eac0dfcd4 [#8641] feat(clients): Add Java and Python client API for 
job template alteration (#8802)
6eac0dfcd4 is described below

commit 6eac0dfcd4a0c30b2979dc09ad3ab91ef7bbfdf7
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Oct 13 16:39:38 2025 +0800

    [#8641] feat(clients): Add Java and Python client API for job template 
alteration (#8802)
    
    ### What changes were proposed in this pull request?
    
    This PR adds the Java and Python client API for job template alteration.
    
    ### Why are the changes needed?
    
    This is a part of work for job template alteration
    
    Fix: #8641
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    Add UTs.
    
    Co-authored-by: Jerry Shao <[email protected]>
    Co-authored-by: Mini Yu <[email protected]>
---
 .../org/apache/gravitino/client/DTOConverters.java |  57 +++++++++++
 .../apache/gravitino/client/GravitinoClient.java   |   7 ++
 .../apache/gravitino/client/GravitinoMetalake.java |  29 ++++++
 .../apache/gravitino/client/TestSupportsJobs.java  |  57 +++++++++++
 .../gravitino/client/dto_converters.py             |  62 +++++++++++
 .../gravitino/client/gravitino_client.py           |  19 ++++
 .../gravitino/client/gravitino_metalake.py         |  40 ++++++++
 .../gravitino/dto/job/shell_template_update_dto.py |  56 ++++++++++
 .../gravitino/dto/job/spark_template_update_dto.py |  76 ++++++++++++++
 .../gravitino/dto/job/template_update_dto.py       |  44 ++++++++
 .../dto/requests/job_template_update_request.py    | 113 +++++++++++++++++++++
 .../dto/requests/job_template_updates_request.py   |  38 +++++++
 .../tests/unittests/test_supports_jobs.py          |  26 +++++
 13 files changed, 624 insertions(+)

diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
index 4657644e1f..5b8aad2417 100644
--- 
a/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/DTOConverters.java
@@ -38,9 +38,13 @@ import org.apache.gravitino.dto.authorization.PrivilegeDTO;
 import org.apache.gravitino.dto.authorization.SecurableObjectDTO;
 import org.apache.gravitino.dto.job.JobTemplateDTO;
 import org.apache.gravitino.dto.job.ShellJobTemplateDTO;
+import org.apache.gravitino.dto.job.ShellTemplateUpdateDTO;
 import org.apache.gravitino.dto.job.SparkJobTemplateDTO;
+import org.apache.gravitino.dto.job.SparkTemplateUpdateDTO;
+import org.apache.gravitino.dto.job.TemplateUpdateDTO;
 import org.apache.gravitino.dto.requests.CatalogUpdateRequest;
 import org.apache.gravitino.dto.requests.FilesetUpdateRequest;
+import org.apache.gravitino.dto.requests.JobTemplateUpdateRequest;
 import org.apache.gravitino.dto.requests.MetalakeUpdateRequest;
 import org.apache.gravitino.dto.requests.ModelUpdateRequest;
 import org.apache.gravitino.dto.requests.ModelVersionUpdateRequest;
@@ -51,6 +55,7 @@ import org.apache.gravitino.dto.requests.TagUpdateRequest;
 import org.apache.gravitino.dto.requests.TopicUpdateRequest;
 import org.apache.gravitino.file.FilesetChange;
 import org.apache.gravitino.job.JobTemplate;
+import org.apache.gravitino.job.JobTemplateChange;
 import org.apache.gravitino.job.ShellJobTemplate;
 import org.apache.gravitino.job.SparkJobTemplate;
 import org.apache.gravitino.messaging.TopicChange;
@@ -494,4 +499,56 @@ class DTOConverters {
         throw new IllegalArgumentException("Unsupported job type: " + 
jobTemplate.jobType());
     }
   }
+
+  static JobTemplateUpdateRequest toJobTemplateUpdateRequest(JobTemplateChange 
change) {
+    if (change instanceof JobTemplateChange.RenameJobTemplate) {
+      return new JobTemplateUpdateRequest.RenameJobTemplateRequest(
+          ((JobTemplateChange.RenameJobTemplate) change).getNewName());
+
+    } else if (change instanceof JobTemplateChange.UpdateJobTemplateComment) {
+      return new JobTemplateUpdateRequest.UpdateJobTemplateCommentRequest(
+          ((JobTemplateChange.UpdateJobTemplateComment) 
change).getNewComment());
+
+    } else if (change instanceof JobTemplateChange.UpdateJobTemplate) {
+      return new JobTemplateUpdateRequest.UpdateJobTemplateContentRequest(
+          toTemplateUpdateDTO(((JobTemplateChange.UpdateJobTemplate) 
change).getTemplateUpdate()));
+
+    } else {
+      throw new IllegalArgumentException(
+          "Unknown change type: " + change.getClass().getSimpleName());
+    }
+  }
+
+  static TemplateUpdateDTO 
toTemplateUpdateDTO(JobTemplateChange.TemplateUpdate change) {
+    if (change instanceof JobTemplateChange.ShellTemplateUpdate) {
+      JobTemplateChange.ShellTemplateUpdate shellUpdate =
+          (JobTemplateChange.ShellTemplateUpdate) change;
+      return ShellTemplateUpdateDTO.builder()
+          .withNewExecutable(shellUpdate.getNewExecutable())
+          .withNewArguments(shellUpdate.getNewArguments())
+          .withNewEnvironments(shellUpdate.getNewEnvironments())
+          .withNewCustomFields(shellUpdate.getNewCustomFields())
+          .withNewScripts(shellUpdate.getNewScripts())
+          .build();
+
+    } else if (change instanceof JobTemplateChange.SparkTemplateUpdate) {
+      JobTemplateChange.SparkTemplateUpdate sparkUpdate =
+          (JobTemplateChange.SparkTemplateUpdate) change;
+      return SparkTemplateUpdateDTO.builder()
+          .withNewExecutable(sparkUpdate.getNewExecutable())
+          .withNewArguments(sparkUpdate.getNewArguments())
+          .withNewEnvironments(sparkUpdate.getNewEnvironments())
+          .withNewCustomFields(sparkUpdate.getNewCustomFields())
+          .withNewClassName(sparkUpdate.getNewClassName())
+          .withNewJars(sparkUpdate.getNewJars())
+          .withNewFiles(sparkUpdate.getNewFiles())
+          .withNewArchives(sparkUpdate.getNewArchives())
+          .withNewConfigs(sparkUpdate.getNewConfigs())
+          .build();
+
+    } else {
+      throw new IllegalArgumentException(
+          "Unknown template update type: " + 
change.getClass().getSimpleName());
+    }
+  }
 }
diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
index f22a334452..4083f2e5ff 100644
--- 
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java
@@ -61,6 +61,7 @@ import 
org.apache.gravitino.exceptions.TagAlreadyExistsException;
 import org.apache.gravitino.exceptions.UserAlreadyExistsException;
 import org.apache.gravitino.job.JobHandle;
 import org.apache.gravitino.job.JobTemplate;
+import org.apache.gravitino.job.JobTemplateChange;
 import org.apache.gravitino.job.SupportsJobs;
 import org.apache.gravitino.policy.Policy;
 import org.apache.gravitino.policy.PolicyChange;
@@ -594,6 +595,12 @@ public class GravitinoClient extends GravitinoClientBase
     return getMetalake().deleteJobTemplate(jobTemplateName);
   }
 
+  @Override
+  public JobTemplate alterJobTemplate(String jobTemplateName, 
JobTemplateChange... changes)
+      throws NoSuchJobTemplateException, IllegalArgumentException {
+    return getMetalake().alterJobTemplate(jobTemplateName, changes);
+  }
+
   @Override
   public List<JobHandle> listJobs(String jobTemplateName) throws 
NoSuchJobTemplateException {
     return getMetalake().listJobs(jobTemplateName);
diff --git 
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
 
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
index ae01555de0..0c3c43b442 100644
--- 
a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
+++ 
b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java
@@ -56,6 +56,8 @@ import 
org.apache.gravitino.dto.requests.CatalogUpdatesRequest;
 import org.apache.gravitino.dto.requests.GroupAddRequest;
 import org.apache.gravitino.dto.requests.JobRunRequest;
 import org.apache.gravitino.dto.requests.JobTemplateRegisterRequest;
+import org.apache.gravitino.dto.requests.JobTemplateUpdateRequest;
+import org.apache.gravitino.dto.requests.JobTemplateUpdatesRequest;
 import org.apache.gravitino.dto.requests.OwnerSetRequest;
 import org.apache.gravitino.dto.requests.PolicyCreateRequest;
 import org.apache.gravitino.dto.requests.PolicySetRequest;
@@ -119,6 +121,7 @@ import 
org.apache.gravitino.exceptions.TagAlreadyExistsException;
 import org.apache.gravitino.exceptions.UserAlreadyExistsException;
 import org.apache.gravitino.job.JobHandle;
 import org.apache.gravitino.job.JobTemplate;
+import org.apache.gravitino.job.JobTemplateChange;
 import org.apache.gravitino.job.SupportsJobs;
 import org.apache.gravitino.policy.Policy;
 import org.apache.gravitino.policy.PolicyChange;
@@ -1467,6 +1470,32 @@ public class GravitinoMetalake extends MetalakeDTO
     return resp.dropped();
   }
 
+  @Override
+  public JobTemplate alterJobTemplate(String jobTemplateName, 
JobTemplateChange... changes)
+      throws NoSuchJobTemplateException, IllegalArgumentException {
+    Preconditions.checkArgument(
+        StringUtils.isNotBlank(jobTemplateName), "job template name must not 
be null or empty");
+
+    List<JobTemplateUpdateRequest> updates =
+        Arrays.stream(changes)
+            .map(DTOConverters::toJobTemplateUpdateRequest)
+            .collect(Collectors.toList());
+    JobTemplateUpdatesRequest req = new JobTemplateUpdatesRequest(updates);
+
+    JobTemplateResponse resp =
+        restClient.put(
+            String.format(API_METALAKES_JOB_TEMPLATES_PATH, 
RESTUtils.encodeString(this.name()))
+                + "/"
+                + RESTUtils.encodeString(jobTemplateName),
+            req,
+            JobTemplateResponse.class,
+            Collections.emptyMap(),
+            ErrorHandlers.jobErrorHandler());
+    resp.validate();
+
+    return 
org.apache.gravitino.dto.util.DTOConverters.fromDTO(resp.getJobTemplate());
+  }
+
   @Override
   public List<JobHandle> listJobs(String jobTemplateName) throws 
NoSuchJobTemplateException {
     Preconditions.checkArgument(
diff --git 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportsJobs.java
 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportsJobs.java
index f51874f2f8..b73590b8a9 100644
--- 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportsJobs.java
+++ 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestSupportsJobs.java
@@ -32,6 +32,8 @@ import org.apache.gravitino.dto.job.ShellJobTemplateDTO;
 import org.apache.gravitino.dto.job.SparkJobTemplateDTO;
 import org.apache.gravitino.dto.requests.JobRunRequest;
 import org.apache.gravitino.dto.requests.JobTemplateRegisterRequest;
+import org.apache.gravitino.dto.requests.JobTemplateUpdateRequest;
+import org.apache.gravitino.dto.requests.JobTemplateUpdatesRequest;
 import org.apache.gravitino.dto.responses.BaseResponse;
 import org.apache.gravitino.dto.responses.DropResponse;
 import org.apache.gravitino.dto.responses.ErrorResponse;
@@ -177,6 +179,61 @@ public class TestSupportsJobs extends TestBase {
         InUseException.class, () -> 
metalake.deleteJobTemplate(jobTemplateName));
   }
 
+  @Test
+  public void testAlterJobTemplate() throws JsonProcessingException {
+    String jobTemplateName = "shell-job-template";
+    JobTemplateDTO templateDTO = newShellJobTemplateDTO(jobTemplateName);
+    JobTemplate expected = 
org.apache.gravitino.dto.util.DTOConverters.fromDTO(templateDTO);
+    JobTemplateResponse resp = new JobTemplateResponse(templateDTO);
+
+    JobTemplateUpdateRequest rename =
+        new JobTemplateUpdateRequest.RenameJobTemplateRequest(jobTemplateName);
+    JobTemplateUpdatesRequest req = new 
JobTemplateUpdatesRequest(Lists.newArrayList(rename));
+
+    buildMockResource(
+        Method.PUT, jobTemplatesPath() + "/" + jobTemplateName, req, resp, 
HttpStatus.SC_OK);
+
+    JobTemplate actual = metalake.alterJobTemplate(jobTemplateName, 
rename.jobTemplateChange());
+    Assertions.assertEquals(expected, actual);
+
+    // Test throw NoSuchJobTemplateException
+    ErrorResponse errorResp =
+        
ErrorResponse.notFound(NoSuchJobTemplateException.class.getSimpleName(), "mock 
error");
+    buildMockResource(
+        Method.PUT,
+        jobTemplatesPath() + "/" + jobTemplateName,
+        req,
+        errorResp,
+        HttpStatus.SC_NOT_FOUND);
+    Assertions.assertThrows(
+        NoSuchJobTemplateException.class,
+        () -> metalake.alterJobTemplate(jobTemplateName, 
rename.jobTemplateChange()));
+
+    // Test throw IllegalArgumentException
+    ErrorResponse errorResp2 = ErrorResponse.illegalArguments("mock error");
+    buildMockResource(
+        Method.PUT,
+        jobTemplatesPath() + "/" + jobTemplateName,
+        req,
+        errorResp2,
+        HttpStatus.SC_BAD_REQUEST);
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> metalake.alterJobTemplate(jobTemplateName, 
rename.jobTemplateChange()));
+
+    // Test throw RuntimeException
+    ErrorResponse errorResp3 = ErrorResponse.internalError("mock error");
+    buildMockResource(
+        Method.PUT,
+        jobTemplatesPath() + "/" + jobTemplateName,
+        req,
+        errorResp3,
+        HttpStatus.SC_INTERNAL_SERVER_ERROR);
+    Assertions.assertThrows(
+        RuntimeException.class,
+        () -> metalake.alterJobTemplate(jobTemplateName, 
rename.jobTemplateChange()));
+  }
+
   @Test
   public void testListJobs() throws JsonProcessingException {
     String jobTemplateName = "shell-job-template";
diff --git a/clients/client-python/gravitino/client/dto_converters.py 
b/clients/client-python/gravitino/client/dto_converters.py
index ec35d537bb..5283075318 100644
--- a/clients/client-python/gravitino/client/dto_converters.py
+++ b/clients/client-python/gravitino/client/dto_converters.py
@@ -18,6 +18,15 @@
 from gravitino.api.catalog import Catalog
 from gravitino.api.catalog_change import CatalogChange
 from gravitino.api.job.job_template import JobTemplate, JobType
+from gravitino.api.job.job_template_change import (
+    TemplateUpdate,
+    ShellTemplateUpdate,
+    SparkTemplateUpdate,
+    JobTemplateChange,
+    RenameJobTemplate,
+    UpdateJobTemplateComment,
+    UpdateJobTemplate,
+)
 from gravitino.api.job.shell_job_template import ShellJobTemplate
 from gravitino.api.job.spark_job_template import SparkJobTemplate
 from gravitino.client.fileset_catalog import FilesetCatalog
@@ -25,8 +34,17 @@ from gravitino.client.generic_model_catalog import 
GenericModelCatalog
 from gravitino.dto.catalog_dto import CatalogDTO
 from gravitino.dto.job.job_template_dto import JobTemplateDTO
 from gravitino.dto.job.shell_job_template_dto import ShellJobTemplateDTO
+from gravitino.dto.job.shell_template_update_dto import ShellTemplateUpdateDTO
 from gravitino.dto.job.spark_job_template_dto import SparkJobTemplateDTO
+from gravitino.dto.job.spark_template_update_dto import SparkTemplateUpdateDTO
+from gravitino.dto.job.template_update_dto import TemplateUpdateDTO
 from gravitino.dto.requests.catalog_update_request import CatalogUpdateRequest
+from gravitino.dto.requests.job_template_update_request import (
+    JobTemplateUpdateRequest,
+    RenameJobTemplateRequest,
+    UpdateJobTemplateCommentRequest,
+    UpdateJobTemplateContentRequest,
+)
 from gravitino.dto.requests.metalake_update_request import 
MetalakeUpdateRequest
 from gravitino.api.metalake_change import MetalakeChange
 from gravitino.utils import HTTPClient
@@ -211,3 +229,47 @@ class DTOConverters:
             )
 
         raise ValueError(f"Unsupported job type: {type(template)}")
+
+    @staticmethod
+    def to_template_update_dto(template_update: TemplateUpdate) -> 
TemplateUpdateDTO:
+        if isinstance(template_update, ShellTemplateUpdate):
+            return ShellTemplateUpdateDTO(
+                new_executable=template_update.get_new_executable(),
+                new_arguments=template_update.get_new_arguments(),
+                new_environments=template_update.get_new_environments(),
+                new_custom_fields=template_update.get_new_custom_fields(),
+                new_scripts=template_update.get_new_scripts(),
+            )
+
+        if isinstance(template_update, SparkTemplateUpdate):
+            return SparkTemplateUpdateDTO(
+                new_executable=template_update.get_new_executable(),
+                new_arguments=template_update.get_new_arguments(),
+                new_environments=template_update.get_new_environments(),
+                new_custom_fields=template_update.get_new_custom_fields(),
+                new_class_name=template_update.get_new_class_name(),
+                new_jars=template_update.get_new_jars(),
+                new_files=template_update.get_new_files(),
+                new_archives=template_update.get_new_archives(),
+                new_configs=template_update.get_new_configs(),
+            )
+
+        raise ValueError(f"Unsupported template update type: 
{type(template_update)}")
+
+    @staticmethod
+    def to_job_template_update_request(
+        change: JobTemplateChange,
+    ) -> JobTemplateUpdateRequest:
+        if isinstance(change, RenameJobTemplate):
+            return RenameJobTemplateRequest(change.get_new_name())
+
+        if isinstance(change, UpdateJobTemplateComment):
+            return UpdateJobTemplateCommentRequest(change.get_new_comment())
+
+        if isinstance(change, UpdateJobTemplate):
+            template_update_dto = DTOConverters.to_template_update_dto(
+                change.get_template_update()
+            )
+            return UpdateJobTemplateContentRequest(template_update_dto)
+
+        raise ValueError(f"Unknown change type: {type(change).__name__}")
diff --git a/clients/client-python/gravitino/client/gravitino_client.py 
b/clients/client-python/gravitino/client/gravitino_client.py
index ea27a86443..02b61cb774 100644
--- a/clients/client-python/gravitino/client/gravitino_client.py
+++ b/clients/client-python/gravitino/client/gravitino_client.py
@@ -21,6 +21,7 @@ from gravitino.api.catalog import Catalog
 from gravitino.api.catalog_change import CatalogChange
 from gravitino.api.job.job_handle import JobHandle
 from gravitino.api.job.job_template import JobTemplate
+from gravitino.api.job.job_template_change import JobTemplateChange
 from gravitino.api.job.supports_jobs import SupportsJobs
 from gravitino.auth.auth_data_provider import AuthDataProvider
 from gravitino.client.gravitino_client_base import GravitinoClientBase
@@ -163,6 +164,24 @@ class GravitinoClient(GravitinoClientBase, SupportsJobs):
         """
         return self.get_metalake().delete_job_template(job_template_name)
 
+    def alter_job_template(
+        self, job_template_name: str, *changes: JobTemplateChange
+    ) -> JobTemplate:
+        """Alters a job template with the specified changes.
+
+        Args:
+            job_template_name: The name of the job template to alter.
+            changes: The changes to apply to the job template.
+
+        Returns:
+            The altered JobTemplate object.
+
+        Raises:
+            NoSuchJobTemplateException: If no job template with the specified 
name exists.
+            IllegalArgumentException: If any of the changes cannot be applied.
+        """
+        return self.get_metalake().alter_job_template(job_template_name, 
*changes)
+
     def list_jobs(self, job_template_name: str = None) -> List[JobHandle]:
         """Lists all the jobs in the current metalake.
 
diff --git a/clients/client-python/gravitino/client/gravitino_metalake.py 
b/clients/client-python/gravitino/client/gravitino_metalake.py
index 00c393537b..86ae92b812 100644
--- a/clients/client-python/gravitino/client/gravitino_metalake.py
+++ b/clients/client-python/gravitino/client/gravitino_metalake.py
@@ -22,6 +22,7 @@ from gravitino.api.catalog import Catalog
 from gravitino.api.catalog_change import CatalogChange
 from gravitino.api.job.job_handle import JobHandle
 from gravitino.api.job.job_template import JobTemplate
+from gravitino.api.job.job_template_change import JobTemplateChange
 from gravitino.api.job.supports_jobs import SupportsJobs
 from gravitino.client.dto_converters import DTOConverters
 from gravitino.client.generic_job_handle import GenericJobHandle
@@ -33,6 +34,9 @@ from gravitino.dto.requests.job_run_request import 
JobRunRequest
 from gravitino.dto.requests.job_template_register_request import (
     JobTemplateRegisterRequest,
 )
+from gravitino.dto.requests.job_template_updates_request import (
+    JobTemplateUpdatesRequest,
+)
 from gravitino.dto.responses.catalog_list_response import CatalogListResponse
 from gravitino.dto.responses.catalog_response import CatalogResponse
 from gravitino.dto.responses.drop_response import DropResponse
@@ -365,6 +369,42 @@ class GravitinoMetalake(MetalakeDTO, SupportsJobs):
 
         return drop_response.dropped()
 
+    def alter_job_template(
+        self, job_template_name: str, *changes: JobTemplateChange
+    ) -> JobTemplate:
+        """Alter the job template with specified name by applying the changes.
+
+        Args:
+            job_template_name: the name of the job template.
+            changes: the changes to apply to the job template.
+
+        Raises:
+            NoSuchJobTemplateException if the job template with specified name 
does not exist.
+            IllegalArgumentException if the changes are invalid.
+
+        Returns:
+            the altered JobTemplate.
+        """
+
+        reqs = [
+            DTOConverters.to_job_template_update_request(change) for change in 
changes
+        ]
+        updates_request = JobTemplateUpdatesRequest(reqs)
+
+        url = (
+            
f"{self.API_METALAKES_JOB_TEMPLATES_PATH.format(encode_string(self.name()))}/"
+            f"{encode_string(job_template_name)}"
+        )
+        response = self.rest_client.put(
+            url, json=updates_request, error_handler=JOB_ERROR_HANDLER
+        )
+        job_template_response = JobTemplateResponse.from_json(
+            response.body, infer_missing=True
+        )
+        job_template_response.validate()
+
+        return 
DTOConverters.from_job_template_dto(job_template_response.job_template())
+
     def list_jobs(self, job_template_name: str = None) -> List[JobHandle]:
         """List all the jobs under this metalake.
 
diff --git 
a/clients/client-python/gravitino/dto/job/shell_template_update_dto.py 
b/clients/client-python/gravitino/dto/job/shell_template_update_dto.py
new file mode 100644
index 0000000000..1fd299f3af
--- /dev/null
+++ b/clients/client-python/gravitino/dto/job/shell_template_update_dto.py
@@ -0,0 +1,56 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from dataclasses import dataclass, field
+from typing import Optional, List
+
+from dataclasses_json import config
+
+from gravitino.api.job.job_template_change import TemplateUpdate, 
ShellTemplateUpdate
+from gravitino.dto.job.template_update_dto import TemplateUpdateDTO
+
+
+@dataclass
+class ShellTemplateUpdateDTO(TemplateUpdateDTO):
+    """DTO for updating shell job templates."""
+
+    _new_scripts: Optional[List[str]] = 
field(metadata=config(field_name="newScripts"))
+
+    def __init__(
+        self,
+        new_executable: Optional[str] = None,
+        new_arguments: Optional[List[str]] = None,
+        new_environments: Optional[dict] = None,
+        new_custom_fields: Optional[dict] = None,
+        new_scripts: Optional[List[str]] = None,
+    ):
+        super().__init__(
+            _type="shell",
+            _new_executable=new_executable,
+            _new_arguments=new_arguments,
+            _new_environments=new_environments,
+            _new_custom_fields=new_custom_fields,
+        )
+        self._new_scripts = new_scripts
+
+    def to_template_update(self) -> TemplateUpdate:
+        return ShellTemplateUpdate(
+            new_executable=self._new_executable,
+            new_arguments=self._new_arguments,
+            new_environments=self._new_environments,
+            new_custom_fields=self._new_custom_fields,
+            new_scripts=self._new_scripts,
+        )
diff --git 
a/clients/client-python/gravitino/dto/job/spark_template_update_dto.py 
b/clients/client-python/gravitino/dto/job/spark_template_update_dto.py
new file mode 100644
index 0000000000..741a2ba617
--- /dev/null
+++ b/clients/client-python/gravitino/dto/job/spark_template_update_dto.py
@@ -0,0 +1,76 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from dataclasses import dataclass, field
+from typing import Optional, List, Dict
+
+from dataclasses_json import config
+
+from gravitino.api.job.job_template_change import TemplateUpdate, 
SparkTemplateUpdate
+from gravitino.dto.job.template_update_dto import TemplateUpdateDTO
+
+
+@dataclass
+class SparkTemplateUpdateDTO(TemplateUpdateDTO):
+    """DTO for updating Spark job templates."""
+
+    _new_class_name: Optional[str] = 
field(metadata=config(field_name="newClassName"))
+    _new_jars: Optional[List[str]] = 
field(metadata=config(field_name="newJars"))
+    _new_files: Optional[List[str]] = 
field(metadata=config(field_name="newFiles"))
+    _new_archives: Optional[List[str]] = field(
+        metadata=config(field_name="newArchives")
+    )
+    _new_configs: Optional[Dict[str, str]] = field(
+        metadata=config(field_name="newConfigs")
+    )
+
+    def __init__(
+        self,
+        new_executable: Optional[str] = None,
+        new_arguments: Optional[List[str]] = None,
+        new_environments: Optional[dict] = None,
+        new_custom_fields: Optional[dict] = None,
+        new_class_name: Optional[str] = None,
+        new_jars: Optional[List[str]] = None,
+        new_files: Optional[List[str]] = None,
+        new_archives: Optional[List[str]] = None,
+        new_configs: Optional[Dict[str, str]] = None,
+    ):
+        super().__init__(
+            _type="spark",
+            _new_executable=new_executable,
+            _new_arguments=new_arguments,
+            _new_environments=new_environments,
+            _new_custom_fields=new_custom_fields,
+        )
+        self._new_class_name = new_class_name
+        self._new_jars = new_jars
+        self._new_files = new_files
+        self._new_archives = new_archives
+        self._new_configs = new_configs
+
+    def to_template_update(self) -> TemplateUpdate:
+        return SparkTemplateUpdate(
+            new_executable=self._new_executable,
+            new_arguments=self._new_arguments,
+            new_environments=self._new_environments,
+            new_custom_fields=self._new_custom_fields,
+            new_class_name=self._new_class_name,
+            new_jars=self._new_jars,
+            new_files=self._new_files,
+            new_archives=self._new_archives,
+            new_configs=self._new_configs,
+        )
diff --git a/clients/client-python/gravitino/dto/job/template_update_dto.py 
b/clients/client-python/gravitino/dto/job/template_update_dto.py
new file mode 100644
index 0000000000..9e12ba70bc
--- /dev/null
+++ b/clients/client-python/gravitino/dto/job/template_update_dto.py
@@ -0,0 +1,44 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+from typing import Optional, List, Dict
+
+from dataclasses_json import config, DataClassJsonMixin
+
+from gravitino.api.job.job_template_change import TemplateUpdate
+
+
+@dataclass
+class TemplateUpdateDTO(DataClassJsonMixin, ABC):
+    """Represents a template update data transfer object (DTO)."""
+
+    _type: str = field(metadata=config(field_name="@type"))
+    _new_executable: Optional[str] = 
field(metadata=config(field_name="newExecutable"))
+    _new_arguments: Optional[List[str]] = field(
+        metadata=config(field_name="newArguments")
+    )
+    _new_environments: Optional[Dict[str, str]] = field(
+        metadata=config(field_name="newEnvironments")
+    )
+    _new_custom_fields: Optional[Dict[str, str]] = field(
+        metadata=config(field_name="newCustomFields")
+    )
+
+    @abstractmethod
+    def to_template_update(self) -> TemplateUpdate:
+        pass
diff --git 
a/clients/client-python/gravitino/dto/requests/job_template_update_request.py 
b/clients/client-python/gravitino/dto/requests/job_template_update_request.py
new file mode 100644
index 0000000000..32f0ca0f00
--- /dev/null
+++ 
b/clients/client-python/gravitino/dto/requests/job_template_update_request.py
@@ -0,0 +1,113 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+
+from dataclasses_json import config
+
+from gravitino.api.job.job_template_change import JobTemplateChange
+from gravitino.dto.job.template_update_dto import TemplateUpdateDTO
+from gravitino.rest.rest_message import RESTRequest
+
+
+@dataclass
+class JobTemplateUpdateRequest(RESTRequest, ABC):
+    """Represents a request to update a job template."""
+
+    _type: str = field(metadata=config(field_name="@type"))
+
+    def __init__(self, action_type: str):
+        self._type = action_type
+
+    @abstractmethod
+    def job_template_change(self) -> JobTemplateChange:
+        """Converts the request to a JobTemplateChange object."""
+        pass
+
+
+@dataclass
+class RenameJobTemplateRequest(JobTemplateUpdateRequest):
+    """Request to rename a job template."""
+
+    _new_name: str = field(metadata=config(field_name="newName"))
+    """The new name for the job template."""
+
+    def __init__(self, new_name: str):
+        super().__init__("rename")
+        self._new_name = new_name
+
+    def validate(self):
+        """Validates the fields of the request.
+
+        Raises:
+            ValueError if the new name is not set.
+        """
+        if not self._new_name:
+            raise ValueError('"new_name" field is required and cannot be 
empty')
+
+    def job_template_change(self) -> JobTemplateChange:
+        return JobTemplateChange.rename(new_name=self._new_name)
+
+
+@dataclass
+class UpdateJobTemplateCommentRequest(JobTemplateUpdateRequest):
+    """Request to update the comment of a job template."""
+
+    _new_comment: str = field(metadata=config(field_name="newComment"))
+    """The new comment for the job template."""
+
+    def __init__(self, new_comment: str):
+        super().__init__("updateComment")
+        self._new_comment = new_comment
+
+    def validate(self):
+        """Validates the fields of the request.
+
+        Raises:
+            ValueError if the new comment is not set.
+        """
+        if self._new_comment is None:
+            raise ValueError('"new_comment" field is required and cannot be 
None')
+
+    def job_template_change(self) -> JobTemplateChange:
+        return JobTemplateChange.update_comment(new_comment=self._new_comment)
+
+
+@dataclass
+class UpdateJobTemplateContentRequest(JobTemplateUpdateRequest):
+    """Request to update the content of a job template."""
+
+    _new_template: TemplateUpdateDTO = 
field(metadata=config(field_name="newTemplate"))
+    """The template update details."""
+
+    def __init__(self, new_template: TemplateUpdateDTO):
+        super().__init__("updateTemplate")
+        self._new_template = new_template
+
+    def validate(self):
+        """Validates the fields of the request.
+
+        Raises:
+            ValueError if the template update is not set.
+        """
+        if self._new_template is None:
+            raise ValueError('"new_template" field is required and cannot be 
None')
+
+    def job_template_change(self) -> JobTemplateChange:
+        return JobTemplateChange.update_template(
+            self._new_template.to_template_update()
+        )
diff --git 
a/clients/client-python/gravitino/dto/requests/job_template_updates_request.py 
b/clients/client-python/gravitino/dto/requests/job_template_updates_request.py
new file mode 100644
index 0000000000..a1fd2383c7
--- /dev/null
+++ 
b/clients/client-python/gravitino/dto/requests/job_template_updates_request.py
@@ -0,0 +1,38 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from dataclasses import field, dataclass
+from typing import List
+
+from dataclasses_json import config
+
+from gravitino.dto.requests.job_template_update_request import 
JobTemplateUpdateRequest
+from gravitino.rest.rest_message import RESTRequest
+
+
+@dataclass
+class JobTemplateUpdatesRequest(RESTRequest):
+    """Represents a request to get job template updates."""
+
+    _updates: List[JobTemplateUpdateRequest] = field(
+        metadata=config(field_name="updates"), default_factory=list
+    )
+
+    def validate(self):
+        if not self._updates:
+            raise ValueError("Updates cannot be empty")
+        for update_request in self._updates:
+            update_request.validate()
diff --git a/clients/client-python/tests/unittests/test_supports_jobs.py 
b/clients/client-python/tests/unittests/test_supports_jobs.py
index 697c2a54b7..a24766d714 100644
--- a/clients/client-python/tests/unittests/test_supports_jobs.py
+++ b/clients/client-python/tests/unittests/test_supports_jobs.py
@@ -21,6 +21,7 @@ from unittest.mock import Mock, patch
 from gravitino import GravitinoClient
 from gravitino.api.job.job_handle import JobHandle
 from gravitino.api.job.job_template import JobType
+from gravitino.api.job.job_template_change import JobTemplateChange, 
ShellTemplateUpdate
 from gravitino.api.job.shell_job_template import ShellJobTemplate
 from gravitino.api.job.spark_job_template import SparkJobTemplate
 from gravitino.dto.audit_dto import AuditDTO
@@ -133,6 +134,31 @@ class TestSupportsJobs(unittest.TestCase):
             result = gravitino_client.delete_job_template(shell_template.name)
             self.assertFalse(result)
 
+    def test_alter_job_template(self, *mock_methods):
+        gravitino_client = GravitinoClient(
+            uri="http://localhost:8090";,
+            metalake_name=self._metalake_name,
+        )
+
+        shell_template = self._new_shell_job_template()
+        shell_template_dto = self._new_shell_job_template_dto(shell_template)
+        resp = JobTemplateResponse(_job_template=shell_template_dto, _code=0)
+        mock_resp = self._mock_http_response(resp.to_json())
+
+        with patch(
+            "gravitino.utils.http_client.HTTPClient.put", 
return_value=mock_resp
+        ):
+            result_template = gravitino_client.alter_job_template(
+                shell_template.name,
+                JobTemplateChange.rename(shell_template.name),
+                JobTemplateChange.update_comment(shell_template.comment),
+                JobTemplateChange.update_template(
+                    ShellTemplateUpdate(new_executable="test")
+                ),
+            )
+
+            self.assertEqual(shell_template, result_template)
+
     def test_list_jobs(self, *mock_methods):
         gravitino_client = GravitinoClient(
             uri="http://localhost:8090";,


Reply via email to