Add local 1C runtime adapter execution for CF files
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-22 08:10:49 +03:00
parent aa36d58a73
commit 5a4e3c6d9d
2 changed files with 160 additions and 1 deletions
@@ -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"])