Files
arrr-erp/app/modules/documents/agent_package.py
T
2026-08-24 22:07:16 +05:30

69 lines
3.7 KiB
Python

from __future__ import annotations
import io
from pathlib import Path
import zipfile
ERP_LOCAL_AGENT_VERSION = "1.21.0"
ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
_DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0)
def build_agent_env(*, erp_base_url: str, node_code: str, node_secret: str, storage_root: str, tenant_id=None, branch_id=None, sync_interval_seconds: int = 30, request_timeout_seconds: int = 60, tunnel_enabled: bool = True, tunnel_reconnect_seconds: int = 10) -> str:
erp_base_url = (erp_base_url or "").strip().rstrip("/")
if erp_base_url.startswith("http://"):
host = erp_base_url[7:].split("/", 1)[0].split(":", 1)[0].lower()
if host not in {"localhost", "127.0.0.1", "::1"}:
erp_base_url = "https://" + erp_base_url[7:]
storage_root = (storage_root or r"D:\AuditFirmStorage").strip()
return (
f"ERP_BASE_URL={erp_base_url}\n" f"NODE_CODE={(node_code or '').strip()}\n" f"NODE_SECRET={(node_secret or '').strip()}\n"
f"STORAGE_ROOT={storage_root}\n" f"TENANT_ID={'' if tenant_id is None else tenant_id}\n" f"AUDIT_FIRM_ID={'' if tenant_id is None else tenant_id}\n"
f"BRANCH_ID={'' if branch_id is None else branch_id}\n" f"SYNC_INTERVAL_SECONDS={int(sync_interval_seconds or 30)}\n" f"POLL_INTERVAL_SECONDS={int(sync_interval_seconds or 30)}\n"
f"REQUEST_TIMEOUT_SECONDS={int(request_timeout_seconds or 60)}\n" f"TUNNEL_ENABLED={str(bool(tunnel_enabled)).lower()}\n" f"TUNNEL_RECONNECT_SECONDS={int(tunnel_reconnect_seconds or 10)}\n"
"AUTO_UPDATE=true\nAUTO_INSTALL_UPDATES=false\nUPDATE_CHECK_INTERVAL_SECONDS=300\nDASHBOARD_ENABLED=true\nDASHBOARD_HOST=127.0.0.1\nDASHBOARD_PORT=8788\n"
)
def _zip_info(name: str) -> zipfile.ZipInfo:
info = zipfile.ZipInfo(filename=name, date_time=_DETERMINISTIC_ZIP_TIMESTAMP)
info.compress_type = zipfile.ZIP_DEFLATED
info.create_system = 3
info.external_attr = (0o100644 & 0xFFFF) << 16
return info
def _write_zip_bytes(dst: zipfile.ZipFile, name: str, payload: bytes) -> None:
dst.writestr(_zip_info(name), payload)
def _build_zip(*, env_text: str | None, include_env: bool, include_admin_readme: bool) -> bytes:
if not RUNTIME_ROOT.exists():
raise RuntimeError(f"ERP Local Agent runtime is missing: {RUNTIME_ROOT}")
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as dst:
for path in sorted(RUNTIME_ROOT.rglob("*"), key=lambda item: item.as_posix()):
if not path.is_file() or "__pycache__" in path.parts:
continue
_write_zip_bytes(dst, path.relative_to(RUNTIME_ROOT).as_posix(), path.read_bytes())
if include_env and env_text is not None:
_write_zip_bytes(dst, ".env", env_text.encode("utf-8"))
if include_admin_readme:
text = (
f"ERP Local Agent {ERP_LOCAL_AGENT_VERSION}\n"
"Existing storage, WebSocket tunnel, dashboard, Tally mapping and client .act functionality are preserved.\n"
"Existing master/transaction sync and depreciation are preserved. Phase 5 adds neutral historical-ledger evidence read capability for server-side learning.\n"
"Dashboard: http://127.0.0.1:8788\n"
)
_write_zip_bytes(dst, "README_ERP_LOCAL_AGENT.txt", text.encode("utf-8"))
return buffer.getvalue()
def build_preconfigured_agent_zip(*, env_text: str, include_admin_readme: bool = False) -> bytes:
return _build_zip(env_text=env_text, include_env=True, include_admin_readme=include_admin_readme)
def build_agent_update_zip() -> bytes:
return _build_zip(env_text=None, include_env=False, include_admin_readme=False)