From c06361976368d96a7581e53ccbded729cac4e626 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:06:32 +0000 Subject: [PATCH 1/3] Initial plan From da8089aaac5fe6253b37bfc6de51a4b4fc8c4ccd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:20:35 +0000 Subject: [PATCH 2/3] Add documentation, tests, and comments for QLAPI.getEnvs functionality Co-authored-by: whyour <22700758+whyour@users.noreply.github.com> --- shell/preload/README_PYTHON_API.md | 348 +++++++++++++++++++++++++++++ shell/preload/client.py | 34 +++ shell/preload/sitecustomize.py | 4 + shell/preload/test_client.py | 122 ++++++++++ 4 files changed, 508 insertions(+) create mode 100644 shell/preload/README_PYTHON_API.md create mode 100644 shell/preload/test_client.py diff --git a/shell/preload/README_PYTHON_API.md b/shell/preload/README_PYTHON_API.md new file mode 100644 index 00000000..0e88220e --- /dev/null +++ b/shell/preload/README_PYTHON_API.md @@ -0,0 +1,348 @@ +# Qinglong Python API (QLAPI) Documentation + +## Overview + +The Qinglong Python API provides a convenient way to interact with the Qinglong system from Python scripts. The `QLAPI` object is automatically available in your Python scripts when they run within the Qinglong environment. + +## Availability + +The Python QLAPI is available starting from **version 2.8.0+**. If you're using an older version (e.g., v2.7.11), please upgrade to access these features. + +## Prerequisites + +- Qinglong version 2.8.0 or higher +- Python 3.6 or higher +- Running within Qinglong environment (scripts executed through Qinglong task system) + +## Usage + +The `QLAPI` object is automatically injected into your Python script's global namespace. You don't need to import anything - just use it directly: + +```python +# QLAPI is automatically available +result = QLAPI.getEnvs({"searchValue": "USER"}) +print(result) +``` + +## Available Methods + +### Environment Variables Management + +#### getEnvs +Get environment variables with optional search filter. + +```python +# Get all environment variables +envs = QLAPI.getEnvs() + +# Search for specific environment variables +envs = QLAPI.getEnvs({"searchValue": "USER"}) +``` + +**Parameters:** +- `searchValue` (optional): String to search for in environment variable names or values + +**Returns:** `EnvsResponse` with list of environment variables + +#### createEnv +Create new environment variables. + +```python +result = QLAPI.createEnv({ + "envs": [ + { + "name": "MY_VAR", + "value": "my_value", + "remarks": "My custom variable" + } + ] +}) +``` + +**Parameters:** +- `envs`: List of environment variable objects to create + +**Returns:** `EnvsResponse` + +#### updateEnv +Update an existing environment variable. + +```python +result = QLAPI.updateEnv({ + "env": { + "id": 123, + "name": "MY_VAR", + "value": "new_value", + "remarks": "Updated variable" + } +}) +``` + +**Parameters:** +- `env`: Environment variable object with id and updated fields + +**Returns:** `EnvResponse` + +#### deleteEnvs +Delete environment variables by IDs. + +```python +result = QLAPI.deleteEnvs({"ids": [123, 456]}) +``` + +**Parameters:** +- `ids`: List of environment variable IDs to delete + +**Returns:** `Response` + +#### enableEnvs +Enable environment variables. + +```python +result = QLAPI.enableEnvs({"ids": [123, 456]}) +``` + +**Parameters:** +- `ids`: List of environment variable IDs to enable + +**Returns:** `Response` + +#### disableEnvs +Disable environment variables. + +```python +result = QLAPI.disableEnvs({"ids": [123, 456]}) +``` + +**Parameters:** +- `ids`: List of environment variable IDs to disable + +**Returns:** `Response` + +#### updateEnvNames +Update names of multiple environment variables. + +```python +result = QLAPI.updateEnvNames({ + "ids": [123, 456], + "name": "NEW_NAME" +}) +``` + +**Parameters:** +- `ids`: List of environment variable IDs +- `name`: New name to set + +**Returns:** `Response` + +#### getEnvById +Get a specific environment variable by ID. + +```python +env = QLAPI.getEnvById({"id": 123}) +``` + +**Parameters:** +- `id`: Environment variable ID + +**Returns:** `EnvResponse` + +#### moveEnv +Change the position/order of an environment variable. + +```python +result = QLAPI.moveEnv({ + "id": 123, + "fromIndex": 0, + "toIndex": 5 +}) +``` + +**Parameters:** +- `id`: Environment variable ID +- `fromIndex`: Current position index +- `toIndex`: Target position index + +**Returns:** `EnvResponse` + +### Scheduled Tasks (Cron) Management + +#### getCronDetail +Get details of a scheduled task. + +```python +cron = QLAPI.getCronDetail({"log_path": "/path/to/log"}) +``` + +**Parameters:** +- `log_path`: Path to the task log file + +**Returns:** `CronResponse` + +#### createCron +Create a new scheduled task. + +```python +result = QLAPI.createCron({ + "command": "node script.js", + "schedule": "0 0 * * *", + "name": "Daily Task", + "labels": ["tag1", "tag2"], + "sub_id": None, + "extra_schedules": [], + "task_before": "", + "task_after": "" +}) +``` + +**Parameters:** +- `command`: Command to execute +- `schedule`: Cron expression +- `name`: Task name (optional) +- `labels`: List of labels (optional) +- Other optional fields + +**Returns:** `CronResponse` + +#### updateCron +Update an existing scheduled task. + +```python +result = QLAPI.updateCron({ + "id": 123, + "command": "node updated_script.js", + "schedule": "0 0 * * *", + "name": "Updated Task", + "labels": [], + "sub_id": None, + "extra_schedules": [], + "task_before": "", + "task_after": "" +}) +``` + +**Returns:** `CronResponse` + +#### deleteCrons +Delete scheduled tasks by IDs. + +```python +result = QLAPI.deleteCrons({"ids": [123, 456]}) +``` + +**Parameters:** +- `ids`: List of task IDs to delete + +**Returns:** `Response` + +### Notifications + +#### notify +Send a notification using configured notification channels. + +```python +result = QLAPI.notify("Notification Title", "Notification Content") +``` + +**Parameters:** +- First argument: Notification title +- Second argument: Notification content + +**Returns:** Notification result + +#### systemNotify +Send a system notification with custom parameters. + +```python +result = QLAPI.systemNotify({ + "title": "System Alert", + "content": "This is a system notification" +}) +``` + +**Parameters:** +- `title`: Notification title +- `content`: Notification content + +**Returns:** `Response` + +## Complete Example + +```python +""" +Example Qinglong Python script demonstrating QLAPI usage +""" + +# Get environment variables +print("Fetching environment variables...") +envs = QLAPI.getEnvs({"searchValue": "TOKEN"}) +print(f"Found {len(envs.get('data', []))} environment variables") + +# Create a new environment variable +print("Creating new environment variable...") +result = QLAPI.createEnv({ + "envs": [ + { + "name": "MY_TEST_VAR", + "value": "test_value_123", + "remarks": "Created by script" + } + ] +}) +print(f"Create result: {result}") + +# Send notification +print("Sending notification...") +QLAPI.notify("Script Completed", "The script has finished executing successfully") + +# Send system notification +QLAPI.systemNotify({ + "title": "Task Report", + "content": f"Processed {len(envs.get('data', []))} environment variables" +}) + +print("Done!") +``` + +## Error Handling + +All QLAPI methods may raise exceptions if there are errors communicating with the backend. It's recommended to use try-except blocks: + +```python +try: + envs = QLAPI.getEnvs({"searchValue": "USER"}) + print(f"Success: {envs}") +except Exception as e: + print(f"Error: {e}") + QLAPI.notify("Script Error", str(e)) +``` + +## Troubleshooting + +### AttributeError: 'BaseApi' object has no attribute 'getEnvs' + +This error occurs when using an older version of Qinglong (before v2.8.0). Solutions: + +1. **Upgrade Qinglong**: Update to version 2.8.0 or higher +2. **Check Installation**: Ensure `shell/preload/client.py` exists +3. **Verify Environment**: Make sure scripts are running within Qinglong environment + +### Module Import Errors + +The QLAPI is only available when scripts run within the Qinglong environment. If you're testing locally, you won't have access to QLAPI. + +## Technical Details + +The Python QLAPI is a wrapper around the Node.js gRPC API client. When you call a method like `QLAPI.getEnvs()`: + +1. Python Client creates a temporary Node.js script +2. Executes the corresponding Node.js API method +3. Returns the JSON result back to Python + +This architecture ensures consistency between JavaScript and Python APIs. + +## See Also + +- [JavaScript API Documentation](./client.js) +- [Sample Python Script](../../sample/ql_sample.py) +- [Sample JavaScript Script](../../sample/ql_sample.js) diff --git a/shell/preload/client.py b/shell/preload/client.py index 07bad008..4e71886f 100644 --- a/shell/preload/client.py +++ b/shell/preload/client.py @@ -1,3 +1,20 @@ +""" +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 @@ -176,6 +193,23 @@ class CronResponse(TypedDict): 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") diff --git a/shell/preload/sitecustomize.py b/shell/preload/sitecustomize.py index f4c51dd6..73708072 100644 --- a/shell/preload/sitecustomize.py +++ b/shell/preload/sitecustomize.py @@ -131,10 +131,14 @@ try: from __ql_notify__ import send + # BaseApi inherits all methods from Client (getEnvs, createEnv, etc.) + # and adds the notify method for sending notifications class BaseApi(Client): def notify(self, *args, **kwargs): return send(*args, **kwargs) + # Create QLAPI instance and make it globally available + # This allows scripts to use: QLAPI.getEnvs(), QLAPI.notify(), etc. QLAPI = BaseApi() builtins.QLAPI = QLAPI except Exception as error: diff --git a/shell/preload/test_client.py b/shell/preload/test_client.py new file mode 100644 index 00000000..4306cdc7 --- /dev/null +++ b/shell/preload/test_client.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +""" +Validation script for QLAPI Client functionality. +Tests that all methods are properly accessible through the BaseApi class. +""" + +import sys +from client import Client + + +def test_client_has_required_methods(): + """Test that Client class has all required API methods.""" + client = Client() + + required_methods = [ + 'getEnvs', + 'createEnv', + 'updateEnv', + 'deleteEnvs', + 'moveEnv', + 'disableEnvs', + 'enableEnvs', + 'updateEnvNames', + 'getEnvById', + 'systemNotify', + 'getCronDetail', + 'createCron', + 'updateCron', + 'deleteCrons', + ] + + print("Testing Client class methods...") + for method in required_methods: + assert hasattr(client, method), f"Client missing method: {method}" + assert callable(getattr(client, method)), f"Client.{method} is not callable" + print(f" ✓ {method}") + + print(f"\n✓ All {len(required_methods)} methods are present and callable") + return True + + +def test_baseapi_inheritance(): + """Test that BaseApi properly inherits from Client.""" + + # Simulate the BaseApi class from sitecustomize.py + class BaseApi(Client): + def notify(self, *args, **kwargs): + return "mock_notify_result" + + api = BaseApi() + + print("\nTesting BaseApi inheritance...") + + # Test that BaseApi has all Client methods + assert hasattr(api, 'getEnvs'), "BaseApi missing getEnvs" + assert callable(api.getEnvs), "BaseApi.getEnvs is not callable" + print(" ✓ getEnvs is accessible") + + # Test that BaseApi also has its own method + assert hasattr(api, 'notify'), "BaseApi missing notify" + assert callable(api.notify), "BaseApi.notify is not callable" + print(" ✓ notify is accessible") + + # Verify getEnvs signature + assert 'params' in api.getEnvs.__annotations__, "getEnvs missing params annotation" + print(" ✓ getEnvs has correct signature") + + print("\n✓ BaseApi properly inherits from Client and adds notify method") + return True + + +def test_method_signatures(): + """Test that methods have correct type annotations.""" + client = Client() + + print("\nTesting method signatures...") + + # Test getEnvs signature + getEnvs_annotations = client.getEnvs.__annotations__ + assert 'params' in getEnvs_annotations or 'return' in getEnvs_annotations, \ + "getEnvs missing annotations" + print(" ✓ getEnvs has type annotations") + + # Test other critical methods + assert hasattr(client, 'createEnv'), "Missing createEnv" + assert hasattr(client, 'updateEnv'), "Missing updateEnv" + print(" ✓ Critical methods present") + + print("\n✓ All method signatures are correct") + return True + + +def main(): + """Run all validation tests.""" + print("=" * 60) + print("QLAPI Client Validation Tests") + print("=" * 60) + + try: + test_client_has_required_methods() + test_baseapi_inheritance() + test_method_signatures() + + print("\n" + "=" * 60) + print("ALL TESTS PASSED ✓") + print("=" * 60) + print("\nThe QLAPI Client is working correctly.") + print("Users can safely use: QLAPI.getEnvs({'searchValue': 'USER'})") + return 0 + + except AssertionError as e: + print(f"\n❌ TEST FAILED: {e}") + return 1 + except Exception as e: + print(f"\n❌ UNEXPECTED ERROR: {e}") + import traceback + traceback.print_exc() + return 1 + + +if __name__ == "__main__": + sys.exit(main()) From c17dbdc28f747556b7d7a6fab252edb01892930f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 16:23:34 +0000 Subject: [PATCH 3/3] Address code review feedback - improve test robustness and documentation clarity Co-authored-by: whyour <22700758+whyour@users.noreply.github.com> --- shell/preload/README_PYTHON_API.md | 10 +++++++--- shell/preload/test_client.py | 13 ++++++++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/shell/preload/README_PYTHON_API.md b/shell/preload/README_PYTHON_API.md index 0e88220e..8abbb662 100644 --- a/shell/preload/README_PYTHON_API.md +++ b/shell/preload/README_PYTHON_API.md @@ -343,6 +343,10 @@ This architecture ensures consistency between JavaScript and Python APIs. ## See Also -- [JavaScript API Documentation](./client.js) -- [Sample Python Script](../../sample/ql_sample.py) -- [Sample JavaScript Script](../../sample/ql_sample.js) +For more information and examples: + +- [JavaScript API Client Source](./client.js) - The underlying Node.js gRPC client +- [Python Sample Script](../../sample/ql_sample.py) - Example Python script using QLAPI +- [JavaScript Sample Script](../../sample/ql_sample.js) - Example JavaScript script for comparison + +These files are located in the Qinglong repository under `shell/preload/` and `sample/` directories. diff --git a/shell/preload/test_client.py b/shell/preload/test_client.py index 4306cdc7..b04f3ae1 100644 --- a/shell/preload/test_client.py +++ b/shell/preload/test_client.py @@ -61,9 +61,11 @@ def test_baseapi_inheritance(): assert callable(api.notify), "BaseApi.notify is not callable" print(" ✓ notify is accessible") - # Verify getEnvs signature - assert 'params' in api.getEnvs.__annotations__, "getEnvs missing params annotation" - print(" ✓ getEnvs has correct signature") + # Verify getEnvs has type annotations (either params or return) + annotations = api.getEnvs.__annotations__ + assert len(annotations) > 0, "getEnvs should have type annotations" + assert 'return' in annotations, "getEnvs should have return type annotation" + print(" ✓ getEnvs has correct signature with type annotations") print("\n✓ BaseApi properly inherits from Client and adds notify method") return True @@ -77,8 +79,9 @@ def test_method_signatures(): # Test getEnvs signature getEnvs_annotations = client.getEnvs.__annotations__ - assert 'params' in getEnvs_annotations or 'return' in getEnvs_annotations, \ - "getEnvs missing annotations" + # Check that annotations exist - could be 'params', or just 'return' + assert len(getEnvs_annotations) > 0, "getEnvs should have type annotations" + assert 'return' in getEnvs_annotations, "getEnvs should have return type annotation" print(" ✓ getEnvs has type annotations") # Test other critical methods