Add local 1C runtime adapter execution for CF files
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -134,7 +138,19 @@ async def runtime_import(request: RuntimeImportRequest) -> RuntimeImportResponse
|
|||||||
],
|
],
|
||||||
dump_plan=dump_plan,
|
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:
|
def _mode() -> RuntimeMode:
|
||||||
@@ -217,6 +233,105 @@ def _designer_dump_plan(request: RuntimeImportRequest) -> list[str]:
|
|||||||
return plan
|
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:
|
def _redact_connection_string(value: str) -> str:
|
||||||
sensitive_keys = {"pwd", "password", "пароль"}
|
sensitive_keys = {"pwd", "password", "пароль"}
|
||||||
chunks: list[str] = []
|
chunks: list[str] = []
|
||||||
|
|||||||
@@ -111,3 +111,47 @@ def test_runtime_platform_reports_capabilities(monkeypatch, tmp_path):
|
|||||||
payload = response.json()
|
payload = response.json()
|
||||||
assert payload["platform_found"] is True
|
assert payload["platform_found"] is True
|
||||||
assert "cf_dump_plan" in payload["capabilities"]
|
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