From c3546b191fafa8993d7a26b674be1bca92d87963 Mon Sep 17 00:00:00 2001 From: Amit Ghadge Date: Sun, 26 Jan 2025 15:47:59 -0800 Subject: [PATCH] [community] Add Jenkins tool support. This PR are used to support Jenkins API to cover below feature, * Create Jenkin Job. * Delete Jenkin Job. * Run the Jenkin job. * Get Job Status. --- docs/docs/integrations/tools/jenkins.ipynb | 184 ++++++++++++++++++ .../tools/jenkins/__init__.py | 5 + .../langchain_community/tools/jenkins/tool.py | 114 +++++++++++ .../langchain_community/utilities/__init__.py | 4 + .../langchain_community/utilities/jenkins.py | 91 +++++++++ 5 files changed, 398 insertions(+) create mode 100644 docs/docs/integrations/tools/jenkins.ipynb create mode 100644 libs/community/langchain_community/tools/jenkins/__init__.py create mode 100644 libs/community/langchain_community/tools/jenkins/tool.py create mode 100644 libs/community/langchain_community/utilities/jenkins.py diff --git a/docs/docs/integrations/tools/jenkins.ipynb b/docs/docs/integrations/tools/jenkins.ipynb new file mode 100644 index 0000000000000..a5c9e0968ab6b --- /dev/null +++ b/docs/docs/integrations/tools/jenkins.ipynb @@ -0,0 +1,184 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Jenkins\n", + "\n", + "This notebook, go over how to use Jenkins. \n", + "First make sure that you have installed python-jenkins with the command below: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "vscode": { + "languageId": "shellscript" + } + }, + "outputs": [], + "source": [ + "%pip install --upgrade --quiet python-jenkins" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Before start using Jenkins, first setup or get authorization to access Jenkins server." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "vscode": { + "languageId": "shellscript" + } + }, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "_set_env(\"PASSWORD\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To disable the SSL Verify, set `os.environ[\"PYTHONHTTPSVERIFY\"] = \"0\"`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_community.tools.jenkins.tool import JenkinsJobRun\n", + "from langchain_community.utilities.jenkins import JenkinsAPIWrapper\n", + "\n", + "\n", + "tools = [JenkinsJobRun(\n", + " api_wrapper=JenkinsAPIWrapper(\n", + " jenkins_server=\"https://example.com\",\n", + " username=\"admin\",\n", + " password=os.environ[\"PASSWORD\"]\n", + " )\n", + ")]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can now call invoke and pass arguments." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "1. Create the Jenkins job" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "jenkins_job_content = \"\"\n", + "src_file = \"job1.xml\"\n", + "with open(src_file) as fread:\n", + " jenkins_job_content = fread.read()\n", + "tools[0].invoke({'job': \"job01\", \"config_xml\": jenkins_job_content, \"action\": \"create\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "2. Run the Jenkins Job" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tools[0].invoke({'job': \"job01\", \"parameters\": {}, \"action\": \"run\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "3. Get job info" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "resp = tools[0].invoke({'job': \"job01\", \"number\": 1, \"action\": \"status\"})\n", + "if not resp[\"inProgress\"]:\n", + " print(resp[\"result\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "4. Delete the jenkins job" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tools[0].invoke({'job': \"job01\", \"action\": \"delete\"})" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.1" + }, + "vscode": { + "interpreter": { + "hash": "3929050b09828356c9f5ebaf862d05c053d8228eddbc70f990c168e54dd824ba" + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/libs/community/langchain_community/tools/jenkins/__init__.py b/libs/community/langchain_community/tools/jenkins/__init__.py new file mode 100644 index 0000000000000..8f4dea84a1082 --- /dev/null +++ b/libs/community/langchain_community/tools/jenkins/__init__.py @@ -0,0 +1,5 @@ +"""Jenkins Tool.""" + +from langchain_community.tools.jenkins.tool import JenkinsJobRun + +__all__ = ["JenkinsJobRun"] \ No newline at end of file diff --git a/libs/community/langchain_community/tools/jenkins/tool.py b/libs/community/langchain_community/tools/jenkins/tool.py new file mode 100644 index 0000000000000..f29124c209787 --- /dev/null +++ b/libs/community/langchain_community/tools/jenkins/tool.py @@ -0,0 +1,114 @@ +"""Tool for the Jenkins API""" + +from typing import Optional, Type + +from langchain_core.callbacks import CallbackManagerForToolRun +from langchain_core.tools import BaseTool +from pydantic import BaseModel, Field + +from langchain_community.utilities.jenkins import JenkinsAPIWrapper + +class JenkinsSchema(BaseModel): + """Input for the Jenkins tool. + + + Instantiate: + + .. code-block:: python + + from tools.jenkins.tool import JenkinsJobRun + from tools.jenkins.utility import JenkinsAPIWrapper + + tools = [JenkinsJobRun( + api_wrapper=JenkinsAPIWrapper( + jenkins_server="https://jenkinsserver.com", + username="admin", + password=os.environ["PASSWORD"] + ) + )] + + Invoke directly with args: + + .. code-block:: python + + # delete jenkins job + tools[0].invoke({'job': "job01", "action": "delete"}) + + # create jenkins job + jenkins_job_content = "" + src_file = "job1.xml" + with open(src_file) as fread: + jenkins_job_content = fread.read() + tools[0].invoke({'job': "job01", "config_xml": jenkins_job_content, "action": "create"}) + + # run the jenkins job + tools[0].invoke({'job': "job01", "parameters": {}, "action": "run"}) + + # get jenkins job info by passing job number + resp = tools[0].invoke({'job': "job01", "number": 1, "action": "status"}) + if not resp["inProgress"]: + print(resp["result"]) + + """ + + job: str = Field( + description="name of the job" + ) + action: str = Field( + description="action of the job, like, create, run, delete" + ) + number: int = Field( + default=1, + description="job number" + ) + config_xml: str = Field( + default='', + description="job xml content" + ) + parameters: dict = Field( + default={}, + description="job parameters" + ) + + +class JenkinsJobRun(BaseTool): # type: ignore[override, override] + """Tool that execute the job""" + name: str = "jenkins" + description: str = ( + "A tool that is used to create, trigger and delete Jenkins jobs with specified parameters." + ) + api_wrapper: JenkinsAPIWrapper = Field(default_factory=JenkinsAPIWrapper) # type: ignore[arg-type] + args_schema: Type[BaseModel] = JenkinsSchema + + + def _run( + self, + job: str, + action: str, + number: Optional[int] = 1, + config_xml: Optional[str] = "", + parameters: Optional[dict] = {}, + run_manager: Optional[CallbackManagerForToolRun] = None, + ) -> any: + """Use the tool.""" + if action == "create": + self.api_wrapper.create_job( + job=job, + config_xml=config_xml, + ) + elif action == "run": + return self.api_wrapper.run_job( + job=job, + parameters=parameters, + ) + elif action == "delete": + self.api_wrapper.delete_job( + job=job, + ) + elif action == "status": + return self.api_wrapper.status_job( + job=job, + number=number + ) + else: + raise ValueError("'action' not matched") \ No newline at end of file diff --git a/libs/community/langchain_community/utilities/__init__.py b/libs/community/langchain_community/utilities/__init__.py index 0174d37c07045..c80e058375718 100644 --- a/libs/community/langchain_community/utilities/__init__.py +++ b/libs/community/langchain_community/utilities/__init__.py @@ -78,6 +78,9 @@ from langchain_community.utilities.infobip import ( InfobipAPIWrapper, ) + from langchain_community.utilities.jenkins import ( + JenkinsAPIWrapper + ) from langchain_community.utilities.jira import ( JiraAPIWrapper, ) @@ -199,6 +202,7 @@ "GoogleTrendsAPIWrapper", "GraphQLAPIWrapper", "InfobipAPIWrapper", + "JenkinsAPIWrapper", "JiraAPIWrapper", "LambdaWrapper", "MaxComputeAPIWrapper", diff --git a/libs/community/langchain_community/utilities/jenkins.py b/libs/community/langchain_community/utilities/jenkins.py new file mode 100644 index 0000000000000..844d1a9a299b7 --- /dev/null +++ b/libs/community/langchain_community/utilities/jenkins.py @@ -0,0 +1,91 @@ +"""Wrapper for the Jenkins API""" +import time +from typing import Any, Dict, List, Optional + +from langchain_core.utils import get_from_dict_or_env +from pydantic import BaseModel, model_validator + +class JenkinsAPIWrapper(BaseModel): + """Wrapper for Jenkins API + + To use, set the environment variables ``JENKINS_SERVER``, + ``USERNAME`` and ``PASSWORD``. OR those input can supplay by + API parameter. + """ + jenkins_client: Any + + jenkins_server: Optional[str] + username: Optional[str] + password: Optional[str] + + @model_validator(mode="before") + @classmethod + def validate_environment(cls, values: Dict) -> Any: + """Validate that api key and endpoint exists in environment.""" + jenkins_server = get_from_dict_or_env( + values, "jenkins_server", "JENKINS_SERVER" + ) + values["jenkins_server"] = jenkins_server + + username = get_from_dict_or_env( + values, "username", "USERNAME" + ) + values["username"] = username + + password = get_from_dict_or_env( + values, "password", "PASSWORD" + ) + values["password"] = password + + try: + from jenkins import Jenkins + from jenkins import NotFoundException, JenkinsException + except ImportError: + raise ImportError( + "jenkins package not found, please install it with pip install python-jenkins" + ) + + jenkins_client = Jenkins(jenkins_server, + username=username, + password=password) + values["jenkins_client"] = jenkins_client + + return values + + def delete_job( + self, + job: str + ) -> None: + try: + self.jenkins_client.delete_job(job) + except Exception as e: + pass + + def create_job( + self, + job: str, + config_xml: str + ) -> None: + self.jenkins_client.create_job( + job, config_xml) + + def run_job( + self, + job: str, + parameters: dict[str] + ) -> int: + next_build_number = self.jenkins_client.get_job_info(job)['nextBuildNumber'] + self.jenkins_client.build_job(job, parameters=parameters) + return next_build_number + + def status_job( + self, + job: str, + number: int + ) -> int: + from jenkins import NotFoundException, JenkinsException + try: + return self.jenkins_client.get_build_info(job, number) + except (NotFoundException, JenkinsException) as e: + time.sleep(5) + return "Not Started.." \ No newline at end of file