Add local 1C runtime adapter execution for CF files
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
@@ -134,7 +138,19 @@ async def runtime_import(request: RuntimeImportRequest) -> RuntimeImportResponse
|
||||
],
|
||||
dump_plan=dump_plan,
|
||||
)
|
||||
raise HTTPException(status_code=501, detail="Designer execution runner is not implemented yet")
|
||||
if source_kind in {"CF_FILE", "CFE_FILE"}:
|
||||
if not request.path:
|
||||
raise HTTPException(status_code=400, detail="path is required for CF/CFE import")
|
||||
dump_root, execution_logs = _convert_local_cf_or_cfe_to_metadata_dump(request, platform_status)
|
||||
return RuntimeImportResponse(
|
||||
status="normalized",
|
||||
mode=mode.value,
|
||||
platform_found=True,
|
||||
normalized_project=normalize_one_c_project(dump_root, project_id=request.project_id),
|
||||
diagnostics=[*platform_status.diagnostics, *execution_logs],
|
||||
dump_plan=dump_plan,
|
||||
)
|
||||
raise HTTPException(status_code=501, detail=f"Designer execution runner is not implemented yet for {source_kind}")
|
||||
|
||||
|
||||
def _mode() -> RuntimeMode:
|
||||
@@ -217,6 +233,105 @@ def _designer_dump_plan(request: RuntimeImportRequest) -> list[str]:
|
||||
return plan
|
||||
|
||||
|
||||
def _convert_local_cf_or_cfe_to_metadata_dump(
|
||||
request: RuntimeImportRequest,
|
||||
platform_status: RuntimePlatformResponse,
|
||||
) -> tuple[Path, list[str]]:
|
||||
source_kind = request.source_kind.upper()
|
||||
payload_path = Path(request.path or "")
|
||||
if not payload_path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Path not found: {request.path}")
|
||||
if not platform_status.designer_path:
|
||||
raise HTTPException(status_code=503, detail="1C Designer CLI path is not configured")
|
||||
export_root = Path(tempfile.mkdtemp(prefix=f"sfera-runtime-{request.project_id or 'project'}-"))
|
||||
builder_infobase = export_root / "builder-infobase"
|
||||
logs: list[str] = []
|
||||
|
||||
_run_designer_command(
|
||||
platform_status.designer_path,
|
||||
["CREATEINFOBASE", f"File={builder_infobase};"],
|
||||
export_root / "create-builder-infobase.log",
|
||||
"1C CREATEINFOBASE for local CF/CFE conversion",
|
||||
)
|
||||
builder_args = ["/F", str(builder_infobase)]
|
||||
artifacts_root = export_root / "artifacts"
|
||||
artifacts_root.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(payload_path, artifacts_root / payload_path.name)
|
||||
|
||||
if source_kind == "CF_FILE":
|
||||
_run_designer_command(
|
||||
platform_status.designer_path,
|
||||
[*builder_args, "/LoadCfg", str(payload_path)],
|
||||
export_root / "designer-loadcfg-local-cf.log",
|
||||
"1C LoadCfg local CF",
|
||||
)
|
||||
metadata_root = export_root / "configuration"
|
||||
metadata_root.mkdir(parents=True, exist_ok=True)
|
||||
_run_designer_command(
|
||||
platform_status.designer_path,
|
||||
[*builder_args, "/DumpConfigToFiles", str(metadata_root), "-Format", "Hierarchical"],
|
||||
export_root / "designer-dumpconfigtofiles-local-cf.log",
|
||||
"1C DumpConfigToFiles from local CF",
|
||||
)
|
||||
shutil.copyfile(payload_path, metadata_root / payload_path.name)
|
||||
logs.append("Local .cf converted to metadata export for server-side parsing.")
|
||||
return export_root, logs
|
||||
|
||||
if source_kind == "CFE_FILE":
|
||||
extension_name = str(request.metadata.get("one_c_extension") or payload_path.stem).strip()
|
||||
if not extension_name:
|
||||
raise HTTPException(status_code=400, detail="Extension name is required for local CFE conversion.")
|
||||
_run_designer_command(
|
||||
platform_status.designer_path,
|
||||
[*builder_args, "/LoadCfg", str(payload_path), "-Extension", extension_name, "/UpdateDBCfg"],
|
||||
export_root / "designer-loadcfg-local-cfe.log",
|
||||
"1C LoadCfg local CFE",
|
||||
)
|
||||
metadata_root = export_root / "extension"
|
||||
metadata_root.mkdir(parents=True, exist_ok=True)
|
||||
_run_designer_command(
|
||||
platform_status.designer_path,
|
||||
[*builder_args, "/DumpConfigToFiles", str(metadata_root), "-Format", "Hierarchical", "-Extension", extension_name],
|
||||
export_root / "designer-dumpconfigtofiles-local-cfe.log",
|
||||
"1C DumpConfigToFiles from local CFE",
|
||||
)
|
||||
shutil.copyfile(payload_path, metadata_root / payload_path.name)
|
||||
logs.append("Local .cfe converted to metadata export for server-side parsing.")
|
||||
return export_root, logs
|
||||
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported local 1C source: {source_kind}")
|
||||
|
||||
|
||||
def _designer_process_command(designer_path: str, arguments: list[str]) -> list[str]:
|
||||
path = Path(designer_path)
|
||||
if path.suffix.casefold() == ".py":
|
||||
return [sys.executable, designer_path, *arguments]
|
||||
return [designer_path, *arguments]
|
||||
|
||||
|
||||
def _run_designer_command(designer_path: str, arguments: list[str], log_path: Path, action_title: str, timeout_seconds: int = 180) -> None:
|
||||
command = _designer_process_command(designer_path, arguments)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=timeout_seconds,
|
||||
check=False,
|
||||
)
|
||||
output = completed.stdout or ""
|
||||
log_path.write_text(output, encoding="utf-8")
|
||||
if completed.returncode != 0:
|
||||
tail = output[-4000:] if output else ""
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"{action_title} failed with code {completed.returncode}. {tail}".strip(),
|
||||
)
|
||||
|
||||
|
||||
def _redact_connection_string(value: str) -> str:
|
||||
sensitive_keys = {"pwd", "password", "пароль"}
|
||||
chunks: list[str] = []
|
||||
|
||||
@@ -111,3 +111,47 @@ def test_runtime_platform_reports_capabilities(monkeypatch, tmp_path):
|
||||
payload = response.json()
|
||||
assert payload["platform_found"] is True
|
||||
assert "cf_dump_plan" in payload["capabilities"]
|
||||
|
||||
|
||||
def test_runtime_adapter_local_1c_executes_cf_dump(monkeypatch, tmp_path):
|
||||
designer = tmp_path / "fake_designer.py"
|
||||
designer.write_text(
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
args = sys.argv[1:]
|
||||
if args and args[0] == "CREATEINFOBASE":
|
||||
target = next(item for item in args[1:] if item.startswith("File="))[5:].rstrip(";")
|
||||
Path(target).mkdir(parents=True, exist_ok=True)
|
||||
raise SystemExit(0)
|
||||
if "/DumpConfigToFiles" in args:
|
||||
target = Path(args[args.index("/DumpConfigToFiles") + 1])
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
(target / "metadata.xml").write_text(
|
||||
"<Configuration><Catalog name='Контрагенты' qualifiedName='Справочник.Контрагенты' /></Configuration>",
|
||||
encoding="utf-8",
|
||||
)
|
||||
raise SystemExit(0)
|
||||
raise SystemExit(0)
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
cf_file = tmp_path / "demo.cf"
|
||||
cf_file.write_text("binary-placeholder", encoding="utf-8")
|
||||
monkeypatch.setenv("RUNTIME_ADAPTER_MODE", "local_1c")
|
||||
monkeypatch.setenv("ONEC_DESIGNER_PATH", str(designer))
|
||||
monkeypatch.setenv("ONEC_ENABLE_DESIGNER_EXECUTION", "true")
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post(
|
||||
"/runtime/import",
|
||||
json={"source_kind": "CF_FILE", "project_id": "cf-exec", "path": str(cf_file)},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["status"] == "normalized"
|
||||
assert payload["platform_found"] is True
|
||||
assert payload["normalized_project"]["project_id"] == "cf-exec"
|
||||
assert "Local .cf converted to metadata export" in "\n".join(payload["diagnostics"])
|
||||
|
||||
Reference in New Issue
Block a user