Files
sap-cli-skill/assets/sapcli/commands/batch.py
T
wurangyu 1e39b6da88 feat: sap-cli skill v2.1.0 — self-contained distributable package
- assets/: sap-cli source (v2.1.0, 26 commands, 16 object types)
- references/: tool constraints + error handling (self-contained)
- scripts/setup.py: one-click install/config/verify
- SKILL.md: full command reference + dual-platform install guide
- VERSION: 2.1.0

Built from D:/Codespace/sap-cli via scripts/pack_skill.py
2026-06-13 20:52:24 +08:00

329 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""批量操作命令:init / refresh / sync-all。"""
from __future__ import annotations
import argparse
import logging
import os
from sapcli.client import ADTClient
from sapcli.commands.crud import _sync_single
from sapcli.exceptions import ConfigError, CyclicDependencyError
from sapcli.manifest import Manifest, ManifestEntry, init_manifest
from sapcli.scanner import scan_project
from sapcli.sorter import topological_sort
from sapcli.types import parse_object_name
logger = logging.getLogger("sapcli.commands.batch")
def cmd_init(args: argparse.Namespace, client: ADTClient) -> None:
"""项目初始化:扫描本地文件 → 查询 SAP → 生成 manifest.json。"""
from datetime import datetime, timezone
from sapcli.types import parse_object_name
project_path = os.path.abspath(args.path)
if not os.path.isdir(project_path):
raise ConfigError(f"项目目录不存在: {project_path}")
print("=" * 60)
print(" sap-cli 项目初始化")
print("=" * 60)
print(f" 项目路径: {project_path}")
# ── Step 1: 扫描本地文件 ──
print(f"\n → 正在扫描本地文件...")
scanned = scan_project(project_path)
if not scanned:
print(" 未扫描到 .abap 文件")
print(" 请确认项目目录结构是否符合约定:")
print(" reports/, classes/, interfaces/, functions/, domains/, ...")
return
print(f" ✓ 扫描到 {len(scanned)} 个本地文件\n")
# ── Step 2: 逐个查询 SAP 状态 ──
print(f" → 正在查询 SAP 系统状态...")
manifest = init_manifest(project_path)
counts = {"active": 0, "inactive": 0, "not_exists": 0}
for obj in scanned:
parsed = parse_object_name(obj.name, obj.type)
try:
status_info = client.get_object_status(parsed.obj_uri)
except Exception as e:
logger.warning("查询 %s 状态失败: %s", obj.name, e)
status_info = {"exists": False, "status": "not_exists", "corr_nr": None}
system_status = status_info["status"]
corr_nr = status_info["corr_nr"]
counts[system_status] = counts.get(system_status, 0) + 1
# 状态图标
if system_status == "active":
icon = "✓"
corr_display = f"corr: {corr_nr or '本地对象'}"
elif system_status == "inactive":
icon = "⚠"
corr_display = f"corr: {corr_nr or '本地对象'}"
else:
icon = "✗"
corr_display = "不存在"
print(f" {icon} {obj.name:<20s} ({obj.type:<10s}) {system_status:<12s} {corr_display}")
entry = ManifestEntry(
name=obj.name,
type=obj.type,
file=obj.file,
system_status=system_status,
corr_nr=corr_nr,
depends_on=[],
last_sync=None,
last_sync_result="pending",
)
manifest.upsert(entry)
# ── Step 3: 保存清单 ──
manifest.save()
total = sum(counts.values())
print(f"\n ✓ 清单已生成: {os.path.join(project_path, 'manifest.json')}")
print(f" {total} 个对象: "
f"{counts.get('active', 0)} 已激活, "
f"{counts.get('inactive', 0)} 未激活, "
f"{counts.get('not_exists', 0)} 不存在")
def cmd_refresh(args: argparse.Namespace, client: ADTClient) -> None:
"""刷新清单:重新查询 SAP 状态,更新 system_status 和 corr_nr。"""
from datetime import datetime, timezone
from sapcli.types import parse_object_name
project_path = os.path.abspath(args.path)
manifest = Manifest.load(project_path)
print("=" * 60)
print(" sap-cli 清单刷新")
print("=" * 60)
print(f" 项目路径: {project_path}")
print(f" 清单对象: {len(manifest.objects)}\n")
print(f" → 正在查询 SAP 系统状态...")
changed = 0
unchanged = 0
for name, entry in manifest.objects.items():
parsed = parse_object_name(name, entry.type)
old_status = entry.system_status
old_corr = entry.corr_nr
try:
status_info = client.get_object_status(parsed.obj_uri)
except Exception as e:
logger.warning("查询 %s 状态失败: %s", name, e)
print(f" ✗ {name:<20s} ({entry.type:<10s}) 查询失败: {e}")
unchanged += 1
continue
new_status = status_info["status"]
new_corr = status_info["corr_nr"]
# 更新
entry.system_status = new_status
entry.corr_nr = new_corr
# 对比变化
status_changed = old_status != new_status
corr_changed = old_corr != new_corr
is_changed = status_changed or corr_changed
if is_changed:
changed += 1
changes = []
if status_changed:
changes.append(f"状态变更: {old_status}{new_status}")
if corr_changed:
changes.append(f"传输请求: {old_corr or '无'}{new_corr or '无'}")
change_str = ", ".join(changes)
print(f" ~ {name:<20s} ({entry.type:<10s}) {new_status:<12s} corr: {new_corr or 'null':<15s} ({change_str})")
else:
unchanged += 1
print(f" ✓ {name:<20s} ({entry.type:<10s}) {new_status:<12s} corr: {new_corr or 'null':<15s} (未变化)")
now = datetime.now(timezone.utc).isoformat(timespec="seconds")
manifest.last_refresh = now
manifest.save()
print(f"\n ✓ 刷新完成")
print(f" 变更: {changed} 个 | 未变化: {unchanged} 个")
def cmd_sync_all(args: argparse.Namespace, client: ADTClient) -> None:
"""批量同步:读取清单 → 拓扑排序 → 逐个同步 → 更新清单。"""
from datetime import datetime, timezone
from sapcli.types import parse_object_name
project_path = os.path.abspath(args.path)
manifest = Manifest.load(project_path)
dry_run = getattr(args, "dry_run", False)
fail_fast = getattr(args, "fail_fast", False)
all_entries = list(manifest.objects.values())
total = len(all_entries)
print("=" * 60)
if dry_run:
print(" sap-cli 批量同步 (dry-run)")
else:
print(" sap-cli 批量同步")
print("=" * 60)
print(f" 项目路径: {project_path}")
print(f" 清单对象: {total}")
# ── Step 1: 扫描本地文件 vs 清单差异 ──
scanned = scan_project(project_path)
scanned_names = {s.name for s in scanned}
manifest_names = set(manifest.objects.keys())
new_files = scanned_names - manifest_names
missing_files = manifest_names - scanned_names
if new_files:
print(f"\n{len(new_files)} 个本地文件未纳入清单,建议先 create:")
for name in sorted(new_files):
print(f" - {name}")
# ── Step 2: 过滤待处理对象 ──
pending = []
skipped_up_to_date = []
for entry in all_entries:
# 检查本地文件是否存在
abs_file = manifest.file_path(entry)
if entry.name in missing_files or not os.path.isfile(abs_file):
print(f"\n{entry.name} 本地文件缺失,将跳过")
skipped_up_to_date.append(entry)
continue
# 跳过已同步且激活的对象
if entry.system_status == "active" and entry.last_sync_result == "success":
skipped_up_to_date.append(entry)
continue
pending.append(entry)
# ── Step 3: 拓扑排序 ──
try:
sorted_entries = topological_sort(all_entries)
except CyclicDependencyError as e:
print(f"\n{e}")
raise
# ── Step 4: dry-run 模式 ──
if dry_run:
print(f"\n 执行计划(按依赖排序):")
pending_set = {e.name for e in pending}
idx = 0
for entry in sorted_entries:
idx += 1
if entry in skipped_up_to_date and entry.name not in pending_set:
print(f" {idx}. {entry.name:<20s} ({entry.type:<10s}) → 跳过 [已是最新]")
elif entry.name in missing_files:
print(f" {idx}. {entry.name:<20s} ({entry.type:<10s}) → 跳过 [文件缺失]")
else:
status_info = f"{entry.system_status} → active"
deps = entry.depends_on
dep_str = f" [依赖: {', '.join(deps)}]" if deps else ""
print(f" {idx}. {entry.name:<20s} ({entry.type:<10s}) → sync [{status_info}]{dep_str}")
print(f"\n 待处理: {len(pending)} 个 | 跳过: {total - len(pending)} 个")
return
# ── Step 5: 执行同步 ──
print(f" 待处理: {len(pending)} 个对象\n")
succeeded: list[str] = []
failed: list[tuple[str, str]] = [] # (name, reason)
skipped_deps: list[str] = [] # 因依赖失败而跳过的
failed_set: set[str] = set() # 失败的对象名集合(用于级联跳过)
# 建立排序后的顺序映射
sorted_order = {entry.name: i for i, entry in enumerate(sorted_entries)}
pending_sorted = sorted(pending, key=lambda e: sorted_order.get(e.name, 999))
for i, entry in enumerate(pending_sorted, 1):
# 检查依赖是否全部成功
dep_failed = [d for d in entry.depends_on if d in failed_set]
if dep_failed:
skipped_deps.append(entry.name)
failed_set.add(entry.name)
entry.last_sync_result = "skipped"
print(f" ⊘ [{i}/{len(pending_sorted)}] {entry.name} — 跳过(依赖失败: {', '.join(dep_failed)}")
continue
abs_file = manifest.file_path(entry)
print(f"\n ── [{i}/{len(pending_sorted)}] {entry.name} ({entry.type}) ──")
# 如果对象不存在,先创建
if entry.system_status == "not_exists":
parsed = parse_object_name(entry.name, entry.type)
print(f" ℹ 对象不存在,自动创建空对象...")
try:
if entry.type == "function":
group_name = entry.name.split("/", 1)[0]
if not client.function_group_exists(group_name):
print(f" → 函数组 {group_name} 不存在,自动创建...")
client.create_function_group(group_name)
print(f" ✓ 函数组 {group_name} 创建成功")
client.create_object(entry.type, entry.name, entry.name, source=None)
print(f" ✓ 空对象创建成功")
except Exception as e:
error_msg = f"创建失败: {e}"
print(f" ✗ {error_msg}")
failed.append((entry.name, error_msg))
failed_set.add(entry.name)
entry.last_sync_result = "failed"
if fail_fast:
break
continue
# 执行同步
success, error_msg, actual_corr_nr = _sync_single(
entry.name, entry.type, abs_file, client, quiet=False,
)
now = datetime.now(timezone.utc).isoformat(timespec="seconds")
if success:
succeeded.append(entry.name)
entry.system_status = "active"
entry.corr_nr = actual_corr_nr
entry.last_sync = now
entry.last_sync_result = "success"
else:
failed.append((entry.name, error_msg or "未知错误"))
failed_set.add(entry.name)
entry.last_sync = now
entry.last_sync_result = "failed"
print(f" ⊘ 跳过(标记为 failed")
if fail_fast:
break
# ── Step 6: 保存并输出汇总 ──
manifest.save()
print(f"\n{'=' * 60}")
print(f" 批量同步完成")
print(f"{'=' * 60}")
if succeeded:
print(f" ✓ 成功: {', '.join(succeeded)}")
if failed:
for name, reason in failed:
print(f" ✗ 失败: {name} ({reason})")
if skipped_deps:
print(f" ⊘ 跳过: {', '.join(skipped_deps)}")
if not failed and not skipped_deps:
print(f" ⊘ 跳过: (无)")
print(f" 清单已更新: {os.path.join(project_path, 'manifest.json')}")