qinglong/shell/preload/client.py
copilot-swe-agent[bot] da8089aaac Add documentation, tests, and comments for QLAPI.getEnvs functionality
Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
2025-11-07 16:20:35 +00:00

318 lines
8.6 KiB
Python

"""
Qinglong Python API Client
This module provides a Python interface to the Qinglong API through a gRPC-based
Node.js client. It enables Python scripts to interact with Qinglong's environment
variables, scheduled tasks, and notification systems.
The Client class is used as a base for the QLAPI object that is automatically
available in Qinglong Python scripts. Users can call methods like:
QLAPI.getEnvs({"searchValue": "USER"})
QLAPI.createEnv({"envs": [{"name": "VAR", "value": "val"}]})
QLAPI.notify("Title", "Content")
For detailed documentation, see README_PYTHON_API.md
"""
import subprocess
import json
import tempfile
import os
from typing import Dict, List, TypedDict, Optional
from functools import wraps
def error_handler(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except TypeError as e:
if "missing" in str(e):
func_name = func.__name__
annotations = func.__annotations__
param_type = next(
(t for name, t in annotations.items() if name != "return"), None
)
if param_type and hasattr(param_type, "__annotations__"):
required_fields = {
k: v
for k, v in param_type.__annotations__.items()
if not getattr(param_type, "__total__", True)
or k in getattr(param_type, "__required_keys__", set())
}
fields_str = ", ".join(
f'"{k}": {v.__name__}' for k, v in required_fields.items()
)
raise Exception(
f"{func_name}() requires a dictionary with parameters: {{{fields_str}}}"
) from None
raise Exception(f"{str(e)}") from None
except Exception as e:
error_msg = str(e)
if "Error:" in error_msg:
error_msg = error_msg.split("Error:")[-1].split("\n")[0].strip()
raise Exception(f"{error_msg}") from None
return wrapper
class EnvItem(TypedDict, total=False):
id: Optional[int]
name: Optional[str]
value: Optional[str]
remarks: Optional[str]
status: Optional[int]
position: Optional[int]
class GetEnvsParams(TypedDict, total=False):
searchValue: str
class CreateEnvParams(TypedDict):
envs: List[EnvItem]
class UpdateEnvParams(TypedDict):
env: EnvItem
class DeleteEnvsParams(TypedDict):
ids: List[int]
class MoveEnvParams(TypedDict):
id: int
fromIndex: int
toIndex: int
class DisableEnvsParams(TypedDict):
ids: List[int]
class EnableEnvsParams(TypedDict):
ids: List[int]
class UpdateEnvNamesParams(TypedDict):
ids: List[int]
name: str
class GetEnvByIdParams(TypedDict):
id: int
class SystemNotifyParams(TypedDict):
title: str
content: str
class EnvsResponse(TypedDict):
code: int
data: List[EnvItem]
message: Optional[str]
class EnvResponse(TypedDict):
code: int
data: EnvItem
message: Optional[str]
class Response(TypedDict):
code: int
message: Optional[str]
class ExtraScheduleItem(TypedDict, total=False):
schedule: Optional[str]
class CronItem(TypedDict, total=False):
id: Optional[int]
command: Optional[str]
schedule: Optional[str]
name: Optional[str]
labels: List[str]
sub_id: Optional[int]
extra_schedules: List[ExtraScheduleItem]
task_before: Optional[str]
task_after: Optional[str]
status: Optional[int]
log_path: Optional[str]
pid: Optional[int]
last_running_time: Optional[int]
last_execution_time: Optional[int]
class CreateCronParams(TypedDict):
command: str
schedule: str
name: Optional[str]
labels: List[str]
sub_id: Optional[int]
extra_schedules: List[ExtraScheduleItem]
task_before: Optional[str]
task_after: Optional[str]
class UpdateCronParams(TypedDict):
id: int
command: str
schedule: str
name: Optional[str]
labels: List[str]
sub_id: Optional[int]
extra_schedules: List[ExtraScheduleItem]
task_before: Optional[str]
task_after: Optional[str]
class DeleteCronsParams(TypedDict):
ids: List[int]
class CronDetailParams(TypedDict):
log_path: str
class CronsResponse(TypedDict):
code: int
data: List[CronItem]
message: Optional[str]
class CronResponse(TypedDict):
code: int
data: CronItem
message: Optional[str]
class Client:
"""
Qinglong API Client for Python.
This class provides methods to interact with Qinglong's API for managing
environment variables, scheduled tasks (crons), and notifications.
The client works by executing Node.js code that calls the actual gRPC API,
then returning the results to Python. This ensures consistency between
JavaScript and Python API interfaces.
Usage:
client = Client()
envs = client.getEnvs({"searchValue": "TOKEN"})
Note: This class is typically used through the QLAPI object which is
automatically available in Qinglong Python scripts.
"""
def __init__(self):
self.temp_dir = tempfile.mkdtemp(prefix="node_client_")
self.temp_script = os.path.join(self.temp_dir, "temp_script.js")
def __del__(self):
try:
if os.path.exists(self.temp_script):
os.remove(self.temp_script)
os.rmdir(self.temp_dir)
except Exception:
pass
@error_handler
def _execute_node(self, method: str, params: Dict = None) -> Dict:
node_code = f"""
const api = require('{os.getenv("QL_DIR")}/shell/preload/client.js');
(async () => {{
try {{
const result = await api.{method}({json.dumps(params) if params else ''});
console.log(JSON.stringify(result));
}} catch (error) {{
console.error(JSON.stringify({{
error: error.message,
stack: error.stack,
name: error.name
}}));
process.exit(1);
}}
}})();
"""
with open(self.temp_script, "w", encoding="utf-8") as f:
f.write(node_code)
result = subprocess.run(
["node", self.temp_script],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode != 0:
error_data = json.loads(result.stderr)
raise Exception(
f"{error_data.get('name', 'Error')}: {error_data.get('stack')}"
)
return json.loads(result.stdout)
@error_handler
def getEnvs(self, params: GetEnvsParams = None) -> EnvsResponse:
return self._execute_node("getEnvs", params)
@error_handler
def createEnv(self, data: CreateEnvParams) -> EnvsResponse:
return self._execute_node("createEnv", data)
@error_handler
def updateEnv(self, data: UpdateEnvParams) -> EnvResponse:
return self._execute_node("updateEnv", data)
@error_handler
def deleteEnvs(self, data: DeleteEnvsParams) -> Response:
return self._execute_node("deleteEnvs", data)
@error_handler
def moveEnv(self, data: MoveEnvParams) -> EnvResponse:
return self._execute_node("moveEnv", data)
@error_handler
def disableEnvs(self, data: DisableEnvsParams) -> Response:
return self._execute_node("disableEnvs", data)
@error_handler
def enableEnvs(self, data: EnableEnvsParams) -> Response:
return self._execute_node("enableEnvs", data)
@error_handler
def updateEnvNames(self, data: UpdateEnvNamesParams) -> Response:
return self._execute_node("updateEnvNames", data)
@error_handler
def getEnvById(self, data: GetEnvByIdParams) -> EnvResponse:
return self._execute_node("getEnvById", data)
@error_handler
def systemNotify(self, data: SystemNotifyParams) -> Response:
return self._execute_node("systemNotify", data)
@error_handler
def getCronDetail(self, data: CronDetailParams) -> CronResponse:
return self._execute_node("getCronDetail", data)
@error_handler
def createCron(self, data: CreateCronParams) -> CronResponse:
return self._execute_node("createCron", data)
@error_handler
def updateCron(self, data: UpdateCronParams) -> CronResponse:
return self._execute_node("updateCron", data)
@error_handler
def deleteCrons(self, data: DeleteCronsParams) -> Response:
return self._execute_node("deleteCrons", data)