feat: Enhance PluginHttpClient and SoftwareConfig for improved task handling and configuration management
This commit is contained in:
parent
08623bf4d6
commit
9008201d51
@ -65,11 +65,16 @@ class SoftwareConfig:
|
||||
def __init__(self, config_path: str):
|
||||
self.config_path = config_path
|
||||
self._config = None
|
||||
self._config_mtime: Optional[float] = None
|
||||
|
||||
def load_config(self) -> dict:
|
||||
try:
|
||||
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||
self._config = yaml.safe_load(f) or {}
|
||||
try:
|
||||
self._config_mtime = Path(self.config_path).stat().st_mtime
|
||||
except OSError:
|
||||
self._config_mtime = None
|
||||
return self._config
|
||||
except FileNotFoundError as exc:
|
||||
raise FileNotFoundError(f"Software config not found: {self.config_path}") from exc
|
||||
@ -86,6 +91,18 @@ class SoftwareConfig:
|
||||
def _ensure_loaded(self):
|
||||
if self._config is None:
|
||||
self.load_config()
|
||||
return
|
||||
|
||||
try:
|
||||
current_mtime = Path(self.config_path).stat().st_mtime
|
||||
except OSError:
|
||||
current_mtime = None
|
||||
|
||||
if self._config_mtime is None or current_mtime is None:
|
||||
return
|
||||
|
||||
if current_mtime > self._config_mtime:
|
||||
self.load_config()
|
||||
|
||||
def get_software_list(self) -> List[str]:
|
||||
self._ensure_loaded()
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from typing import Any, Dict, Optional
|
||||
from urllib import error, parse, request
|
||||
|
||||
@ -35,6 +36,14 @@ class PluginHttpClient:
|
||||
|
||||
url = parse.urljoin(base_url.rstrip("/") + "/", submit_path.lstrip("/"))
|
||||
request_payload = self._build_payload(payload=payload, body_mode=body_mode)
|
||||
if software_id == "creo" and task_type == "open_model":
|
||||
request_payload.setdefault("software_type", "creo")
|
||||
# Creo plugin expects snake_case file path keys.
|
||||
raw_path = request_payload.get("file_path") or request_payload.get("filePath")
|
||||
if isinstance(raw_path, str) and raw_path.strip():
|
||||
request_payload["file_path"] = raw_path
|
||||
request_payload.setdefault("dirname", os.path.dirname(raw_path))
|
||||
request_payload.setdefault("filename", os.path.basename(raw_path))
|
||||
|
||||
try:
|
||||
return await asyncio.to_thread(self._post_json, url, request_payload, timeout)
|
||||
@ -105,7 +114,7 @@ class PluginHttpClient:
|
||||
|
||||
@staticmethod
|
||||
def _post_json(url: str, payload: Dict[str, Any], timeout: int) -> Dict[str, Any]:
|
||||
raw = json.dumps(payload).encode("utf-8")
|
||||
raw = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
req = request.Request(
|
||||
url=url,
|
||||
data=raw,
|
||||
|
||||
@ -14,6 +14,20 @@ file_storage:
|
||||
- .rte
|
||||
plugins:
|
||||
creo:
|
||||
auto_close_exclude_task_types:
|
||||
- open_model
|
||||
- close_model
|
||||
auto_close_include_task_types:
|
||||
- shrinkwrap_shell
|
||||
- creo_shrinkwrap
|
||||
auto_close_model_after_tasks: true
|
||||
auto_open_exclude_task_types:
|
||||
- open_model
|
||||
- close_model
|
||||
auto_open_include_task_types:
|
||||
- shrinkwrap_shell
|
||||
- creo_shrinkwrap
|
||||
auto_open_model_before_tasks: true
|
||||
base_url: http://localhost:12345
|
||||
callback_timeout_sec: 60
|
||||
callback_token: creo-callback-token
|
||||
@ -23,8 +37,10 @@ plugins:
|
||||
pre_batch_cleanup_task_params:
|
||||
force_close: true
|
||||
pre_batch_cleanup_task_type: close_model
|
||||
pre_batch_cleanup_ignore_error_markers: []
|
||||
close_model_ignore_error_markers: []
|
||||
pre_batch_cleanup_ignore_error_markers:
|
||||
- No current model loaded
|
||||
close_model_ignore_error_markers:
|
||||
- No current model loaded
|
||||
request_timeout_sec: 10
|
||||
retry_backoff_sec:
|
||||
- 1
|
||||
@ -37,6 +53,7 @@ plugins:
|
||||
path: /api/model/close
|
||||
open_model:
|
||||
body_mode: file_path
|
||||
completion_mode: sync
|
||||
path: /api/model/open
|
||||
shell_analysis:
|
||||
body_mode: task_params_only
|
||||
@ -46,6 +63,10 @@ plugins:
|
||||
body_mode: task_params_only
|
||||
completion_mode: sync
|
||||
path: /api/creo/shrinkwrap/shell
|
||||
creo_shrinkwrap:
|
||||
body_mode: task_params_only
|
||||
completion_mode: sync
|
||||
path: /api/creo/shrinkwrap/shell
|
||||
pdms:
|
||||
base_url: http://localhost:9001
|
||||
callback_timeout_sec: 60
|
||||
|
||||
@ -32,7 +32,10 @@ async def test_plugin_http_client_uses_creo_open_model_endpoint_and_payload(monk
|
||||
|
||||
assert response["status"] == "success"
|
||||
assert captured["url"].endswith("/api/model/open")
|
||||
assert captured["payload"]["filePath"].endswith("part.prt")
|
||||
assert captured["payload"]["file_path"].endswith("part.prt")
|
||||
assert captured["payload"]["dirname"].endswith("models")
|
||||
assert captured["payload"]["filename"] == "part.prt"
|
||||
assert captured["payload"]["software_type"] == "creo"
|
||||
assert captured["payload"]["execution_id"] == "exec-1"
|
||||
|
||||
|
||||
@ -143,3 +146,36 @@ async def test_plugin_http_client_uses_revit_close_model_empty_payload(monkeypat
|
||||
assert response["success"] is True
|
||||
assert captured["url"].endswith("/api/close")
|
||||
assert captured["payload"] == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plugin_http_client_maps_creo_shrinkwrap_alias_to_shell_endpoint(monkeypatch):
|
||||
client = PluginHttpClient()
|
||||
captured = {}
|
||||
|
||||
def fake_post_json(url, payload, timeout):
|
||||
captured["url"] = url
|
||||
captured["payload"] = payload
|
||||
captured["timeout"] = timeout
|
||||
return {"success": True}
|
||||
|
||||
monkeypatch.setattr(PluginHttpClient, "_post_json", staticmethod(fake_post_json))
|
||||
|
||||
response = await client.submit_task(
|
||||
"creo",
|
||||
"creo_shrinkwrap",
|
||||
{
|
||||
"model_path": r"C:\\models\\overall_top_design.asm",
|
||||
"task_params": {"quality": "normal"},
|
||||
"execution_id": "exec-creo-shrinkwrap",
|
||||
"batch_id": "batch-creo-shrinkwrap",
|
||||
"item_id": "item-creo-shrinkwrap",
|
||||
"callback_url": "http://localhost/callback",
|
||||
"attempt": 0,
|
||||
},
|
||||
)
|
||||
|
||||
assert response["success"] is True
|
||||
assert captured["url"].endswith("/api/creo/shrinkwrap/shell")
|
||||
assert captured["payload"]["quality"] == "normal"
|
||||
assert captured["payload"]["execution_id"] == "exec-creo-shrinkwrap"
|
||||
|
||||
@ -27,6 +27,12 @@ class FakePluginHttpClient:
|
||||
behavior = payload.get("task_params", {}).get("behavior", "success")
|
||||
attempt = payload.get("attempt", 0)
|
||||
|
||||
if task_type == "open_model":
|
||||
return {"success": True, "code": 200}
|
||||
|
||||
if task_type == "close_model" and behavior == "no_current_model_loaded":
|
||||
return {"success": False, "error": "No current model loaded", "code": 500}
|
||||
|
||||
if task_type == "close_model" and behavior == "no_document_open":
|
||||
return {"success": False, "error": "NO_DOCUMENT_OPEN", "code": 409, "message": "没有打开的文档"}
|
||||
|
||||
@ -200,6 +206,78 @@ async def test_pre_batch_cleanup_ignores_no_document_open():
|
||||
await manager.stop()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_creo_pre_batch_cleanup_ignores_no_current_model_loaded():
|
||||
callback_registry = PluginCallbackRegistry()
|
||||
fake_client = FakePluginHttpClient(callback_registry)
|
||||
router = CadTaskRouter({".asm": "creo"})
|
||||
|
||||
creo_plugin = software_config._config.setdefault("plugins", {}).setdefault("creo", {})
|
||||
creo_plugin["pre_batch_cleanup_enabled"] = True
|
||||
creo_plugin["pre_batch_cleanup_task_type"] = "close_model"
|
||||
creo_plugin["pre_batch_cleanup_task_params"] = {"behavior": "no_current_model_loaded"}
|
||||
creo_plugin["pre_batch_cleanup_ignore_error_markers"] = ["No current model loaded"]
|
||||
creo_plugin["auto_open_model_before_tasks"] = False
|
||||
creo_plugin["auto_close_model_after_tasks"] = False
|
||||
creo_plugin.setdefault("tasks", {}).setdefault("close_model", {})["completion_mode"] = "sync"
|
||||
creo_plugin.setdefault("tasks", {}).setdefault("shrinkwrap_shell", {})["completion_mode"] = "submit_only"
|
||||
|
||||
manager = CadBatchManager(task_router=router, plugin_client=fake_client, callback_registry=callback_registry)
|
||||
await manager.start()
|
||||
|
||||
request = BatchSubmitRequest(
|
||||
items=[BatchSubmitItem(model_path="a.asm", task_type="shrinkwrap_shell", task_params={})],
|
||||
)
|
||||
|
||||
try:
|
||||
batch = await manager.create_batch(request, submitter_id="tester")
|
||||
final_batch = await _wait_batch_terminal(manager, batch.id)
|
||||
items = await manager.get_batch_items(batch.id)
|
||||
|
||||
assert final_batch.status == BatchStatus.COMPLETED
|
||||
assert items[0].status.value == "succeeded"
|
||||
assert len(fake_client.calls) == 2
|
||||
assert fake_client.calls[0]["task_type"] == "close_model"
|
||||
assert fake_client.calls[1]["task_type"] == "shrinkwrap_shell"
|
||||
finally:
|
||||
await manager.stop()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_creo_auto_open_runs_before_creo_shrinkwrap():
|
||||
callback_registry = PluginCallbackRegistry()
|
||||
fake_client = FakePluginHttpClient(callback_registry)
|
||||
router = CadTaskRouter({".asm": "creo"})
|
||||
|
||||
creo_plugin = software_config._config.setdefault("plugins", {}).setdefault("creo", {})
|
||||
creo_plugin["pre_batch_cleanup_enabled"] = False
|
||||
creo_plugin["auto_open_model_before_tasks"] = True
|
||||
creo_plugin["auto_open_include_task_types"] = ["creo_shrinkwrap"]
|
||||
creo_plugin["auto_close_model_after_tasks"] = False
|
||||
creo_plugin.setdefault("tasks", {}).setdefault("open_model", {})["completion_mode"] = "sync"
|
||||
creo_plugin.setdefault("tasks", {}).setdefault("creo_shrinkwrap", {})["completion_mode"] = "submit_only"
|
||||
|
||||
manager = CadBatchManager(task_router=router, plugin_client=fake_client, callback_registry=callback_registry)
|
||||
await manager.start()
|
||||
|
||||
request = BatchSubmitRequest(
|
||||
items=[BatchSubmitItem(model_path="a.asm", task_type="creo_shrinkwrap", task_params={})],
|
||||
)
|
||||
|
||||
try:
|
||||
batch = await manager.create_batch(request, submitter_id="tester")
|
||||
final_batch = await _wait_batch_terminal(manager, batch.id)
|
||||
items = await manager.get_batch_items(batch.id)
|
||||
|
||||
assert final_batch.status == BatchStatus.COMPLETED
|
||||
assert items[0].status.value == "succeeded"
|
||||
assert len(fake_client.calls) == 2
|
||||
assert fake_client.calls[0]["task_type"] == "open_model"
|
||||
assert fake_client.calls[1]["task_type"] == "creo_shrinkwrap"
|
||||
finally:
|
||||
await manager.stop()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_close_ignores_no_document_open():
|
||||
callback_registry = PluginCallbackRegistry()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user