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 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"])